3Dプリンタ スマートロック編(4) obniz

obniz

obniz(オブナイズ)というarduino系のハードがある。ESP32が乗ったボードなのだが、開発がwebで実施出来て、しかもjavascriptで記述。実行は1ボタンで開始と、便利なものがあり、I/Oはサーボも余裕で動かせるため、 錠操作としては、こちらを使用してみる。

サイズもラズパイZEROとほぼ同等だ。

ESP32のため、電源はそれなりに必要なので電池での動作は不可。この辺りはラズパイと変わりないが、単機能に絞れるのと、 IOの電源容量に余裕があるのでこちらが安心かも。

ということで、obnizでのスマートロック処理を作成してみる。

サーボ処理

メインの処理となるサーボ操作。中心位置と開閉双方に90度動かすことが出来ればよい。
obnizの0,1,2ピンにそれぞれ茶、橙、黄の線を接続する。

/*------------------------------------------------------------
錠操作用のサーボ処理
------------------------------------------------------------*/
// サーボ定義
var servo = obniz.wired("ServoMotor", {gnd:0, vcc:1, signal:2});
// センター位置へ初期移動
servo.angle(90.0);
 
// 開錠ボタン処理
$('#LockOpen').click(function () {
  servo.angle(180);
  $("#print").text('ON');
  setTimeout(function(){servo.angle(90.0)}, 600);
});
// 施錠ボタン処理
$('#LockClose').click(function () {
  servo.angle(0);
  $("#print").text('OFF');
  setTimeout(function(){servo.angle(90.0)}, 600);
});

動作は0°~180°なので、最初は90°の位置をデフォルトとする。 開錠操作時は180°の位置まで動かして、また90°に戻しておく。 サーボ回転時間があるので、戻し処理は600ms後に実行する。 施錠操作も同様で、回転方向が逆となる0°に動かしてから、90°に戻す。

これで動かしてみたところ、サーボ回転中にボタンを押すと、少し変な動きが発生。 サーボ回転でのウエイトを差し込んでいて、全部で1.2秒あるが、その間でボタンを押すと、 さらにイベントが発生して、サーボが困っているようだ。 では、フラグを追加しておき、動作中はイベントはスルーさせよう。

/*------------------------------------------------------------
錠操作用のサーボ処理
------------------------------------------------------------*/
const servoWait = 600;
var moved = false;
// サーボ定義
var servo = obniz.wired("ServoMotor", {gnd:0, vcc:1, signal:2});
// センター位置へ初期移動
servo.angle(90.0);
 
// 開錠ボタン処理
$('#LockOpen').click(function () {
  if (!moved) {
    moved = true;
    servo.angle(180);
    $("#print").text('ON');
    setTimeout( function() {
      servo.angle(90.0);
      setTimeout( function() {moved = false;}, servoWait);
    }, servoWait);
  }
});
// 施錠ボタン処理
$('#LockClose').click(function () {
  if (!moved) {
    moved = true;
    servo.angle(0);
    $("#print").text('OFF');
    setTimeout( function() {
      servo.angle(90.0)
      setTimeout( function() {moved = false;}, servoWait);
    }, servoWait);
  }
});

「moved」の動作中変数を用意して、動作開始でTrueとして、動作終了(センター戻り含め)時にFalseで戻している。 movedがTrueの時のイベントは、何もせず終わらせており、無視している。これで、ボタンをガチャガチャ押しても、 サーボが変な動作になることは無くなった。

最終的には、処理の最適化を行い、以下のようにした。

// 開錠ボタン処理
$('#LockOpen').click( function(){
  smartLockServo(false);
});
// 施錠ボタン処理
$('#LockClose').click( function(){
  smartLockServo(true);
});
/*------------------------------------------------------------
錠操作用のサーボ処理
------------------------------------------------------------*/
const servoWait = 600;  // サーボが90度動く待ち時間
var moved = false;      // サーボ動作中フラグ
// サーボ定義
var servo = obniz.wired("ServoMotor", {gnd:0, vcc:1, signal:2});
// センター位置へ初期移動
servo.angle(90.0);
 
// 開錠・施錠ボタン処理
function smartLockServo(lock) {
  var servoAngle = lock?0:180;
  var msg = lock?'ON':'OFF';
  // 動作中チェック
  if (!moved) {
    moved = true;  // 動作中セット
    servo.angle(servoAngle);
    $("#print").text(msg);
    // 初期動作のウェイト
    setTimeout( function() {
      servo.angle(90.0);
      // 戻り動作のウェイト
      setTimeout( function() {
        // 動作終了
        moved = false;
      }, servoWait);
    }, servoWait);
  }
}

状態

現在の錠の状態チェックを行う。リードスイッチが取り付けてあるため、これの状態で判断する。

// canvas作成
const width = obniz.display.width;
const height = obniz.display.height;
const ctxOpen = obniz.util.createCanvasContext(width, height);
const ctxClose = obniz.util.createCanvasContext(width, height);
// 描画
ctxOpen.fillStyle = "white";
ctxOpen.font = "40px Arial";
ctxOpen.fillText(" 開", 0, 60);
ctxClose.fillStyle = "white";
ctxClose.font = "40px Arial";
ctxClose.fillText("施錠中", 0, 60);
 
/*------------------------------------------------------------
状態チェック用のリードスイッチ処理
------------------------------------------------------------*/
// リードスイッチ定義
var button = obniz.wired("Button",  {signal:8, gnd:9});
// 変化を検知したら、表示
button.onchange = function(locked) {
  if (locked) {
    $("#state").text('施錠中');
    $("#state").css('color', 'red');
    // 表示
    obniz.display.clear();
    obniz.display.draw(ctxClose);
  } else {
    $("#state").text('開錠中');
    $("#state").css('color', 'green');
    // 表示
    obniz.display.clear();
    obniz.display.draw(ctxOpen);
  }
  $("#print").text(locked);
};

特に難しいところは無く、「Button」のパーツ処理で実施。onchange発生時に内容を変化させる。 初期時も最初のonchangeが発生するので、初回処理をあえて作る必要はなかった。 画面(ブラウザ)表示用に「#state」のエリアを用意して、状態を表示。 また、obniz側にもctxOpen/ctxCloseのcanvas.contextを用意して、状態表示させる。

スイッチ

オブナイズ本体には小さなスイッチがついており、ちょっとだけ操作が可能。 これを使った施錠や開錠もできるようにしてみよう。

/*------------------------------------------------------------
本体スイッチ操作
------------------------------------------------------------*/
obniz.switch.onchange = function(state) {
  if (state == 'none') {
    // 何もしない
  } else if (state == 'push') {
    // 何もしない
  } else if (state == 'left') {
    // 施錠
    smartLockServo(true);
  } else if (state == 'right') {
    // 開錠
    smartLockServo(false);
  }
}  

本体スイッチは4つの状態が発生する。左右に倒したときに、施錠・開錠を行わせてみた。 それ以外の場合には何もしない。

LED

現在の状態を取得することはできており、ディスプレイに表示するようにしてみたが、 併せてLEDでの状態表示もさせてみようと思う。
ロックしているときは赤もしくはオレンジ表示、解除しているときは緑または青表示とする。 まあ、その色のLEDをつなげばいいだけなので、施錠時用と開錠時用のLED表示をさせてみよう。

/*------------------------------------------------------------
状態表示用のLED準備
------------------------------------------------------------*/
// LED定義
var ledClose = obniz.wired("LED", { anode:4, cathode:5 } );
var ledOpen = obniz.wired("LED", { anode:6, cathode:7 } );
 
/*------------------------------------------------------------
状態チェック用のリードスイッチ処理
------------------------------------------------------------*/
// リードスイッチ定義
var button = obniz.wired("Button",  {signal:8, gnd:9});
// 変化を検知したら、表示
button.onchange = function(locked) {
  if (locked) {
    $("#state").text('施錠中');
    $("#state").css('color', 'red');
    // 表示
    obniz.display.clear();
    obniz.display.draw(ctxClose);
    ledOpen.off();
    ledClose.on();
  } else {
    $("#state").text('開錠中');
    $("#state").css('color', 'green');
    // 表示
    obniz.display.clear();
    obniz.display.draw(ctxOpen);
    ledOpen.on();
    ledClose.off();
  }
  $("#print").text(locked);
};

これで、施錠用LEDとしてio4に赤色系LEDを、開錠用LEDとしてio6に緑色系LEDをつなげばいいはずだ。

nodejs

実際の運用を考えた時に、どうやって動かすのか、ふと疑問になった。ドキュメントを見ると、

ブラウザで動かす場合はそのページを開いていないとobnizを使えなかったのですが、
nodejsを使えば手元のパソコンやサーバーなどでずっと動かすことが出来ます。

とのことで、常時起動させるにはローカルでnodejsによる動作をさせると良いようだ。チュートリアルとして 「Nodejsから使う」があるので、 これを見ながら、構築する。 あらかじめnodejsが動くサーバの用意が必要だが、家ではラズパイがいろいろ動いているので、問題ない。

では、手順に従って環境を作ってみる。nodejsはインストール済みなので、 まずは、プロジェクトのディレクトリを作成し、そのディレクトリで
npm init
でプロジェクトを作成。各種質問はすべてEnterでやっつける。 そのあとはひとまず、
npm install –save obniz
で、obnizパッケージをインストールする。

では、コーディングを実施。 まずは初期の練習から。

var Obniz = require("obniz");
 
var obniz = new Obniz("OBNIZ_ID_HERE");
 
obniz.onconnect = async function () {
 
  obniz.display.clear();
  obniz.display.print("Hello World!")
 
}

「OBNIZ_ID_HERE」の部分にIDを入れて実行すれば、obnizの画面にHelloWorld!が表示された。 IDを入れないと、タイムアウトが発生して、nodeにエラーが返ってくる。

invalid obniz id

nodeで動かす際には、ディスプレイの扱いに注意が必要となる。 というのも、html部分がないので、html要素となるcanvasが用意できない。 そのため、今まで使用していた「obniz.util.createCanvasContext」でcanvas要素を作成するのではなく、 node-canvasを使用した方式に変更が必要となる。
canvasをnpmで入れればいいのだが、ちょっとつまずいたので、こちらを参照。

canvasが導入できれば、下記サンプルのようにcontext作成を行い、各種出力が可能。

var Obniz = require("obniz");
 
var obniz = new Obniz("OBNIZ_ID_HERE");
 
obniz.onconnect = async function () {
 
  obniz.display.clear();
  obniz.display.print("Hello World")
 
  const { createCanvas } = require('canvas');
  const canvas = createCanvas(128, 64); 
  const ctx = canvas.getContext('2d');
 
  ctx.fillStyle = "white";
  ctx.font = "25px Arial";
  ctx.fillText("おぶないず", 0, 60);
 
  await obniz.wait(5000);
 
  obniz.display.clear();
  obniz.display.draw(ctx);
 
}

なるほど、これで先ほどのwebブラウザ版プログラムが移植できそうだ。

var Obniz = require("obniz");
 
var obniz = new Obniz("OBNIZ_ID_HERE");
 
  // canvas作成
  const { createCanvas } = require('canvas');
  const width = obniz.display.width;
  const height = obniz.display.height;
  const ctxOpen = createCanvas(width, height).getContext('2d');
  const ctxClose = createCanvas(width, height).getContext('2d');
 
  // 描画
  ctxOpen.fillStyle = "white";
  ctxOpen.font = "40px Arial";
  ctxOpen.fillText(" 開", 0, 60);
  ctxClose.fillStyle = "white";
  ctxClose.font = "40px Arial";
  ctxClose.fillText("施錠中", 0, 60);
 
obniz.onconnect = async function () {
  // 接続完了
 
  /*------------------------------------------------------------
  状態表示用のLED準備
  ------------------------------------------------------------*/
  // LED定義
  var ledOpen = obniz.wired("LED", { anode:6, cathode:7 } );
  var ledClose = obniz.wired("LED", { anode:4, cathode:5 } );
 
  /*------------------------------------------------------------
  状態チェック用のリードスイッチ処理
  ------------------------------------------------------------*/
  // リードスイッチ定義
  var button = obniz.wired("Button",  {signal:8, gnd:9});
  // 変化を検知したら、表示
  button.onchange = function(locked) {
    if (locked) {
      // 表示
      obniz.display.clear();
      obniz.display.draw(ctxClose);
      ledOpen.off();
      ledClose.on();
    } else {
      // 表示
      obniz.display.clear();
      obniz.display.draw(ctxOpen);
      ledOpen.on();
      ledClose.off();
    }
  };
  /*------------------------------------------------------------
  錠操作用のサーボ処理
  ------------------------------------------------------------*/
  const servoWait = 600;  // サーボが90度動く待ち時間
  var moved = false;      // サーボ動作中フラグ
  // サーボ定義
  var servo = obniz.wired("ServoMotor", {gnd:0, vcc:1, signal:2});
  // センター位置へ初期移動
  servo.angle(90.0);
 
  // 開錠・施錠ボタン処理
  function smartLockServo(lock) {
    var servoAngle = lock?0:180;
    // 動作中チェック
    if (!moved) {
      moved = true;  // 動作中セット
      servo.angle(servoAngle);
      // 初期動作のウェイト
      setTimeout( function() {
        servo.angle(90.0);
        // 戻り動作のウェイト
        setTimeout( function() {
          // 動作終了
          moved = false;
        }, servoWait);
      }, servoWait);
    }
  }
 
  /*------------------------------------------------------------
  本体スイッチ操作
  ------------------------------------------------------------*/
  obniz.switch.onchange = function(state) {
    if (state == 'none') {
      // 何もしない
    } else if (state == 'push') {
      // 何もしない
    } else if (state == 'left') {
      // 施錠
      smartLockServo(true);
    } else if (state == 'right') {
      // 開錠
      smartLockServo(false);
    }
  }  
 
}

express

nodejsといえば、expressである...なのかわからないが、外部からの要求の受け答えをしたいので、 expressを導入する。現在は本体スイッチでサーボを動かせるが、画面がないのでそれ以外の入力がない。 錠の開け閉めなどに自分のスマホで指示を出したいので、webhookを受け取って処理を行う必要がある。

では、expressをインストールする。
npm install express –save
動かしてみる。

const express = require('express');
const app = express();
 
app.get('/', function(req, res) {
  res.send('Hello World!')
});
 
var server = app.listen(3300, function() {
  console.log('Example app listening on port:' + server.address().port)
});

これで起動すると、画面上のログには起動メッセージが出て、ブラウザから3300ポートにアクセスしてみると、 ブラウザ上にHello World!が出てきた。問題なし。http://raspberrypi3p.local:3300

では、apiを用意してみよう。 用意する処理は開錠、施錠、状態の3つ。

// routing
app.get('/', function(req, res) {
  res.send('Hello obniz!');
});
app.get('/open', function(req, res) {
    // 開錠
    smartLockServo(false);
    res.send('door is unLocked');
});
app.get('/close', function(req, res) {
    // 施錠
    smartLockServo(true);
    res.send('door is Locked');
});
app.get('/status', function(req, res) {
    // 状態
    button.isPressedWait()
    .then(function(pressed){
      res.send('{lock: ' + pressed + '}');
    });
});

これで、urlにアクセスするだけで、家の錠を開け閉めできるようになった。

expressでwebhookを受けることで、ボタン代わりとなるので、 何かアクションをしたい場合は、app.getでapiを用意してやれば、いろいろできそうだ。

LEDのPWM化

状態表示のLEDが少しまぶしい。あまり明るく点灯している必要はないので、暗くしたいと思うが、 抵抗は既にはんだ付けしてしまっており、取り換えるのは厄介。 するとobnizにはpwm出力があるので、これを使って明るさを変更してみよう。

pwmとは、繰り返しパルスを生成しこれを使ってLEDの点滅を行う。 高速に点滅させることで、見た目には点灯中に見える。そしてこの点灯中の時間を多くしたり少なくすることで、 明るさを変更することが出来る。では、やってみる。

~
  var pwm = obniz.getFreePwm();   // 開錠LED用PWM
  pwm.start({io:6});              // io6をアノードとしてpwm出力
  obniz.io5.output(false);        // io5をGND
  pwm.duty(50);                   // 点灯
 
~

これで、50%の明るさで点灯させることが出来る。デフォルトでは周波数は1kHzとのこと。1秒間に1000回点滅を繰り返している。 そしてdutyで設定した値がDuty比で、1/1000の1ms間のうち何%点灯中とするかの比率だ。100%にすれば全点灯。0%にすると消灯となる。 忘れがちなのが、「output(false)」でGNDを用意しておかないと、出力側だけ設定しても電気は流れない。最初はなぜ点灯しないか悩んでしまった。

併せて、Duty比については、変数化させておき、APIで変更可能としてみた。

~
  // 状態管理
  var OpenPwmDuty = 10;         // 開錠時のLED明るさ
  var ClosePwmDuty = 10;        // 施錠時のLED明るさ
~
  app.get('/ledopen/:duty', function(req, res) {
    // 開錠時LED明るさ
    if (parseInt(req.params.duty) && req.params.duty >= 0 && req.params.duty <= 100) {
      OpenPwmDuty = req.params.duty;
      res.send('OpenLED Duty is Set ' + OpenPwmDuty + '%');
      //施錠中チェック
      button.isPressedWait().then(function(pressed){
        if (!pressed) pwmOpen.duty(OpenPwmDuty);
      });
    } else {
      res.send('OpenLED Parameter Error (duty = ' + req.params.duty + ')');
    }
  });
  app.get('/ledclose/:duty', function(req, res) {
    // 施錠時LED明るさ
    if (parseInt(req.params.duty) && req.params.duty >= 0 && req.params.duty <= 100) {
      ClosePwmDuty = req.params.duty;
      res.send('CloseLED Duty is Set ' + ClosePwmDuty + '%');
      //施錠中チェック
      button.isPressedWait().then(function(pressed){
        if (pressed) pwmClose.duty(ClosePwmDuty);
      });
    } else {
      res.send('CloseLED Parameter Error (duty = ' + req.params.duty + ')');
    }
  });
~

これで、外部からも明るさを自由に変更可能だ。

クライアント

サーバ処理が出来たので、クライアントを作成する。各種APIをアクセスすれば、錠が開閉可能となるはず。 ブラウザから簡単にAPIアクセスすると、問題なく動作するので、ラクショ~。

デザインのみ頑張れば、簡単と思っていたクライアント処理なのだが、よく考えるともう一つ問題が。 クライアント側アクションによる開錠・施錠については問題ないのだが、現在の状態を表示させる際に、 onchangeイベントによる錠の変化がリアルタイムに画面に出すことが出来ない。

webのhtmlの性質上、クライアントからのアクションなら良いのだが、サーバ側アクションによるクライアントの再描画は標準実装されていない。 錠のステータスを拾うAPIは用意されているので、常時アクセスを行って変化するのを見ることになるのだが、 アクセス間隔が短いと、無駄なアクセスが多く、無駄なパケットを消費してよくない。ではアクセス間隔を広げると、 リアルタイム性が落ちて、タイムラグが大きくなる。

そこで、サーバープッシュといえばwebSocketであり、nodejsでのwebsocketといえばsocket.ioである。 socket.ioを入れて、リアルタイムに更新できるようにしよう。となると、サーバ側から改造が必要。 サーバ側にはsocket.ioを入れておいてから、接続処理を入れておく。

~
//****************************************
// socket.ioセッティング
//****************************************
var io = require('socket.io')(server);
// クライアントが接続してきたときの処理
io.sockets.on('connection', function(socket) {
  console.log("connection");
  // クライアント一覧
  getClients();
  // 送信
  socket.emit('message', { msg: 'SmartLock' });
  
  // 受信
  socket.on('message', function(data) {
    console.log("message:" + JSON.stringify(data,undefined,1));
  });
  
  // クライアントが切断したときの処理
  socket.on('disconnect', function(){
    console.log("disconnect");
    // クライアント一覧
    getClients();
  });
  // クライアント一覧取得
  function getClients () {
    io.clients(function(error, clients) {
      console.log('connect users:' + clients);
    });
  }
~

これで、サーバ側のsocket.io処理の基本部完成。

クライアント側は、まずはindex.htmlにクライアントjsのcdnへのリンクを作る。

~
    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.3/socket.io.js"></script>
~

自サーバに接続でもよいのだが、サーバアドレス可変に対応するため、cdnを使用。

接続処理部分や、各種処理部分は以下。
createdでsocketクライアントを作成して、sock()関数を呼び出す。
sock()関数側では、各種イベント毎の処理を記述。 また、APIアクセスはaxiosを使用して、APIをコールている。

~
var serverURL = 'http://' + window.location.hostname
var sockPort = '3300'
~
  data () {
    return {
      // socket
      socket: '',
      locked: false,
      socketConnected: false,
~
  created () {
    // 起動時設定
    console.log('created:')
    this.socket = window.io.connect(serverURL + ':' + sockPort)
    if (this.socket) this.sock()
  },
~
  methods: {
    // 各種関数
    sock () {
      /* socket.ioの監視を開始
      */
      var self = this
      this.socket.on('message', function (data) {
        console.log(data)
        self.msg = data.msg
        // 応答を返却
        self.socket.emit('message', { msg: 'Hello SmartLock Client' })
      })
      this.socket.on('action', function (data) {
        console.log(data)
        // ステータス再取得
        self.getLockStatus()
      })
      this.socket.on('connection', function (data) {
        // 接続中
        console.log('connection:' + self.socket.id)
      })
      this.socket.on('connect', function (data) {
        // 接続完了
        console.log('connect:' + self.socket.id)
        self.socketConnected = true
      })
      this.socket.on('disconnect', function (data) {
        // 切断
        console.log('disconnect')
        self.socketConnected = false
      })
    },
    setSmartLock (lock) {
      // 施錠・開錠を実施
      axios.get(serverURL + ':' + sockPort + '/' + lock).then((res) => {
        console.log(res.data)
        return res.data
      })
    },
    getLockStatus () {
      // 現在の錠ステータス
      axios.get(serverURL + ':' + sockPort + '/status').then((res) => {
        console.log(res.data)
        this.locked = res.data.lock
        return res.data
      })
    }
  }
~

socket通信はmessageイベントで、どうでもいいメッセージのやり取り。接続時にピンポンしている。 もうひとつ、actionイベントでは、錠に変化があった場合に飛んでくるようにしており、 ステータスの再取得をしている。これにより、リアルタイムな錠の変化が表示可能となる。

表示するhtmlのほうは、ヘッダにタイトルを表示させているが、socket状況によりつながっていない場合には赤くして、異常を知らせるようにした。 その下には、現在の錠の状態を表示。「locked」変数に状態が入っているので、これに合わせて表示をv-ifで変化させている。

~
    <!-- ヘッダ -->
    <div class="header"><h1 v-bind:class="{ 'red': !socketConnected }">{{ msg }}</h1></div>
    <div class="status" v-bind:class="{ 'lock': locked, 'unlock': !locked }">
      <h2 v-if="locked">施錠中</h2>
      <h2 v-if="!locked">開錠中</h2>
    </div>
~

また、開錠・施錠処理は、setSmartLock()関数が用意されているので、開錠か施錠の引数をセットして呼び出している。

~
    <div class="contents">
      <button class="btn unlock" v-on:click="setSmartLock('open')">開錠</button>
      <button class="btn lock" v-on:click="setSmartLock('close')">施錠</button>
    </div>
~

現在の錠の状態が変化したらsocketが飛ぶように、以下のコードを追加。

button.onchange = function(locked) {
  console.log('onChange[LockSensor]');
  if (locked) {
    // 表示
    obniz.display.clear();
    obniz.display.draw(ctxClose);
    pwmOpen.duty(0);              // 消灯[Open]
    pwmClose.duty(ClosePwmDuty);  // 点灯[Close]
  } else {
    // 表示
    obniz.display.clear();
    obniz.display.draw(ctxOpen);
    pwmOpen.duty(OpenPwmDuty);    // 点灯[Open]
    pwmClose.duty(0);             // 消灯[Close]
  }
  // 全員にsocket送信
  io.emit('action', { event: 'onchange', lock:locked, msg: locked?'door is Locked!':'door is Locked!' });
  console.log('onChange sended');
};

しかし、なぜかsocketメッセージとapiアクセスのループに陥った。 調べてみると、io.emitすると、onchangeイベントが発生しており、onchangeイベント内でio.emitするため、ループしている。 錠の変化がないのでonchangeイベントは発生しないはずなのに、なぜだろう?

obnizはハードのesp32とアプリのサーバ側とでwebsocket通信を行っていて、それを使ってサーバアプリとハードの変化を伝えていると思われる。 そこに、別のsocketで通信をしたら、影響を受けて、onchangeのイベントが反応したのではないかと思われる。 オープンソースなので、ソースを追えばいいのだが、そんなスキルもパワーもないので、別の回避方法を考える。 とりあえず、サーバ側でのonchangeイベントが発生しすぎるのが問題なので、これを抑制する方法をとる。
onchange ⇒ io.emit ⇒ onchange(emitから) ⇒ io.emit(不要)
であるため、即時発生の2回目のio.emitを飛ばさなければ、ループが切れるはず。 なので、onchangeからのio.emitを行ったら、直後のonchange後のio.emitを行わないようにタイマーを入れる。

~
  button.onchange = function(locked) {
    console.log('onChange[LockSensor]');
~
    // 全員にsocket送信
    if (sending) {
      // 送信後は一定時間送信停止
      console.log('Now Sending');
    } else {
      // 送信開始
      sending = true;
      io.emit('action', { event: 'onchange', lock:locked, msg: locked?'door is Locked!':'door is Locked!' });
      console.log('onChange sended');
      // 送信後は一定時間送信中にしてから、フラグを解除する。
      setTimeout( function() { sending = false; console.log('Sending Flag off'); }, sendingTime);
    }
  };
~

これで、socketの無限ループになることは無くなった。

しかし、動かしてみると、施錠時に画面上のステータスが「施錠中」にならない場合が多い。 ログを見てみると、どうもステータスの取得タイミングが早いようで、施錠時の状態が拾えていないようだ。 先ほどのsocketの関係で、本来のonchangeより前のタイミングで、リードスイッチではなくsocketトリガーでonchangeが起きており、 そのあとはwaitがかかるため、本来のonchangeイベントではsocketが飛ばず、結果的に施錠前のタイミングのステータスが表示されていると思われる。 困った。

これも調べてみたところ、apiコールでもonchangeイベントが起きていることが判明。 なので、施錠のAPIを呼び出したタイミングでonchangeが発生し、未動作のステータス(開錠中)を拾い、 その後ループ防止のwaitがかかり、本来のonchangeが発生しても、socketメッセージは飛ばず、ステータスの再取得が行われずに終了。 そのために、施錠中にもかかわらず、施錠のステータスが拾えず、正しい表示が出来ていないようだ。 onchangeは何でも反応してしまうようだ。

また、obnizが未接続の状態(電源が入っていない)での挙動についても、よい動きをしない。 obnizとの接続状態については、onconnect/oncloseのイベントが発生するので、フラグで管理可能。 しかしそれをリアルタイムに画面表示させるには、socketを使うこととなり、またまた動作確認が難しい。 さらに、ExpressでのAPI受けについても、obnizのonconnect内に処理を記述しているので、 切断状態だと、反応してくれず、クライアントがタイムアウトになる。

数々の不具合つぶしを行っていき、とりあえず暫定版のプログラムがサーバ側とクライアント側ともに完成した。

サーバ側処理(Rev1.0)

「express」に加えて、「socket.io」「log4js」「canvas」あたりを入れたはず。obnizは当然必須

const Obniz = require("obniz");
const express = require('express');
const path = require('path');
const log4js = require('log4js');
const logger = log4js.getLogger();
const app = express();
 
//****************************************
// Logger定義
//****************************************
//log4js-config
log4js.configure({
    appenders: {
        system: {type: 'file', filename: 'system.log'},
        console: {type: 'console'},
        access: {type: 'file', filename: 'access.log', maxLogSize: 10485760, backups: 3, compress: true}
    },
    categories: {
        default: {appenders:['system', 'console'], level: 'debug'},
        web: {appenders: ['access'], level: 'debug'}
    }
});
var systemLogger = log4js.getLogger();
var accessLogger = log4js.getLogger('web');
app.use(log4js.connectLogger(accessLogger));
 
const obnizID = "OBNIZ_ID_HERE";
const sendingTime = 1000;
 
//****************************************
// 使用するobniz定義
//****************************************
var obniz = new Obniz(obnizID);
var obnizExists = false;      // obnizの接続状態
var buttonStatus = false;     // ロック状態
 
// 状態管理
var OpenPwmDuty = 30;         // 開錠時のLED明るさ
var ClosePwmDuty = 30;        // 施錠時のLED明るさ
var servoWait = 600;          // サーボが90度動く待ち時間
var sending = false;          // socket送信中フラグ
var servoMoved = false;            // サーボ動作中フラグ
 
// パーツ定義
var button = null;
var servo = null;
 
// canvas作成
const { createCanvas } = require('canvas');
const width = obniz.display.width;
const height = obniz.display.height;
const ctxOpen = createCanvas(width, height).getContext('2d');
const ctxClose = createCanvas(width, height).getContext('2d');
 
// 描画
ctxOpen.fillStyle = "white";
ctxOpen.font = "40px Arial";
ctxOpen.fillText(" 開", 0, 60);
ctxClose.fillStyle = "white";
ctxClose.font = "40px Arial";
ctxClose.fillText("施錠中", 0, 60);
 
// 初期表示
systemLogger.info('SmartLock Server Starting...');
systemLogger.info('obniz ID is ' + obnizID);
 
/*------------------------------------------------------------
Express処理
------------------------------------------------------------*/
// Crossを有効
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
  res.header('Access-Control-Allow-Methods', 'GET, PUT, POST, DELETE, OPTIONS');
  next();
});
// Options
app.options('*', (req, res) => {
  res.sendStatus(200);
});
 
app.use(function (req, res, next) {
  res.removeHeader('X-Powered-By');
  res.removeHeader('ETag');
  res.header('Cache-Control', ['private', 'no-store', 'no-cache', 'must-revalidate', 'proxy-revalidate'].join(','));
  res.header('no-cache', 'Set-Cookie');
  next();
});
 
// routing
app.use(express.static(path.join(__dirname, 'public')));
// API
app.get('/', function(req, res) {
  res.send('Hello obniz!');
});
app.get('/open', function(req, res) {
  // 開錠
  var ret = '';
  if (servo) {
    smartLockServo(false);
    ret = { sts: true, lock: false, msg: 'door is unLocked' }
  } else {
    ret = { sts: false, msg: 'obniz is not connected' }
  }
  res.send(ret);
});
app.get('/close', function(req, res) {
  // 施錠
  var ret = '';
  if (servo) {
    smartLockServo(true);
    ret = { sts: true, lock: true, msg: 'door is Locked' }
  } else {
    ret = { sts: false, msg: 'obniz is not connected' }
  }
  res.send(ret);
});
app.get('/status', function(req, res) {
  // 状態
  if (button) {
    button.isPressedWait()
    .then(function(pressed){
      var ret = { sts: true, lock: pressed, ledopen: OpenPwmDuty,ledclose: ClosePwmDuty,servowait: servoWait }
      res.send(ret);
    });
  } else {
    var ret = { sts: false, ledopen: OpenPwmDuty,ledclose: ClosePwmDuty,servowait: servoWait, msg: 'obniz is not connected' }
    res.send(ret);
  }
});
app.get('/ledopen/:duty', function(req, res) {
  // 開錠時LED明るさ
  if (parseInt(req.params.duty) && req.params.duty >= 0 && req.params.duty <= 100) {
    OpenPwmDuty = req.params.duty;
    var ret = { sts: true, ledopen: OpenPwmDuty, msg: 'OpenLED Duty is Set ' + OpenPwmDuty + '%' }
    res.send(ret);
    //施錠中チェック
    if (button) {
      button.isPressedWait().then(function(pressed){
        // 開錠中なら即時明るさ変更
        if (!pressed) pwmOpen.duty(OpenPwmDuty);
      });
    }
  } else {
    res.send('OpenLED Parameter Error (duty = ' + req.params.duty + ')');
  }
});
app.get('/ledclose/:duty', function(req, res) {
  // 施錠時LED明るさ
  if (parseInt(req.params.duty) && req.params.duty >= 0 && req.params.duty <= 100) {
    ClosePwmDuty = req.params.duty;
    var ret = { sts: true, ledclose: ClosePwmDuty, msg: 'CloseLED Duty is Set ' + ClosePwmDuty + '%' }
    res.send(ret);
    //施錠中チェック
    if (button) {
      button.isPressedWait().then(function(pressed){
        // 施錠中なら即時明るさ変更
        if (pressed) pwmClose.duty(ClosePwmDuty);
      });
    }
  } else {
    res.send('CloseLED Parameter Error (duty = ' + req.params.duty + ')');
  }
});
app.get('/servowait/:wait', function(req, res) {
  // サーボが90度動く待ち時間
  if (parseInt(req.params.wait) && req.params.wait >= 0 && req.params.wait <= 10000) {
    servoWait = req.params.wait;
    var ret = { sts: true, servowait: servoWait, msg: 'ServoWaitTime is Set ' + servoWait + 'ms' }
    res.send(ret);
  } else {
    var ret = { sts: false, servowait: servoWait, msg: 'ServoWaitTime Parameter Error (wait = ' + req.params.wait + ')' }
    res.send(ret);
  }
});
 
/*------------------------------------------------------------
obniz処理
------------------------------------------------------------*/
obniz.onconnect = async function () {
  // 接続完了
  obnizExists = true;
  systemLogger.info('obniz is connected');
  // 接続時初回送信
  io.emit('action', { event: 'connect', sts: obnizExists, obnizid: obnizID, msg: 'obniz is connecting' });
 
  /*------------------------------------------------------------
  状態表示用のLED準備
  ------------------------------------------------------------*/
  // LED定義
  var pwmOpen = obniz.getFreePwm();   // 開錠LED用PWM
  pwmOpen.start({io:4});              // io4
  pwmOpen.duty(0);                    // まずは消灯
  var pwmClose = obniz.getFreePwm();  // 施錠LED用PWM
  pwmClose.start({io:6});             // io6
  pwmClose.duty(0);                   // まずは消灯
  obniz.io5.output(false);            // io5をGNDに
 
  /*------------------------------------------------------------
  状態チェック用のリードスイッチ処理
  ------------------------------------------------------------*/
  // リードスイッチ定義
  button = obniz.wired("Button",  {signal:8, gnd:9});
  // 変化を検知したら、表示
  button.onchange = function(locked) {
    systemLogger.info('(onChange)[1](lock:' + locked + ')');
    buttonStatus = locked;
    if (locked) {
      // 表示
      obniz.display.clear();
      obniz.display.draw(ctxClose);
      pwmOpen.duty(0);              // 消灯[Open]
      pwmClose.duty(ClosePwmDuty);  // 点灯[Close]
    } else {
      // 表示
      obniz.display.clear();
      obniz.display.draw(ctxOpen);
      pwmOpen.duty(OpenPwmDuty);    // 点灯[Open]
      pwmClose.duty(0);             // 消灯[Close]
    }
    // 全員にsocket送信
    if (sending) {
      // 送信後は一定時間送信停止
      systemLogger.info('(onChange)Now socket sending...');
    } else {
      // 送信開始
      sending = true;
      io.emit('action', { event: 'onchange', lock:locked, msg: locked?'door is Locked!':'door is Locked!' });
      systemLogger.info('(onChange)[2] socket sended');
      // 送信後は一定時間送信中にしてから、フラグを解除する。
      setTimeout( function() {
        sending = false;
        systemLogger.info('(onChange)[3] socket Sending Flag is off');
      }, sendingTime);
    }
  };
  /*------------------------------------------------------------
  錠操作用のサーボ処理
  ------------------------------------------------------------*/
  // サーボ定義
  servo = obniz.wired("ServoMotor", {gnd:0, vcc:1, signal:2});
  // センター位置へ初期移動
  servo.angle(90.0);
 
  /*------------------------------------------------------------
  本体スイッチ操作
  ------------------------------------------------------------*/
  obniz.switch.onchange = async function(state) {
    if (state == 'none') {
      // 何もしない
    } else if (state == 'push') {
      // 全員にsocket送信
      systemLogger.info('Switch Pushed!');
      io.emit('action', { event: 'push', msg: 'switch pushed!!!' });
    } else if (state == 'left') {
      // 施錠
      await smartLockServo(true);
    } else if (state == 'right') {
      // 開錠
      await smartLockServo(false);
    }
  }  
}
 
// 開錠・施錠ボタン処理
async function smartLockServo(lock) {
  var servoAngle = lock?0:180;
  // 動作中チェック
  if (!servoMoved) {
    servoMoved = true;  // 動作中セット
    servo.angle(servoAngle);
    // 初期動作のウェイト
    setTimeout( function() {
      servo.angle(90.0);
      // 戻り動作のウェイト
      setTimeout( function() {
        // 動作終了
        servoMoved = false;
      }, servoWait);
    }, servoWait);
  }
}
 
/*------------------------------------------------------------
obniz切断時の処理
  ------------------------------------------------------------*/
obniz.onclose = async function() {
  // 切断
  obnizExists = false;
  button = null;
  servo = null;
  systemLogger.info('obniz is disconnect');
  // 送信
  io.emit('action', { event: 'disconnect', sts: obnizExists, obnizid:obnizID, msg: 'obniz is disconnect' });
}
 
/*------------------------------------------------------------
ログ用日付
  ------------------------------------------------------------*/
function nowDatetime() {
  var dt = new Date();
  return '[' + dt.getFullYear() + '/' + ('0' + String(dt.getMonth()+1)).slice(-2) + '/' + ('0' + dt.getDate()).slice(-2)
    + '-' + ('0' + dt.getHours()).slice(-2) + ':' + ('0' + dt.getMinutes()).slice(-2) + ':' + ('0' + dt.getSeconds()).slice(-2)
    + '.' + ('00' + dt.getMilliseconds()).slice(-3) + '] ';
}
/*------------------------------------------------------------
Express Server起動
  ------------------------------------------------------------*/
// Server
var server = app.listen(3300, function() {
  systemLogger.info('Example app listening on port:' + server.address().port)
});
//****************************************
// socket.ioセッティング
//****************************************
var io = require('socket.io')(server);
// クライアントが接続してきたときの処理
io.sockets.on('connection', function(socket) {
  systemLogger.info('(socket)connection');
  // クライアント一覧
  getClients();
  // 送信
  socket.emit('message', { msg: 'MyHome SmartLock',sts: obnizExists, obnizid:obnizID });
  
  // 受信
  socket.on('message', function(data) {
    systemLogger.info('(socket)message:' + JSON.stringify(data));
  });
  
  // クライアントが切断したときの処理
  socket.on('disconnect', function(){
    systemLogger.info('(socket)disconnect');
    // クライアント一覧
    getClients();
  });
  // クライアント一覧取得
  function getClients () {
    io.clients(function(error, clients) {
      systemLogger.info('(socket)connect users:' + clients);
    });
  }
});

クライアント側処理(Rev1.0)

main.js/App.vueについては、初期から変更なし。router/index.jsはcomponents/SmartLock.vueにリンクを張る。 画面はすべて下記SmartLock.vueで処理する。画面遷移がないので、routerは不要だった。

<template>
  <div class="smartlock">
    <!-- ヘッダ -->
    <div class="header"><h1 v-bind:class="{ 'red': !socketConnected }">{{ msg }}</h1></div>
    <div class="status" v-bind:class="{ 'lock': locked, 'unlock': !locked, 'disabled': !obnizConnected }">
      <h2 v-if="locked">施錠中</h2>
      <h2 v-if="!locked">開錠中</h2>
    </div>
    <div class="contents">
      <button class="btn unlock" v-on:click="setSmartLock('open')" v-bind:disabled="!obnizConnected">開錠</button>
      <button class="btn lock" v-on:click="setSmartLock('close')" v-bind:disabled="!obnizConnected">施錠</button>
    </div>
    <div class="footer">
      <h5 v-if="obnizConnected">RaspberryPi 3B+ , obniz[{{ obnizID }}] , servo[MG995]</h5>
      <h5 v-if="!obnizConnected" class="red">obniz is not connected</h5>
    </div>
  </div>
</template>
 
<script>
// axios
import axios from 'axios'
// 初期設定
var serverURL = 'http://' + window.location.hostname
var sockPort = '3300'
// 開始
export default {
  name: 'SmartLock',
  data () {
    return {
      // socket
      socket: '',
      locked: false,
      socketConnected: false,
      obnizConnected: false,
      obnizID: '',
      // message
      msg: 'MyHome SmartLock'
    }
  },
  created () {
    // 起動時設定
    console.log('created:')
    this.socket = window.io.connect(serverURL + ':' + sockPort)
    if (this.socket) this.sock()
  },
  mounted () {
    console.log('mounted:')
    // 画面完成時
    this.getLockStatus()
  },
  methods: {
    // 各種関数
    sock () {
      /* socket.ioの監視を開始
      */
      var self = this
      this.socket.on('message', function (data) {
        console.log(data)
        self.msg = data.msg
        self.obnizConnected = data.sts
        self.obnizID = data.obnizid
        // 応答を返却
        self.socket.emit('message', { msg: 'Hello SmartLock Client' })
      })
      this.socket.on('action', function (data) {
        console.log(data)
        // ステータス再取得
        self.getLockStatus()
      })
      this.socket.on('connection', function (data) {
        // 接続中
        console.log('connection:' + self.socket.id)
      })
      this.socket.on('connect', function (data) {
        // 接続完了
        console.log('connect:' + self.socket.id)
        self.socketConnected = true
      })
      this.socket.on('disconnect', function (data) {
        // 切断
        console.log('disconnect')
        self.socketConnected = false
      })
    },
    setSmartLock (lock) {
      // 施錠・開錠を実施
      axios.get(serverURL + ':' + sockPort + '/' + lock).then((res) => {
        console.log(res.data)
        return res.data
      })
    },
    getLockStatus () {
      // 現在の錠ステータス
      axios.get(serverURL + ':' + sockPort + '/status').then((res) => {
        console.log(res.data)
        this.locked = res.data.lock
        this.obnizConnected = res.data.sts
        return res.data
      })
    }
  }
}
</script>
 
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
/* ヘッダ部 */
div.header, div.footer {
  height: 77px;
  background-color: #ccecff;
  text-align: center;
}
h1 {
  padding: 20px 10px 0px 10px;
  font-family: Arial, Helvetica, sans-serif;
}
h1.red, h5.red {
  color: #ff0000;
}
h5 {
  margin: 0px;
  padding: 30px 10px 0px 10px;
}
/* ステータス部 */
div.status {
  height: 77px;
  text-align: center;
}
div.unlock {
  background-color: green;
}
div.lock {
  background-color: red;
}
h2 {
  color: #ffffff;
  padding: 20px 10px 0px 10px;
  margin: 0px;
}
div.disabled {
  background-color: #c0c0c0;
}
/* コンテンツ部 */
div.contents {
  display: flex;
  flex-flow: row wrap;
  justify-content: space-around;
  margin:20px 0px 20px 0px;
}
 
.btn {
  display: inline-block;
  min-width: 170px;
  width: 40%;
  height: 200px;
  text-align: center;
  font-size: 30px;
  color: #FFF;
  text-decoration: none;
  font-weight: bold;
  padding: 10px 24px;
  border-radius: 4px;
}
 
button.btn:active {
  transform: translateY(4px);
  border-bottom: none;
}
button.btn:disabled {
  background-color: #c0c0c0;
  border-bottom: 0px solid #c0c0c0;
  color: #e0e0e0;
}
button.unlock {
  background-color: #8cd460;
  border-bottom: 4px solid #6cb440;
}
button.lock {
  background-color: #ff8181;
  border-bottom: 4px solid #df6161;
}
* {
  user-select: none;
  -moz-user-select: none;
  -webkit-user-select: none;
  -ms-user-select: none;
}
</style>

一応それなりに動いて操作できるようになった。

3Dプリンタ スマートロック編一覧

3Dプリンタ スマートロック編(1) ハード作成
3Dプリンタでスマートロックを作ってみる
3Dプリンタ スマートロック編(2) ソフト作成
3Dプリンタでスマートロックを作ってみる
3Dプリンタ スマートロック編(3) 状態検知
3Dプリンタでスマートロックを作ってみる
3Dプリンタ スマートロック編(4) obniz
3Dプリンタでスマートロックを作ってみる
3Dプリンタ スマートロック編(5) 取付中
3Dプリンタでスマートロックを作ってみる
3Dプリンタ スマートロック「2」編(1) 改善検討
3Dプリンタでスマートロック「2」を作ってみる
3Dプリンタ スマートロック「2」編(2) 交換取付
3Dプリンタでスマートロック「2」を作ってみる
3Dプリンタ スマートロック「2」編(3) 2台目
3Dプリンタでスマートロック「2」を作ってみる
3Dプリンタ スマートロック「2」編(4) ガタつき
3Dプリンタでスマートロック「2」を作ってみる
3Dプリンタ スマートロック「2」編(5) シンプルに
3Dプリンタでスマートロック「2」を作ってみる
3Dプリンタ スマートロック「2」編(6) 配線整理
3Dプリンタでスマートロック「2」を作ってみる
3Dプリンタ スマートロック「3」編(1) 再作成
3Dプリンタでスマートロック「3」を作ってみる
3Dプリンタ スマートロック「3」編(2) APIサーバ
3Dプリンタでスマートロック「3」を作ってみる