はじめに
- Sony製のRFIDリーダーを入手した。
- enebularと組み合わせてネット上にカード情報を流してみたくなった。
- せっかくなのでLine Messaging APIで通知できるようにしてみた。
やったこと
- RFIDを読み取って、その情報をenebular上まで送ってみる。
- enebularはその情報に応じてLINEにメッセージを通知する。(ただし、DBとのやりとりはswitchノード等で置き換え)
- (せっかくなので)電子錠を模したサーボモータを動かしてロック機構をつくる。
注意書き
今回はObniz OSをAtom liteにインストールして使う都合から、課金が発生します。下記リンクで示す料金がかかるためご注意ください。
使った物品
入手先は一例です。
物品名 | スペックなど | 入手先 |
---|---|---|
Atom Motion | Atom lite同梱 | スイッチサイエンス |
RC-S630/S | Read/Write可能、総務省認可済み | スイッチサイエンス |
FeliCa RC-S620S/RC-S730 ピッチ変換基板のセット | フラットケーブルつき | スイッチサイエンス |
Groveケーブル1本 | - | スイッチサイエンス |
Grove-2.54 mmコネクタ変換ケーブル1本 | メス端子のみ使用 | スイッチサイエンス |
RC-S630/SをGPIO等へ接続するためにはFeliCa RC-S620S/RC-S730 ピッチ変換基板&フラットケーブルは必須です。スイッチサイエンスさんで購入する際には合わせて購入しましょう。
また、RC-S630/Sは「予め総務大臣から技術基準に適合していることの指定を受けた次に掲げる設備(型式指定)」に該当しており、法規的にも使用することが問題ないとされています。
RC-S630/Sを改造分解すると型式指定の範囲から外れてしまい法規的にも正しくなくなってしまう場合があります。Atom liteも同じくですが、無線機器本体の改造は絶対にしないようにお願いします。
型式指定に関する情報
型式指定の表示について
製品にもシルクで印字されています。
https://www.tele.soumu.go.jp/j/sys/others/highfre/
型式指定のリストについて
型式指定の型式名・指定番号・製造業者等の氏名又は名称の公示
(平成19年1月1日~平成29年4月16日)のExcelファイルから確認することができます。
https://www.soumu.go.jp/menu_seisaku/ictseisaku/dempa_rikatsuyo/high_frequency/index.html
こちらから型式指定の番号がわかります。
https://www.sony.co.jp/Products/felica/business/products/reader/RC-S620.html
結線した結果
RC-S630/SはUART通信で各種マイコンに情報を渡したり、逆に操作を受信する。
今回は下記のピン配置で配線した。
Atom Motion | RC-S630/S |
---|---|
26(RX) | TXD |
32(TX) | RXD |
5v | Vcc |
G | GND(2つある内のいずれも可) |
RC-S630/S側のピン情報はこちらを参照。
https://www.switch-science.com/products/1029/
RC-S630/SをObnizで使う際の注意:Obniz OSと共存させたい
Arduino向けライブラリはこちらで配布されている。ZipファイルがDLされるため一旦解答して中身を拝見してみる。
コード見るとわかるように、Serial0(Serial.~)を使って通信するように決められている。しかし、Obniz OSもSerial0(Serial.~)を使用するため、ライブラリ間でバッティングが発生してしまう。あちゃー
そのためRC-S630/S間通信で使うUARTをSerial2(Serial2.~)に変更する。
変更の仕方は簡単で、ライブラリ内にあるSerial.
をSerial2.
に変更すれば良い。
著作権の都合もあると思いコードの掲載は避けるが、さしずめ下記のように使用するSerialを一斉置換で変えてしまえばいい。
...
void RCS620S::writeSerial(
const uint8_t* data,
uint16_t len)
{
Serial.write(data, len);
}
...
...
void RCS620S::writeSerial(
const uint8_t* data,
uint16_t len)
{
Serial2.write(data, len);
}
...
RC-S630/SをObnizで使う際の注意:ESP32でつかえるようにしたい
ライブラリは結構古いため、そのまま使おうとするとヘッダファイルが見つからない旨のエラーが出てくる。そのため、下記に変更する。
#include <stdio.h>
#include <string.h>
#include <inttypes.h>
#include "Arduino.h"
#include "Print.h"
#include "HardwareSerial.h"
#include "RCS620S.h"
後はDL時のフォルダを再度zipファイル化して、下記リンクの要領でライブラリをインストールする。
Serial2じゃなくてSerialを使うパターンのライブラリを共存させたい場合は、RCS620S.cppおよびRCS620S.hのファイル名を変更する必要がある。(同じように、zipファイルのファイル名も変更する。)
下記のようにインクルードも変更する必要もあるため注意する。
#include <stdio.h>
#include <string.h>
#include <inttypes.h>
#include "Arduino.h"
#include "Print.h"
#include "HardwareSerial.h"
#include "Serial2_RCS620S.h"
Obniz plugin使用時のコードと構成
エディターはArduino 1.8.3を使用した。また、下記のようにAtomMotion使用に伴うファイルを配置しておく。ファイルは下記githubから入手可能。
https://github.com/m5stack/M5Atom/tree/master/examples/ATOM_BASE/ATOM_Motion
書き込んだコード(上記スクショのobnizos_Atom.ino)の内容は下記です。
obnizos_Atom.ino
#include <M5Atom.h>
#include "AtomMotion.h"
#include <ESP32_Serial2_RCS620S.h>
#include <inttypes.h>
#include <string.h>
#include <obniz.h>
AtomMotion Atom;
RCS620S rcs620s;
#define COMMAND_TIMEOUT 400
#define PUSH_TIMEOUT 2100
#define POLLING_INTERVAL 1000
bool servo_toggle=false;
const int servo_ch=1;
bool onlineFlg = false;
void onCommand(uint8_t* data, uint16_t length){
//Serial.write(data,length);
data[length] = '\0';
if(strcmp((const char*)data, "unlock")==0){
//Serial.println("UNLOCKED");
Atom.SetServoAngle(servo_ch, 30);
servo_toggle = false;
const char* status_str = "CUNLOCKED";
obniz.commandSend((uint8_t*)status_str, strlen(status_str)); // send data to obnizCloud
}else if(strcmp((const char*)data, "lock")==0){
//Serial.println("LOCKED");
Atom.SetServoAngle(servo_ch, 100);
servo_toggle = true;
const char* status_str = "CLOCKED";
obniz.commandSend((uint8_t*)status_str, strlen(status_str)); // send data to obnizCloud
}else if(strcmp((const char*)data, "status")==0){
const char* status_str = servo_toggle ? "CLOCKED":"CUNLOCKED";
obniz.commandSend((uint8_t*)status_str, strlen(status_str)); // send data to obnizCloud
}else if(strcmp((const char*)data, "toggle")==0){
const char* toggle_str = servo_toggle ? "CUNLOCK":"CLOCK";
obniz.commandSend((uint8_t*)toggle_str, strlen(toggle_str)); // send data to obnizCloud
}
}
void onEvent(os_event_t event, uint8_t* data, uint16_t length) {
int ret = 0;
int i = 0;
switch (event) {
case PLUGIN_EVENT_NETWORK_CLOUD_CONNECTED:
Serial.println("cloud Connected");
delay(1000);
ret = rcs620s.initDevice();
while (ret<=0) {
delay(3000);
Serial.println("Please wait...");
ret = rcs620s.initDevice();
}
Serial.print("RCS620S Init = ");
Serial.println(ret);
Atom.SetServoAngle(servo_ch, 30);
onlineFlg = true;
delay(50);
M5.dis.fillpix(0x00ff00);
break;
case PLUGIN_EVENT_NETWORK_WIFI_SCANNING:
delay(50);
M5.dis.fillpix(0xffff00);
break;
case PLUGIN_EVENT_NETWORK_WIFI_FAIL:
for(i=0;i<8;i++){
delay(50);
M5.dis.fillpix(0xff00ff);
delay(1000);
M5.dis.fillpix(0x000000);
}
break;
case PLUGIN_EVENT_OTA_ERROR:
for(i=0;i<8;i++){
delay(50);
M5.dis.fillpix(0xff00ff);
delay(1000);
M5.dis.fillpix(0x000000);
}
break;
case PLUGIN_EVENT_NETWORK_WIFI_NOTFOUND:
for(i=0;i<3;i++){
delay(50);
M5.dis.fillpix(0xff00ff);
delay(1000);
M5.dis.fillpix(0x000000);
}
break;
}
}
void setup() {
Serial.begin(115200);
obniz.onEvent(onEvent);
obniz.start();
obniz.commandReceive(onCommand);
obniz.pinReserve(19);
obniz.pinReserve(22);
obniz.pinReserve(21);
obniz.pinReserve(25);
obniz.pinReserve(26);
obniz.pinReserve(32);
M5.begin(true, false, true);
Atom.Init();
Serial2.begin(115200, SERIAL_8N1, 26, 32);
delay(50);
M5.dis.fillpix(0xff00ff);
}
void loop() {
if(!onlineFlg)
return;
static int no_ret_counter = 0;
// Polling
rcs620s.timeout = COMMAND_TIMEOUT;
int ret = rcs620s.polling();
// Serial.print("RCS620S polling = ");
// Serial.println(ret);
delay(50);
M5.dis.fillpix(0x00ff00);
if (ret>0) {
delay(50);
M5.dis.fillpix(0x0000ff);
uint8_t data[17];
data[0] = 200;
//Serial.print("idm = ");
for (int i = 0; i < 8; i++) {
//Serial.print(rcs620s.idm[i], HEX);
data[i+1] = rcs620s.idm[i];
}
//Serial.println();
//Serial.print("pmm = ");
for (int i = 0; i < 8; i++) {
//Serial.print(rcs620s.pmm[i], HEX);
data[i+8+1] = rcs620s.idm[i];
}
//Serial.println();
obniz.commandSend(data, 17); // send data to obnizCloud
rcs620s.rfOff();
delay(5000);
no_ret_counter = 0;
return;
}else{
Serial.println("RETURN RET <=0");
Serial.println(ret);
if(no_ret_counter>60 || ret < 0){
rcs620s.rfOff();
Serial2.end();
if(ret == 0){
Serial.println("RETURN RET COUNTER >60");
delay(50);
M5.dis.fillpix(0xff0000);
delay(3000);
}else{
Serial.println("RETURN RET < 0");
delay(50);
M5.dis.fillpix(0xff0000);
delay(3000);
}
Serial2.begin(115200, SERIAL_8N1, 26, 32);
ret = rcs620s.initDevice();
while (!ret) {
delay(1000);
ret = rcs620s.initDevice();
}
no_ret_counter = 0;
return;
}
no_ret_counter++;
}
rcs620s.rfOff();
delay(POLLING_INTERVAL);
//Serial.println("LOOP");
}
ポイントとしては再接続をしているポイント(RETURN RET
)で、一定時間経過後(1分)にSerialを再接続させて、初期化を図っている。また、前述の問題もあってSerial2をつかっていr
ネットにつなげる
マイコンのネット接続はObniz OSならびにObniz Cloudにおまかせする。
また、ネット上の処理はenebular Cloud上で済ます。
enebularってなに?っていう方はこちら。
Node−RED上からObnizを使う際にはこちらを使う。enebularのメニューから「パレットの管理」に入り、ノードの追加で下記ノードを追加する。執筆当時のバージョンは0.6.2。
また、こちらの記事の「1. チャネルの作成」と同じ要領でプロバイダーと任意のチャネルを作っておく。
また、Line Messaging APIもNode-REDで取り扱うためのノードが用意されているためインストールする。ちなみに執筆当時のバージョンは0.1.11。
enebularにデプロイするフローはこちら。
flowのjsonデータ
Obniz functionの設定、LINE Messaging APIの設定、認証するカードIDの設定は自身のものを設定すること。[
{
"id": "1f5d39f6736e907b",
"type": "obniz-repeat",
"z": "332f43dd4569b2ff",
"obniz": "62b566cd0cb9c335",
"name": "データ受信",
"interval": 100,
"code": "obniz.plugin.onreceive = data => {\n msg.payload = data;\n node.send(msg)\n};",
"x": 200,
"y": 100,
"wires": [
[
"4085f82e9e970cbb"
]
]
},
{
"id": "0be748e01aaa32f6",
"type": "PushMessage",
"z": "332f43dd4569b2ff",
"name": "RFIDチェック",
"x": 1340,
"y": 500,
"wires": []
},
{
"id": "d95189d849f988dc",
"type": "inject",
"z": "332f43dd4569b2ff",
"name": "",
"props": [
{
"p": "payload"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "デバッグ文字列",
"payloadType": "str",
"x": 260,
"y": 500,
"wires": [
[
"0be748e01aaa32f6"
]
]
},
{
"id": "3ecff793494f3053",
"type": "switch",
"z": "332f43dd4569b2ff",
"name": "本当はDBからデータを採って比較するところ。",
"property": "payload",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "[]",
"vt": "str"
},
{
"t": "neq",
"v": "[]",
"vt": "str"
}
],
"checkall": "true",
"repair": false,
"outputs": 2,
"x": 320,
"y": 400,
"wires": [
[
"ac55ba3283ed74ae"
],
[
"3bee9642a454083d"
]
]
},
{
"id": "3bee9642a454083d",
"type": "template",
"z": "332f43dd4569b2ff",
"name": "正しくないユーザー時の文章",
"field": "payload",
"fieldType": "msg",
"format": "handlebars",
"syntax": "mustache",
"template": "誤ったユーザーが箱を操作しました。",
"output": "str",
"x": 680,
"y": 440,
"wires": [
[
"0be748e01aaa32f6"
]
]
},
{
"id": "9d0be5223f7bb11a",
"type": "obniz-function",
"z": "332f43dd4569b2ff",
"obniz": "62b566cd0cb9c335",
"name": "施錠",
"code": "obniz.plugin.send(\"lock\")",
"x": 870,
"y": 580,
"wires": [
[]
]
},
{
"id": "a3dcf949ada7dc51",
"type": "inject",
"z": "332f43dd4569b2ff",
"name": "施錠ボタン",
"props": [
{
"p": "payload"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 300,
"y": 580,
"wires": [
[
"9d0be5223f7bb11a"
]
]
},
{
"id": "d1ef3404f86e7c1e",
"type": "inject",
"z": "332f43dd4569b2ff",
"name": "解錠ボタン",
"props": [
{
"p": "payload"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 320,
"y": 640,
"wires": [
[
"85c26480f617ae74"
]
]
},
{
"id": "85c26480f617ae74",
"type": "obniz-function",
"z": "332f43dd4569b2ff",
"obniz": "62b566cd0cb9c335",
"name": "解錠",
"code": "obniz.plugin.send(\"unlock\")",
"x": 870,
"y": 640,
"wires": [
[]
]
},
{
"id": "026598effc63df2a",
"type": "obniz-function",
"z": "332f43dd4569b2ff",
"obniz": "62b566cd0cb9c335",
"name": "ステータス確認",
"code": "obniz.plugin.send(\"status\")",
"x": 900,
"y": 720,
"wires": [
[]
]
},
{
"id": "7bfef783732a4593",
"type": "inject",
"z": "332f43dd4569b2ff",
"name": "ステータス確認ボタン",
"props": [
{
"p": "payload"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 300,
"y": 720,
"wires": [
[
"026598effc63df2a"
]
]
},
{
"id": "4085f82e9e970cbb",
"type": "switch",
"z": "332f43dd4569b2ff",
"name": "",
"property": "payload[0]",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "67",
"vt": "str"
},
{
"t": "eq",
"v": "200",
"vt": "str"
}
],
"checkall": "true",
"repair": false,
"outputs": 2,
"x": 310,
"y": 220,
"wires": [
[
"41bd9509396e3c47"
],
[
"6df211fd1f8ac874"
]
]
},
{
"id": "ac55ba3283ed74ae",
"type": "obniz-function",
"z": "332f43dd4569b2ff",
"obniz": "62b566cd0cb9c335",
"name": "トグル操作",
"code": "obniz.plugin.send(\"toggle\")\nreturn msg",
"x": 690,
"y": 380,
"wires": [
[]
]
},
{
"id": "a6d6a65d4351b1fe",
"type": "switch",
"z": "332f43dd4569b2ff",
"name": "解錠,施錠,状態",
"property": "payload",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "LOCK",
"vt": "str"
},
{
"t": "eq",
"v": "UNLOCK",
"vt": "str"
},
{
"t": "eq",
"v": "LOCKED",
"vt": "str"
},
{
"t": "eq",
"v": "UNLOCKED",
"vt": "str"
}
],
"checkall": "true",
"repair": false,
"outputs": 4,
"x": 740,
"y": 240,
"wires": [
[
"15183eb83755bae1"
],
[
"4c1a8a1c93b6d4c1"
],
[
"eda1b479fbc130f3"
],
[
"47747bbd4e29d989"
]
]
},
{
"id": "15183eb83755bae1",
"type": "obniz-function",
"z": "332f43dd4569b2ff",
"obniz": "62b566cd0cb9c335",
"name": "施錠",
"code": "obniz.plugin.send(\"lock\")\nreturn msg;",
"x": 950,
"y": 120,
"wires": [
[]
]
},
{
"id": "31a3018d0f632b36",
"type": "change",
"z": "332f43dd4569b2ff",
"name": "文字列化",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "$string(payload)",
"tot": "jsonata"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 560,
"y": 300,
"wires": [
[
"3ecff793494f3053"
]
]
},
{
"id": "6df211fd1f8ac874",
"type": "function",
"z": "332f43dd4569b2ff",
"name": "OP削除",
"func": "msg.payload = msg.payload.slice(1);\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 460,
"y": 240,
"wires": [
[
"31a3018d0f632b36"
]
]
},
{
"id": "4c1a8a1c93b6d4c1",
"type": "obniz-function",
"z": "332f43dd4569b2ff",
"obniz": "62b566cd0cb9c335",
"name": "解錠",
"code": "obniz.plugin.send(\"unlock\")\nreturn msg;",
"x": 950,
"y": 180,
"wires": [
[]
]
},
{
"id": "2e25331a7c647400",
"type": "debug",
"z": "332f43dd4569b2ff",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 750,
"y": 180,
"wires": []
},
{
"id": "41bd9509396e3c47",
"type": "function",
"z": "332f43dd4569b2ff",
"name": "OP除き&ASCII文字列取得",
"func": "var chars = msg.payload;\nvar result=\"\";\nfor (var i = 1; i < chars.length; i++) {\n result += String.fromCharCode(chars[i]);\n}\nmsg.payload = result;\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 520,
"y": 180,
"wires": [
[
"a6d6a65d4351b1fe",
"2e25331a7c647400"
]
]
},
{
"id": "8c634a3a80691e35",
"type": "function",
"z": "332f43dd4569b2ff",
"name": "施錠データ作成",
"func": "let time = msg.payload;\nmsg.payload = {};\nmsg.payload.status = \"施錠\";\nmsg.payload.time = time;\nmsg.payload.rentaler = \"井上 稔\";\n\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1100,
"y": 280,
"wires": [
[
"532cd26d7800da4b"
]
]
},
{
"id": "ab3ec12ec42fe01a",
"type": "function",
"z": "332f43dd4569b2ff",
"name": "解錠データ作成",
"func": "let time = msg.payload;\nmsg.payload = {};\nmsg.payload.status = \"解錠\";\nmsg.payload.time = time;\nmsg.payload.rentaler = \"井上 稔\"\n\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1060,
"y": 400,
"wires": [
[
"d539ead1c2c3d8fa"
]
]
},
{
"id": "eda1b479fbc130f3",
"type": "moment",
"z": "332f43dd4569b2ff",
"name": "",
"topic": "",
"input": "",
"inputType": "date",
"inTz": "Asia/Tokyo",
"adjAmount": 0,
"adjType": "days",
"adjDir": "add",
"format": "YYYY年MM月DD日 HH時mm分ss秒",
"locale": "ja-JP",
"output": "payload",
"outputType": "msg",
"outTz": "Asia/Tokyo",
"x": 1000,
"y": 240,
"wires": [
[
"8c634a3a80691e35"
]
]
},
{
"id": "47747bbd4e29d989",
"type": "moment",
"z": "332f43dd4569b2ff",
"name": "",
"topic": "",
"input": "",
"inputType": "date",
"inTz": "Asia/Tokyo",
"adjAmount": 0,
"adjType": "days",
"adjDir": "add",
"format": "YYYY年MM月DD日 HH時mm分ss秒",
"locale": "ja-JP",
"output": "payload",
"outputType": "msg",
"outTz": "Asia/Tokyo",
"x": 1000,
"y": 360,
"wires": [
[
"ab3ec12ec42fe01a"
]
]
},
{
"id": "d539ead1c2c3d8fa",
"type": "template",
"z": "332f43dd4569b2ff",
"name": "開錠時の文章",
"field": "payload",
"fieldType": "msg",
"format": "handlebars",
"syntax": "mustache",
"template": "{{payload.status}}しました。\n日時:{{payload.time}}\n借りた人:{{payload.rentaler}}",
"output": "str",
"x": 1120,
"y": 440,
"wires": [
[
"0be748e01aaa32f6"
]
]
},
{
"id": "532cd26d7800da4b",
"type": "template",
"z": "332f43dd4569b2ff",
"name": "施錠時の文章",
"field": "payload",
"fieldType": "msg",
"format": "handlebars",
"syntax": "mustache",
"template": "{{payload.status}}しました。\n日時:{{payload.time}}\n返却した人:{{payload.rentaler}}",
"output": "str",
"x": 1140,
"y": 320,
"wires": [
[
"0be748e01aaa32f6"
]
]
},
{
"id": "62b566cd0cb9c335",
"type": "obniz",
"obnizId": "xxxxxxxx",
"deviceType": "esp32",
"name": "Atom",
"accessToken": "",
"code": ""
}
]
Obniz functionノードでは基本的にcommandデータのやり取りだけで終わっていて、主だったロジックなどは実装していないです。
動かしてみた結果はこちら。
まなび
- Obniz OSからRC-S630/Sを使うのちょっとハードル高いかも?(とはいいつつ、Serialの専有に気づけば対処は可能)
- Obniz OS-enebular(Node-RED)-LINEの流れを作るのは結構楽。(ノードしか触っていない。)
- enebularでデータのインフラが出来上がっているの便利