1年前くらいからRaspberryPi2をサブPCとして使っていたのですが、今年に入ってからRaspberryPi4に乗り換えました。
このままRaspberryPi2をジャンク箱の肥やしにするのももったいないというのと、たまに「今日家の鍵閉めたっけ?」となるのを何とかしたかったので、玄関の施錠状態が変わったときにSlackbotで通知するシステムを作ってみました。
親機

子機

子機側は公式ファームウェアである「無線タグアプリ」をそのまま使用。
親機側は主に以下4つのプログラムを書きました。
以下に内容を示します。
※Spring Initializrなどで自動生成されたソースファイルは省略しています
■動作確認用webページ (KeyWatcherController.java)
| package jp.hontako.keywatcher.controller;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;@Controllerpublic class KeyWatcherController {    @RequestMapping("/")    public String index() {        return "index";    }} | 
■センサ情報の保持クラス (RecieveData.java)
| package jp.hontako.keywatcher;import java.util.regex.Matcher;import java.util.regex.Pattern;import lombok.Getter;/** * 無線タグからの受信データ */public class RecieveData {    // 無線タグからは文字列で各種データが送られる。    // 受信データの例は以下の通り。    // ::rc=(8ケタ半角数字):lq=60:ct=(4ケタ半角英数字):ed=(8ケタ半角英数字):id=1:ba=2920:a1=1358:a2=0699:x=0005:y=0000:z=0000    /** 受信データ文字列 */    private String rxString;    /** rc:未使用 */    private @Getter int rc;    /** lq:電波強度 */    private @Getter int lq;    /** ct:続き番号(?) */    private @Getter String ct;    /** ed:子機のID */    private @Getter String ed;    /** id:未使用 */    private @Getter int id;    /** ba:子機の電源電圧(mV) */    private @Getter int ba;    /** a1:ADC1(mV) */    private @Getter int a1;    /** a2:ADC2(mV) */    private @Getter int a2;    /** x:X軸方向の加速度。無線タグがターンモードの場合、無線タグが上を向いている面を1~6の値で示す */    private @Getter int x;    /** y:Y軸方向の加速度。無線タグがターンモードの場合は未使用 */    private @Getter int y;    /** z:Z軸方向の加速度。無線タグがターンモードの場合は未使用 */    private @Getter int z;    public RecieveData() {    }    public RecieveData(final String rxString) {        this.rxString = rxString;        rc = parse("rc");        lq = parse("lq");        ct = parseString("ct");        ed = parseString("ed");        id = parse("id");        ba = parse("ba");        a1 = parse("a1");        a2 = parse("a2");        x = parse("x");        y = parse("y");        z = parse("z");    }    /**     * 受信した文字列から数値データを取得する     * @param dataName 取得するデータ名     * @return 数値データ。取得に失敗した場合は0を返す     */    private int parse(final String dataName) {        int data = 0;        // ":(dataName)=(一文字以上の数字)"にマッチする部分を検索する        final String regex = ":{1}(" + dataName + ")=(\\d+)";        final Pattern p = Pattern.compile(regex);        final Matcher m = p.matcher(this.rxString);        if (m.find()) {            data = Integer.parseInt(m.group(2));        }        return data;    }    /**     * 受信した文字列からアルファベットが含まれる数値データを取得する     *      * @param dataName 取得するデータ名     * @return 数値データ。取得に失敗した場合はから文字列を返す     */    private String parseString(final String dataName) {        String data = "";        // ":(dataName)=(一文字以上のアルファベットまたは数字)"にマッチする部分を検索する        final String regex = ":{1}(" + dataName + ")=(\\w+)";        final Pattern p = Pattern.compile(regex);        final Matcher m = p.matcher(this.rxString);        if(m.find()) {            data = m.group(2);        }        return data;    }    public String toString() {        return rxString;    }} | 
■Slackletの準備とシリアルポートのイベントリスナ登録処理 (KeyWatcherApplication.java)
| package jp.hontako.keywatcher;import java.io.IOException;import com.fazecast.jSerialComm.SerialPort;import com.fazecast.jSerialComm.SerialPortDataListener;import com.fazecast.jSerialComm.SerialPortEvent;import org.riversun.slacklet.Slacklet;import org.riversun.slacklet.SlackletRequest;import org.riversun.slacklet.SlackletResponse;import org.riversun.xternal.simpleslackapi.SlackChannel;import org.riversun.slacklet.SlackletService;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublic class KeyWatcherApplication {    /** slackbotのトークン */    private static final String SLACKBOT_TOKEN = "XXXX-XXXXXXXXXXXX-XXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXX";    /** slackの通知先ユーザ名 */    private static final String SLACKBOT_SENDUSERNAME = "XXXX";    /** 前回受信したセンサ情報 */    private static RecieveData previewRXData = new RecieveData();    /**     * 本アプリのエントリポイント     * @param args     */    public static void main(String[] args) {        SpringApplication.run(KeyWatcherApplication.class, args);        SerialPortUtil serialport = new SerialPortUtil();        SlackletService slackService = new SlackletService(SLACKBOT_TOKEN);        keywatcherSetUp(serialport, slackService);    }    /**     * シリアルポート受信時のイベントリスナー登録とSlackBotの利用準備を行い、施錠状態監視、通知のセットアップを行う。     * @param serialport     * @param slackService     */    private static void keywatcherSetUp(SerialPortUtil serialport, SlackletService slackService) {        try {            serialport.enableListening(new SerialPortDataListener(){                            @Override                public void serialEvent(SerialPortEvent event) {                    if(event.getEventType() != SerialPort.LISTENING_EVENT_DATA_AVAILABLE) {                        // TWELITE 2525Aからデータを受信したときのみ処理を行う                        return;                    }                                        // 受信したデータを取得                    final SerialPort port = event.getSerialPort();                    byte[] rxBytes = new byte[port.bytesAvailable()];                    port.readBytes(rxBytes, rxBytes.length);                    String rxString = new String(rxBytes);                    RecieveData rxData = new RecieveData(rxString);                    System.out.println("[DEBUG]Data Recieved:" + rxData.toString());                    // 以下のいずれかに該当する場合はSlackで通知しない                    // ・施錠状態(受信データのX値)が前回と同じ                    // ・施錠状態の取得に失敗した(X値が0)                    int previewLockStatus = previewRXData.getX();                    int lockStatus = rxData.getX();                    if(previewLockStatus == lockStatus || lockStatus == 0) {                        return;                    }                    previewRXData = rxData;                    // 施錠状態をSlackで通知                    int signalStrength = rxData.getLq();                    int batteryHealth = rxData.getBa();                    boolean result = slackService.sendDirectMessageTo(SLACKBOT_SENDUSERNAME,                         "x:" + lockStatus +                         "電波強度:" + signalStrength +                        "電源電圧:" + batteryHealth);                    if(!result) {                        System.out.println("[ERROR]slackService.sendDirectMessageTo failed.");                    }                }                        @Override                public int getListeningEvents() {                    return SerialPort.LISTENING_EVENT_DATA_AVAILABLE;                }            });        } catch(Exception e) {            e.printStackTrace();        }        try {            slackbotSetup(slackService);        } catch(IOException e) {            e.printStackTrace();        }    }    /**     * Slackbotのイベントリスナー登録と起動処理を行う     * @param slackService     * @throws IOException     */    private static void slackbotSetup(SlackletService slackService) throws IOException {        // チャネルに何か書き込まれたらエコーバックする        slackService.addSlacklet(new Slacklet() {            @Override            public void onMessagePosted(SlackletRequest req, SlackletResponse res) {                SlackChannel channel = req.getChannel();                System.out.println("[DEBUG]channelName:" + channel.getName());                String content = req.getContent();                System.out.println("[DEBUG]content:" + content);                res.reply(content);            }        });        slackService.start();        boolean result = slackService.sendDirectMessageTo(SLACKBOT_SENDUSERNAME, "keywatcher起動完了しました");        if(result) {            System.out.println("[DEBUG]slackService Started.");        } else {            System.out.println("[ERROR]slackService Setup failed.");        }    }} | 
■シリアルポートの利用準備処理 (SerialPortUtil.java)
| package jp.hontako.keywatcher;import com.fazecast.jSerialComm.SerialPort;import com.fazecast.jSerialComm.SerialPortDataListener;import java.io.IOException;import java.util.Arrays;import java.util.List;/** * シリアルポートの管理クラス */public class SerialPortUtil {    private SerialPort port;    /** MONOSTICKの概要文。MONOSTICKのシリアルポート選別・取得時に使用する */    private static final String PORT_DESCRIPTION = "MONOSTICK";    private static final int BAUDRATE = 115200;    private static final int DATABITS = 8;    private static final int STOPBITS = SerialPort.ONE_STOP_BIT;    private static final int PARITY = SerialPort.NO_PARITY;    public SerialPortUtil() {            }    /**     * シリアルポートのリストから概要文が一致するシリアルポートを取得する     * @param portList シリアルポートのリスト     * @param portDescription 取得するシリアルポートの概要文     * @return シリアルポート。 該当するシリアルポートがない場合はnullを返す     */    private SerialPort getSerialPort(final List<SerialPort> portList, final String portDescription) {        SerialPort serialport = null;        for (final SerialPort port : portList) {            System.out.println("[DEBUG] getDescriptivePortName:" + port.getDescriptivePortName());            System.out.println("[DEBUG] getPortDescription:" + port.getPortDescription());            System.out.println("[DEBUG] getSystemPortName:" + port.getSystemPortName());            if(port.getPortDescription().equals(portDescription)) {                serialport = port;                break;            }        }                return serialport;    }    /**     * シリアルポートをオープンしイベントリスナーを登録する     * @param listener イベントリスナー     * @throws IOException     * @throws Exception     */    public void enableListening(final SerialPortDataListener listener) throws IOException, Exception {        final SerialPort ports[] = SerialPort.getCommPorts();        final List<SerialPort> portList = Arrays.asList(ports);                port = getSerialPort(portList, PORT_DESCRIPTION);        if(port == null) {            throw new IOException(PORT_DESCRIPTION + " not found.");        }        port.setBaudRate(BAUDRATE);        port.setNumDataBits(DATABITS);        port.setNumStopBits(STOPBITS);        port.setParity(PARITY);        if(!port.openPort()) {            throw new IOException(PORT_DESCRIPTION + "connect failed.");        }        if(!port.addDataListener(listener)) {            throw new Exception("SerialPortDataListener regist failed.");        }    }    /**     * イベントリスナーを解除しシリアルポートをクローズする     */    public void disableListening() {        if(port != null) {            port.removeDataListener();            port.closePort();        }    }} | 
気が向いたら改良するかも(しないかも)