一般社団法人 全国個人事業主支援協会

COLUMN コラム

  • RaspberryPi+無線タグ(Twe-Lite2525A)+Spring Bootで玄関の施錠状態を監視してみた

1年前くらいからRaspberryPi2をサブPCとして使っていたのですが、今年に入ってからRaspberryPi4に乗り換えました。

このままRaspberryPi2をジャンク箱の肥やしにするのももったいないというのと、たまに「今日家の鍵閉めたっけ?」となるのを何とかしたかったので、玄関の施錠状態が変わったときにSlackbotで通知するシステムを作ってみました。

おもな仕様

 

子機(無線タグ)

  • 玄関内側の施錠ノブに無線タグを貼り付け、加速度センサで施錠ノブの向きを監視する。
  • 振動、回転など、加速度センサで何らかの動きを感知したタイミングでセンサ情報を送信する。

 

親機(RaspberryPi)

  • 子機からのセンサ情報を受信したら、最新のセンサ情報を保持しておく
  • 最新のセンサ情報と前回のセンサ情報の施錠状態を比較し、値が一致していなければSlackbotで以下を通知する。
    ①施錠状態(子機のどの面が上を向いているか)
    ②無線の電波強度
    ③子機の電源電圧

 

使用したもの

 

ハード

  • RaspberryPi2(親機)
  • MONOSTICK(親機側USB無線モジュール)
  • Twe-Lite 2525 A(加速度センサ付き無線タグ)
  • ボタン電池 CR2032(子機用バッテリー)
  • ポスター用粘土のり(子機の貼りつけに使用 「のり 粘土 ポスター 100均」などで調べると出てくる)

親機

子機

 

ソフト

  • Spring Boot(親機側アプリ)
  • slacklet(SlackBotを用いて通知する際に使用するライブラリ)
  • jSerialCom(Javaでシリアルポートへアクセスする際に使用するライブラリ USB無線モジュールのアクセスに使用)
  • 無線タグアプリ(MONOSTICKおよび無線タグに書き込む公式ファームウェア)

 

プログラム

子機側は公式ファームウェアである「無線タグアプリ」をそのまま使用。

親機側は主に以下4つのプログラムを書きました。

  • 動作確認用webページ
  • センサ情報の保持クラス
  • Slackletの準備とシリアルポートのイベントリスナ登録処理
  • シリアルポートの利用準備処理

以下に内容を示します。

※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();
        }
    }
}

 

今後やりたいこと

気が向いたら改良するかも(しないかも)

  • DBでセンサ値を記録し、グラフなどwebページ上に表示する
  • 設定画面の追加
  • SlackBotにDMして操作できるようにする
  • 無線タグを追加してガス栓の状態も監視する
The following two tabs change content below.

本間 孝行

ニンゲンやってます

最新記事 by 本間 孝行 (全て見る)

この記事をシェアする

  • Twitterでシェア
  • Facebookでシェア
  • LINEでシェア