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; @Controller public 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; @SpringBootApplication public 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(); } } } |
気が向いたら改良するかも(しないかも)