OGIMOノート~家族のためのモノづくり~

OGIMOテックノート ~家族のためのモノづくり~

重度障害の息子を持つ父親エンジニアの備忘録。自作の電子工作おもちゃ/リハビリ器具/ロボット関係の製作記録、思った事を残していきます

二輪走行ロボット「mBot」をWifiコントローラ(息子リハビリ用)で操作してみた

はじめに

前回は息子用モビリティを作成しましたが、そちらのブラッシュアップに現在取り組み中。特に、姿勢保持の課題に現在トライ中。

ogimotokin.hatenablog.com
ogimotokin.hatenablog.com

その一方で、ジョイスティックの操作感に不慣れ」という課題に対応するため、以下にしてジョイスティックの操作するモチベーションを上げる取り組みの必要性を感じました。
その操作練習のためにモビリティを使う手もありましたが、
 ・広くない我が家にて、操作練習のために常時モビリティを置いておくのが困難ために、同じ操作コントローラで動く小型二輪走行ロボットを作ってみました!

f:id:motokiinfinity8:20181220005017j:plain
f:id:motokiinfinity8:20181220005153j:plain



小型二輪走行ロボットはいくつか試してみたのですが、個人的に好きなのは
MakeBlock社のmBot です。

mBot robot kit - educational programmable robot | Makeblock®

子供のプログラミング教育用のロボットです。専用アプリでScratchベースの開発が出来る事に加えて、搭載されているマイコンArduinoベースのため、普通にArduinoベースで開発できる!
これを選んだ理由としては、
 ・二輪駆動が滑らかかつコンパクト
 ・筐体が頑丈で、多少の乱暴な扱いにも耐えられる
 ・見た目が可愛い (超音波センサーを目に見立てたデザインが子供ウケしそう)

といったとこ。

しかし、今回の私の作成した操作コントローラとは直接接続できない。
理由は、「無線モジュールが搭載されていない」点。
mBotの標準オプションとして、Blutooth搭載版はあるが、Classicのみ対応(BLE非対応)である。
一方で、今回私が使用しているジョイスティックはESP32モジュールのためBlutoothは対応しているのだが、現状世の中で普及しているソフトライブラリはBLEのため、そのままでは使うのは難しい。
BLE対応モジュールをmBotに搭載する事も検討したが、
「そもそもESP32間でどうやってペアリングするんだろう」
と悩みはじめたので、一旦これも保留。

結局 mBotに 無線対応モジュールとして最も小型化可能な ESP8266 を搭載する事にしました。

完成品


動画の通り、小型ロボットにした事で息子自身は自分の目線で動く事で親近感が沸いたらしく、それ以降はかなり積極的にジョイスティックを操作する様になりました!!
毎日使い続けようという工夫、だいじ!

しかし、前後の突起物は寝転んで遊ぶ息子には危なそうだなぁ。。
なので、あそこにはカバーが必要そう!という事で、早速3Dプリンターでカバーを設計。
既製品をベースにして、足りない部分は個々でカスタマイズ!

ただのカバーではなく、ちょっとした『遊びゴコロ』も忘れずに!
前側はカワイイお手て、そして後ろ側は……そりゃあカワイイおしりでしょ!!
という事でと無駄にモデリング頑張って、3Dプリンタで出力。

結果、なかなか愛嬌溢れた子になっちゃいました(笑)



f:id:motokiinfinity8:20181220005017j:plain



f:id:motokiinfinity8:20181220004951j:plain





その愛嬌ある見た目と、持ち運んでも壊れない頑丈さのおかげで、息子の通う療育園でも使ってもらえる様になりました!

息子の療育園お友達、普段硬直が激しくモノを触ることが出来ないのですが、ロボットの操作を行った時は硬直なく自分の意思で動かせたらしいです!
お母さんが涙流して喜んでくれたり、「うちにも欲しい」という話も頂いたりで、とても胸が熱くなりました!


ハードウェア構成

ハードウェア構成は以下。

f:id:motokiinfinity8:20181220005845j:plain

mBotに無線対応マイコンESP8266をUARTで接続するだけの構成。
ただし、mBotは5V系マイコン、ESP8266は3.3V系マイコンのため、両者間にレベル変換ICをはさむ必要があります。
レベル変換ICは以下のものを使用。
NTB0104 レベルシフタ ピッチ変換済みモジュール - スイッチサイエンス

そして、ESP8266を使ったもう一つの理由、
それは……頑張ってmBotマイコン筐体内に収める事
これにより子供のおもちゃとしての堅牢性を確保します!(大事)
そのため、具体的な回路図/レイアウトを検討してみた。

まず、接続方法から。

github.com

上記サイトからmBot側の回路図を見てみる。当初は、UART信号としてmBot側はDO端子(RXD)/D1端子(TXD)を使う予定だったが、これだとデバッグのために使うSerial(USB)とバッティングしてしまい開発上面倒くさい事になる。
私の開発スタイルは基本的に速度重視なので、Software SerialでUART信号をソフト対応する事にするため、任意のデジタル端子2系統に変更する事にした。

レイアウトを見ていると、mBot中央部にESP8266モジュールサイズ程度なら入りそうなスペースと、その隣接に5V+GND+デジタル2Pinが出力されていてピンヘッダを手付けすれば使えそうな端子があるので、ここに収まる基板サイズを作ればよさそうです。

f:id:motokiinfinity8:20181220234606j:plain
f:id:motokiinfinity8:20181220005328j:plainf:id:motokiinfinity8:20181220005344j:plain

作ったらこんな感じです。結構コンパクトに収める事ができました!
二つの基板間はピンヘッダメスで直接基板を刺す事で、配線をなくし、堅牢性を上げました。

外装は自由にカスタマイズで!

私はmBotの前後突起物が息子の目に刺さらないためのカバーに遊び心を加えたものをfusion360モデリング3Dプリンタで作りました

f:id:motokiinfinity8:20181220235322p:plain
f:id:motokiinfinity8:20181220071058j:plain


ソフトウェア構成

ソフトウェアに関して、ジョイスティックからの操作命令は先日に記載したモビリティと同じ仕様にしています。
ogimotokin.hatenablog.com

また、mBotのライブラリについては公式で用意されているので、以下を参考にしました。
github.com

一つ方針に悩んだのは、mBot / ESP8266 のどちらを本体頭脳にするのか、です。
ESP8266の方が処理性能は高いのですが、mBotの方が他のデバイス(ブザー/LED等)が接続されている事、今後の拡張性、メンテナンス性を考えると、mBotを本体頭脳に設定しました。(こうすれば別のロボットを作った際でも、ESP8266側のソフトはそのまま使いまわせるメリットを選択しました)

なので、ESP8266は、コントローラから無線経由で送信されたコマンド情報をUARTに変換するだけのシンプルな作りにしました。

また、mBot側は、今回
 ・ジョイスティック方向に応じてモータ制御を実施 (MeDCMotor.run関数)
 ・ブザー音で行動予告 (MeBuzzer.tone関数)
 ・カラーLEDで方向と色を紐づけ (MeRGBLed.setColor関数)

の機能も加えています。

全ソースは下記GitHubにも置いています。
(コード自体は汚いですが、あくまでラピッド開発的に開発しているので、とにかく自分での分かりやすさと使いまわしやすさを優先している感じです)
github.com

■mBot側(mBot_arduino.ino)

#include <Arduino.h>
#include <Wire.h>
#include <SoftwareSerial.h>
#include <MeMCore.h>

#define M_SPEED_L 6
#define M_DIR_L 7
#define M_SPEED_R 5
#define M_DIR_R 4
#define SW_UART_TX 11
#define SW_UART_RX 12

#define CMD_SIZE  12
#define STEP_PWM_1MS   80  //1秒ごとに増加するPWM値

SoftwareSerial softSerial(SW_UART_RX, SW_UART_TX);

double angle_rad = PI/180.0;
double angle_deg = 180.0/PI;

String recv_buffer;//
int i = 0;        // 文字数のカウンタ
bool inst_flg = 0;

String cmd_msg, vel_msg;
int vel_linear=0, vel_argular=0;
int motor_inst, motor_state ;
int running_mode; //0: 事前通知モード、1:事前予告なし(通常)モード
int cmd_start_time = 0;


MeBuzzer buzzer;
MeRGBLed rgbled_7(7, 7==7?2:4);
MeDCMotor motor_9(9);
MeDCMotor motor_10(10);
int Speed = 0;

void setup(){
  Serial.begin(19200);
  softSerial.begin(9600);
  motor_inst = 0;
  motor_state = -3;
  running_mode = 0;
  recv_buffer = "";
}

void loop(){
loop_start: 
  String cmdmsg = "";

  // Serial受信(内部バッファへのコピー)
  rcv_buf_update();
  
  // Serialバッファからコマンド取得
  cmdmsg = getCmdmsg();
  if(cmdmsg != ""){
    //Serial.print(cmdmsg);
    cmd_msg = (cmdmsg.substring(0,3));
    vel_msg = (cmdmsg.substring(3.7));
    running_mode = (cmdmsg.substring(7,8)).toInt();
  
    // MDC (Mobility Direction Control)の場合
    int motor_inst_tmp = motor_inst;
          
    if(cmd_msg.equals("MDC")){
        if((vel_msg.substring(0,2)).equals("-9")) motor_inst = -4;
        if((vel_msg.substring(0,2)).equals("+0")) motor_inst =  0;
        if((vel_msg.substring(0,2)).equals("+9")) motor_inst =  4;
        if((vel_msg.substring(2,4)).equals("-9")) motor_inst -= 1;
        if((vel_msg.substring(2,4)).equals("+9")) motor_inst += 1;
    }

    // レバー操作のチャタ防止対策 2回連続で同じコマンドの場合のみ有効にする
    if(motor_inst_tmp != motor_inst && motor_inst != 0 && running_mode == 0 ){
         delay(100);
         goto loop_start;
    }

    Serial.print("Motor Instructiont:"); 
    Serial.println(motor_inst);   
  }

  //モータ制御
  if(Time_Mesure(cmd_start_time) < 1000){
    Speed = 96;
  }else{
    Speed = (int)(96 + STEP_PWM_1MS * (float)(Time_Mesure(cmd_start_time)-1000) / 1000);
  }

  if(running_mode ==2){
    Speed = 96;
  }

  if(Speed > 255) Speed = 255;

  Serial.print("  ");
  Serial.println(Time_Mesure(cmd_start_time));
  Serial.print("  ");
  Serial.println(Speed);

  // Motor制御命令に変更があった場合に停止 & 音源再生
  if((motor_inst != motor_state) && motor_inst != 0 && running_mode == 0){
    Serial.println("Motor Instruction input!"); 
    // Motor停止
    motor_9.run(0);
    motor_10.run(0);
     switch(motor_inst){
          case  -5: //右後ろ
            (省略)
          case -4:  //後ろ
            (省略)
          case  -3: //左後ろ
            (省略)
          case  -1: //右 
           rgbled_7.setColor(1,0,255,255); //水色
           rgbled_7.setColor(2,0,0,0); //無色
           rgbled_7.show();
           buzzer.tone(659, 500);   //E5音階を500ms 
           break;
          case  1:  //左
           rgbled_7.setColor(1,0,0,0); //無色
           rgbled_7.setColor(2,0,255,0); //緑色
           rgbled_7.show();  
           buzzer.tone(440, 500);   //A4音階を500ms  
           break;
          case  3:  //右前 
            (省略)
          case  4:  //前
           rgbled_7.setColor(0,255,255,0); //黄色
           rgbled_7.show();  
           buzzer.tone(523, 500);   //C5音階を500ms 
           break;
          case  5:  //左前
            (省略)
        }  
  }
  
  int leftSpeed, rightSpeed;
  switch(motor_inst){
      case  -5: //右後ろ
        (省略)
      case -4:  //後ろ
        (省略)
      case  -3: //左後ろ 
        (省略)
      case  -1: //右
        leftSpeed = Speed;
        rightSpeed = -1*Speed;
        rgbled_7.setColor(1,0,255,255); //水色
        rgbled_7.setColor(2,0,0,0); //無色
        rgbled_7.show();  
        break;
      case  1:  //左
        leftSpeed = -1*Speed;
        rightSpeed = Speed;
        rgbled_7.setColor(1,0,0,0); //無色
        rgbled_7.setColor(2,0,255,0); //緑色
        rgbled_7.show();  
       break;
      case  3:  //右前
        (省略)
     case  4:  //前
        leftSpeed = Speed;
        rightSpeed = Speed;
        rgbled_7.setColor(0,255,255,0); //黄色
        rgbled_7.show();  
        break;
      case  5:  //左前
        (省略)
      case  0:  //なし
        leftSpeed = 0;
        rightSpeed = 0;
        rgbled_7.setColor(0,0,0,0); //無色
        rgbled_7.show();  
        break;
    }
    motor_9.run((9)==M1?-(leftSpeed):(leftSpeed));
    motor_10.run((10)==M1?-(rightSpeed):(rightSpeed));



  //情報更新
  delay(10);
  if(running_mode != 1){
    if(motor_inst != motor_state || motor_inst ==0 ) cmd_start_time = millis();  // 命令情報が変わった場合、時間リセット
  }else{
    if(abs(motor_inst-motor_state) >1 || motor_inst ==0 ) cmd_start_time = millis();  // 命令情報が前後で変わった場合、時間リセット
  }
  motor_state = motor_inst; //状態更新    
}


//受信用内部バッファの更新
void rcv_buf_update(){
  int size = softSerial.available();
  char c;
  int i;

  for(i=0; i<size; i++){
    c = softSerial.read();
    recv_buffer += c;
  }
}

//内部バッファからコマンド命令の検索・抽出
String getCmdmsg(){
  String result = "";
  bool incmd = false;
  int start_pos, end_pos = 0;
  int i;
  
  for( i=0; i<recv_buffer.length(); i++){
    if(incmd == false){
      // STX捜索
      if(recv_buffer.charAt(i) == 0x2){
        incmd = true;
        start_pos = i;
      }
    }else{
      // ETX捜索
      if(recv_buffer.charAt(i) == 0x3){
        incmd = false;
        end_pos = i;
        // メッセージ抽出 & メッセージより前のフレーム破棄
        result = recv_buffer.substring( start_pos+1, end_pos);
        recv_buffer = recv_buffer.substring(end_pos+1, recv_buffer.length());
        break;
      }
    }
  }
  return result;
}


■ESP8266側(mBot_esp8266.ino)

#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include <SoftwareSerial.h>

#define SerialPort Serial
#define SW_UART_TX 5
#define SW_UART_RX 4

#define CMD_SIZE  12

SoftwareSerial softSerial(SW_UART_RX, SW_UART_TX);


const char ssid[] = "ESP32_MOBILITY"; // SSID
const char password[] = "esp32_con";  // password
const int WifiPort = 8000;      // ポート番号

const IPAddress HostIP(192, 168, 11, 1);       // IPアドレス
const IPAddress ClientIP(192, 168, 11, 2);       // IPアドレス
const IPAddress subnet(255, 255, 255, 0); // サブネットマスク
const IPAddress gateway(192,168, 11, 0);
const IPAddress dns(192, 168, 11, 0);
bool wifi_connect;

WiFiUDP Udp;
uint8_t WiFibuff[CMD_SIZE];
int motor_inst, motor_state ;
String cmd_msg, vel_msg;
int vel_linear=0, vel_argular=0;
int running_mode; //0: 事前通知モード、1:事前予告なし(通常)モード


void setup() 
{
  //PIN設定
  Serial.begin(9600);
  softSerial.begin(9600);
  motor_inst = 0;
  running_mode = 0;
  
  // Wifi設定 (SSID&パス設定)
  wifi_connect = false;
  WiFi.mode(WIFI_AP);
  WiFi.softAP(ssid, password);
  delay(100);
  WiFi.softAPConfig(HostIP, HostIP, subnet); // IPアドレス設定

  Serial.print("AP IP address: ");
  IPAddress myAddress = WiFi.softAPIP();
  Serial.println(myAddress);

  Udp.begin(WifiPort);  // UDP通信開始
  Serial.println("Starting UDP");
  
  Serial.print("Local port: ");
  Serial.println(WifiPort);

}

void loop() {

loop_start:

  int packet_size = Udp.parsePacket();

  if (packet_size > 0) {
    //Serial.println("Serial Received");   
    rcvWiFi();    
    if(WiFibuff[0] == 0x2){
          String s_buf = String((char*)WiFibuff);
          Serial.write(WiFibuff, CMD_SIZE);
          softSerial.write(WiFibuff, CMD_SIZE);   
      }
  }

void rcvWiFi() {
  Udp.read(WiFibuff, CMD_SIZE);
  //Serial.print(WiFibuff);
  Udp.flush();
}

おわりに

今回頑張って mBotの筐体内に無線モジュールを入れ込んだおかげで、頑丈になっており、息子の療育園への運搬などで壊れる心配もなく
かなり安心して使ってもらえる様になりました! 子供って、本当に色々な使い方をするので、出来る限り既製品の外装部をうまく利用していくやり方がよいのかなぁ、と感じてます!

モビリティMottoyと同じ通信IFにしてありますので、これから新しいコントローラを作ったら「モビリティ」も「小型ロボット」もどちらも動かす事ができるので、色々と応用が広がりそうです!

療育園のお母さんからも「是非欲しい!」というお声を頂いたのですが、残念ながら現状だとなかなか高いお値段になってしまいます。(mBotだけで1万円以上しますので…)
ただ、mBotだと今回の用途にはオーバースペックなので、療育園リハビリ用にはもっと簡単に&安く作れる手段を検討していきたいと思います。

ESP32(M5Stack)+LED:Neopixelを使ってみた② (ライブラリ見直し)

はじめに


f:id:motokiinfinity8:20181201020813j:plain


以前の記事で、無線対応マイコンESP32モジュールに、お馴染みのマイコン搭載LED “NeoPixel" を搭載して、子供が楽しくリハビリするための操作ジョイスティックを作ってみた。

ogimotokin.hatenablog.com

そう、この2つのデバイス組み合わせは、光りモノが大好きな『子供向けのIoTおもちゃ』の作成には必須ともいえる組み合わせなのです!
・Alexaから操作できるIoTイルミネーション
スマホから簡単におもちゃのLED色を操作

など、特にこれから冬にかけてイルミネーションの季節という事もあり、可能性がどんどん広がる!
スマホや無線からLEDを光らせるのに、良い組み合わせ)


しかし、現状使っている NeoPixelの標準となっているAdafruitのライブラリだとどうも動作が安定しない。

github.com

特に、無線通信を行っている時、点灯させていないはずのLEDが時々ノイズが発生した様に一瞬光ってしまう事があるフリッカー現象)
無線が駆動しているか分かって良いともいえるのですが、これでは流石にカッコ悪い。
(これがまた、目立つんだな。。。)

最初はシリアル信号に入る物理ノイズの可能性を疑いましたが、オシロで信号を観測したところ、ノイズ発生時はデジタル信号的に出力を発しているので、どちらかというソフトバグくさいなぁ…

じわじわ進む: ESP32で33個以上のNeoPixelを使うと制御が乱れる
で紹介されていたDisable Interrupや1ms delayも効果なし。

今後に向けて難儀していたところ、別のライブラリが素性が良さそうとの情報を得たので試したみたいと思う。

ライブラリ変更 (NeoPixelBus) への置き換え

今回置き換えるライブラリは以下の「NeoPixelBus」になります。

github.com

Adafruitベースなので、ほとんど関数の使い方が同じなのでありがたい!
なので、テキストエディタの置換機能のみで変更可能。

以下が変更イメージ

#include

#include

Adafruit_NeoPixel strip = Adafruit_NeoPixel(PIXEL_COUNT, PIXEL_PIN, NEO_GRB + NEO_KHZ800);

NeoPixelBus strip(PIXEL_COUNT, PIXEL_PIN);

strip.begin();

strip.Begin();

strip.show();

strip.Show();

strip.setPixelColor(i, strip.Color(0, 0, 0));

strip.SetPixelColor(i, RgbColor(0, 0, 0));


変更はたったこれだけ(笑)


そして、実際に動作を試してみたら、、、
「フリッカー課題が解決されてる!!」

無線通信が行われていようが全くチラつきがおこらず、安定している!
これなら問題なしなので、今度から積極的にNeoPixelBusライブラリを使っていこう!

修正したソースは GitHuに置きましたよー
github.com


今日は、細かい技術話になりましたが、こんなところで!

息子向けポータブルモリビリティを改造してみた② (デバイス間通信ソフトなど)

前回のおさらい

前回記事で記載した通り、息子などの電動乗り物への搭乗機会を増やすため、子供用電動乗り物「mottoy」を改造してみました。
ogimotokin.hatenablog.com

今回は前回で書ききれなかったソフトウェアの話を簡単にします。

ソフト構成の考え方

今回のMottoy改造の様に「コントロール部」と「モータ部」に異なるマイコンに分かれている場合、そのマイコン間で通信のやりとりが必要になってきます。そのため、その通信メッセージのルールを決めてやる必要があります。この検討で大事なのは、「今後に接続デバイスを増やす事を想定して拡張しやすくしておくか」という拡張性と、「いかに複雑にしすぎず分かりやすいルールにするか」という開発速度とのバランスです。
f:id:motokiinfinity8:20181104043251j:plain

業務や商用で検討する場合、出来る限り通信エラー耐性を上げるために
フッタに「チェックサム(伝送エラー)」やら「信頼性を上げるためのハンドシェイク」等様々な耐性強化の工夫を入れるものですが、
開発時間の限られる個人製作物においてその細かい箇所に時間をかける事自体が本質ではないため、必要最低限の絞り込みを実施した感じです。
「あれこれ悩む前にまずはざっと作ってみる」
こういう判断も、ラピットプロト開発ならではかな、と思います。

この辺りは色々と賛否両論あると思いますし、私も自分のやり方が正解とは思っていないので、是非とも有識者・玄人の皆様のやり方をご教示頂けるとありがたいです( ^^) _



独自通信メッセージルールの例

という訳で、今回のやりたい事を実現するための「独自伝送メッセージルール」を定義してみました。

今回は、操作デバイスから「モビリティの移動したい方向」を伝える独自ルールになります。

■モビリティ移動方向
(コマンド名:MDC/Mobility Direction Control)

型は Byte (unsigned char)型

1byte   : 0x2 (STX:テキスト開始文字 )
2-4byte : 命令固有文字 "MDC" ASCIIで表示
5-6byte : 前方方向の速度 "+9": 前進 ~ "+0":停止 ~ "-9":後退
7-8byte : 旋回方向の速度 "+9": 左旋回 ~ "+0":停止 ~ "-9":右旋回
9byte  : 選択ジョイコン ("0" : ジョイコン、"1": バイクコントローラ)
10-11byte  : 予備 ("0x0")
12byte : 0x3 (ETX:テキスト終了文字)

これだけでは分かりにくいので、一例をあげます。

例) コントローラから、『最大速度で前進』する場合、以下の12byteを転送します。

[1byte] 2 : STX
[2byte] 77 : M
[3byte] 68 : D
[4byte] 67 : C
[5byte] 43 : +
[6byte] 57 : 9 (前進)
[7byte] 43 : +
[8byte] 48 : 0 (旋回なし)
[9byte] 48 : 0 (ジョイスティック)
[10byte] 48 : 0
[11byte] 48 : 0
[12byte] 3 : ETX


英語&数字&記号はASCIIコードで表現できるので、その値を伝送するイメージです。
変換表は以下のサイトを参考ください。

ASCII文字コード - IT用語辞典


独自ルールを決める上で気にすべきは、「メッセージの先頭&終わりをどうやって判別させるか」です。
バイスの送信側はかならず一定間隔でメッセージ先頭からデータを投げますが、受信側は必ずしもきっちりメッセージ先頭から取れるとは限りません。受信側が別の処理をするのを頑張っていた場合、受信バッファ(郵便受けのポストの様なもの)が溢れてしまいメッセージが途中で切れてしまう事は実使用上でありえます。
なので、「どこまでが一つのメッセージなのか判別できる様にする仕組み」は最低限必要かなぁ、と思います。
判別のさせ方は色々ありますが、私は『普通の文字通信では絶対に使わない値を先頭/末尾に置く』というやり方がよいかな、と思い、ASCII制御コードであるSTX/ETXを使いました。

伝送メッセージルールの応用

上記のメッセージルールを共通にしておけば、モビリティ側とコントローラ側を自由に組み合わせる事が可能です!
例えば、今は『モビリティ』を 『ジョイスティック』&『段ボールトイコン』から操作できますが、これに別の第三のコントローラを作ってもすぐにモビリティが動かせるし、
また別の新しい二輪ロボットを作った場合でも、すぐにこの3つのコントローラがすべて対応してくれる訳です!リソースのない個人開発に置いては、なかなかの効率化ですね♪
f:id:motokiinfinity8:20181123200648j:plain

ちなみに、無線の場合はWifi(UDP)通信、有線の場合はUART通信(Serial)で、同じプロトコルが使いまわせられるので、超便利!!

ソースコード

以下のGithubに置きました。色々と汚いコードですが、ご参考までに。

① モビリティ(Mottoy)側
github.com

ジョイスティックコントローラ側
github.com

Nintendo Labo バイクトイコン側
github.com

更に参考まで、UDP送信側とUDP受信側のコードを抜粋

UDP送信側
#define CMD_SIZE  12

loop{
    省略
    
    String  m_buf = "";
    m_buf.concat("MDC");  // CMD命令設定

     //前方方向速度
     if(g_stick_pos >= 3) m_buf.concat("+9");
     else if(g_stick_pos >= -1) m_buf.concat("+0");
     else m_buf.concat("-9");

      //旋回方向速度
      if((g_stick_pos+8) % 4 == 1) m_buf.concat("+9");
      else if((g_stick_pos+8) % 4 == 0) m_buf.concat("+0");
      else m_buf.concat("-9");  
  
      m_buf.concat("0");  // ジョイコントローラモード 

     byte wifi_buf[CMD_SIZE-2];
     m_buf.getBytes(wifi_buf, CMD_SIZE-2);
     sendWiFi(wifi_buf);
}

/////////////////////////////
// Wifi送信処理
/////////////////////////////
void sendWiFi(uint8_t byteData[]) {
  uint8_t stx = 0x2;
  uint8_t etx = 0x3;
  if (Udp.beginPacket(HostIP,  WifiPort)) {
    Udp.write(&stx,1); //STX送信  
    Udp.write(byteData,CMD_SIZE-2);
    Udp.write(&etx,1); //ETX送信
    Udp.endPacket();
  }
}
UDP受信側
#define CMD_SIZE  12

uint8_t WiFibuff[CMD_SIZE];

loop(){
  中略
    Udp.read(WiFibuff, CMD_SIZE);
    Udp.flush();

    if(WiFibuff[0] == 0x2){
          String s_buf = String((char*)WiFibuff);
          cmd_msg = (s_buf.substring(1,4));
          vel_msg = (s_buf.substring(4.8));
          running_mode = (s_buf.substring(8,9)).toInt();
   }
}

まとめ

今回は、「モビリティ部」と「コントローラ部」の通信メッセージについて、少し書いてみました。今回は「移動させたい方向を指示するだけ」のシンプルなメッセージですが、当然ながら細かい速度量等を設定できるメッセージも別途設定しようと思っています。(アナログジョイスティックの傾き量に応じてモビリティの速度を変える等)
この様に、個人製作物で設計思想を統一しておくのは、今後モノをたくさん作っていった時に威力を発揮するかなぁ、と思います。自分が作ったモノ同士での新たなコラボレーションが簡単にできたり、色々と楽しそう!

息子向けポータブルモリビリティを改造してみた① (ジョイスティック操作)

今回の学び技術: 電動カート改造、回路ハッキング

はじめに(作成動機)

f:id:motokiinfinity8:20181028180440j:plain

歩けない息子が自分の意志で自由に動ける様になるための練習用電動移動機器を作りたいというのが動機です。
電動移動機器を小さな子供のうちから慣れる事は子供の好奇心・成長に大きな効果があるとの研究もあり、息子の将来の可能性を考えると少しでも早く試したい!
本当は電動車椅子でやりたかったのですが、お値段が高く購入には踏み切れないので、まずは子供用おもちゃで操作練習をする事にしました。
しかし、自分で一から作るとなると時間がかかってしまう…(特に子供の体重に耐える強度のものを作ろうとすると金属加工等のノウハウがいるため手をこまねいていました)
という事で、まずは既製品をベースにちょい変更。

目を付けたのが、
子供用電動移動乗り物「mottoy」!!e-akros.com


本機の特徴は、子供用乗り物としては珍しい左右のタイヤが別々に動く独立二輪 
つまり、前後だけでなく左右旋回も出来るので小回りも効き、車タイプの乗り物に比べて移動自由度が高い! 電動車椅子の移動感に近いので、これは練習にうってつけ!

という事で、さっそく購入!

しかし、、、残念ながら、操作レバーが左右2つ別々に付いている。例えば、右側旋回する場合、左レバーは前、右レバーは後ろに倒すイメージですが、、、知的発達も遅れている息子にはまずこの操作インターフェースは扱いきれない……


という事で、このMottoyをベースに、息子に適した操作インターフェースに変更できる様なモビリティベースに改造したいと思う!

要求仕様① 息子が簡単に操作できるジョイスティックでモビリティを操る
 (コントローラの置き場所は自由に変更できる事)
要求仕様② ジョイスティック以外の手段でも操作可能。簡単に交換できる事


※改造はあくまで自己責任ですので、ご注意ください(^^;

完成品

完成動画はこちら

既製品を改造したおかげで、開発期間は約10日間

なお、コントローラ側は以前の記事で開発した
「LEDが光るジョイスティックコントローラ」です。
ogimotokin.hatenablog.com

ちなみに、ジョイスティック以外にも簡単に置き換えれる様にしてみた応用例として、Nintendo LABOのバイクToy-Conで操作してみました。

なお、Nintendo LABOのバイクToy-Conでの操作方法は過去記事を参照ください。
(過去記事の対応から、今回向けに通信ルールを変更したのみです)
ogimotokin.hatenablog.com

ベース機体分析 (分解/ハッキング検討)

まずは制御マイコン部を見てみます。座席部分のフタをあけてみると、制御回路&バッテリーが収まっていました。

f:id:motokiinfinity8:20181028182347j:plain

意外とスカス…もといシンプルに収まっている。これなら何とか回路構成を追えそうだ。
ネジを外して、基板部分を取り出してみます。

f:id:motokiinfinity8:20181104053254j:plainf:id:motokiinfinity8:20181102010016j:plainf:id:motokiinfinity8:20181104052850j:plain

これもまた結構シンプル。両面基板なので配線の流れが追いやすいね。
そんな感じで、簡易回路図+ブロック図に起こしたのが下記図

f:id:motokiinfinity8:20181102010543j:plain


一部間違いなどあるかもしれませんので、その場合はご容赦を ^^) _

ハック構想検討

どの様にハックするか? オリィ研究所で製作された「Nintendo LABOで動く車椅子」を先日体験させて頂きました。
https://twitter.com/Takeru_FTX/status/991267031520460800
それは電動車椅子のハンドルを物理サーボモータ×2で外部から物理ハックするものでした。同様の方法で、本Mottoyもハンドル部分を物理ハックしようと企んでましたが、残念ながら息子が触って壊す可能性があるため、今回はボツ。
なので、少し考え方を変えて、ハンドル部分に配線されているスイッチ自体をハックする「スイッチ回路ハック方式」に変更しました。

注目したのは、マイコンボードとスイッチ間を接続している10Pinのハーネス
ここを分離/延長して、別途ハッキングマイコンに経由させて必要な信号のみをハッキングする方式を考えました。
ハックするのは、左右のレバー操作と繋がっている4本の信号(黄/緑/橙/青)。どうやら、モーターON時は L(GND)に、OFF時はOpenにする仕様なので、
トランジスタによるオープンドレイン回路を構築して、マイコンからモータ制御方向情報に応じたON/OFF情報を通知してもらえばOKそうですね。


必要部品

① M5stack : ESP32 Dev Kitでもよかったが、「動作事前通知(動きを事前に音声でお知らせ)」をやりたかったので簡単にスピーカ再生ができるM5stackを選択
② M5stack プロトタイプモジュール
③ 圧着コネクタ XHP-10 & ハウジング (分岐用ハーネスを作成するため)
④ M5stack電源用 6V→5Vレギュレータ
  L7805CV STMicroelectronics | Mouser 日本
⑤ NPNトランジスタ×4個 

ハードウェア改造

改造後の全体イメージは以下。
f:id:motokiinfinity8:20181102013530j:plain

元の基板とハーネスの間に、自作追加延長ハーネスを追加して、その一部の信号をM5stackにつなげます。
こうすれば、もし将来的に誰かに譲渡する場合、元の市販品の状態に戻す事が出来ます。(Mottoy自体のハードウェアには手を加えていないため)

f:id:motokiinfinity8:20181104044911j:plainf:id:motokiinfinity8:20181104044820j:plainf:id:motokiinfinity8:20181104044849j:plainf:id:motokiinfinity8:20181104044915j:plain

苦戦したのはモータ左後回転。他信号同様にNPNトランジスタ経由でGPIO制御する改造を試みましたが、何故かここだけ制御できませんでした。原因を追っていくと、黄色信号の電圧が特殊であった事でした。 他の端子はOpen時は3.3V電位なのですが、黄色信号のみなぜか 1.0Vのため、M5stackからのGPIO(3.3V)をトランジスタ ベースに接続してもONできない。
ここから、ベース電圧 > コレクタ電圧 になり ベース→コレクタ方向に電流が流れる(トランジスタがPNダイオードとして動作してしまう)事が原因と推定しました。
しかし、トランジシタVf特性上 ベース電圧は0.7V以上である必要がある。悩んだ結果、ベース電圧を抵抗分圧により コレクタ電圧と同程度(1.0V)にする事で無事にON/OFF出来る様になりました。

f:id:motokiinfinity8:20181104052659j:plain

ソフトウェア制作

ソフトウェアについては、ざっと以下の図の思想で作成しています。
f:id:motokiinfinity8:20181104043251j:plain

① コントローラ部とモビリティ部を無線で接続 (どちらかをAP設定にする)
② コントローラ部で得られた操作命令(「前」とか「右」とか)をUDP通信でモビリティに通知。この時、独自で通信コマンドルール(プロトコル)を決めておく
③ ②に応じて、制御モータ量を調整


②のコマンドルールを合わせておけば、モビリティ側は変更する事なく好きなコントローラをどんどん開発&入れ替えする事が出来る、という訳ですね♪
Nintendo LABOのバイクToy-Conによるモビリティ操作も、実は以前に作ったものからコマンドルールのみ改良する作業のみ(1時間程度)で開発できたので、さっと作るには便利な考え方です!

コマンドルール等ソフト設計の詳細はまた別記事に置きたいと思います。
(現状のコードがあまりに突貫すぎて、一部上記思想からずれているので、今後に向けて少しメンテナンスしたいと思います)

まとめと今後

今回息子向けにモビリティをひとまず作ってみましたが、残念ながら搭乗しながらモビリティを動かすとおびえて泣き出してしまうので、幣息子に向けてという観点では、まだまだ改良が必要という事が分かりました。

・安定して座席に座れないので座位固定方法が必要
 →専用シート&ベルトを作る予定)
・コントローラも筐体固定が必要
・動き出しのスピードが速いので、ゆっくり動き出す制御を追加
 → 残念ながらベース機体の回路では不可なので、モータドライバから作り替えが必要そう


ただ、今回の取り組みで、
電動車椅子を購入しなくても簡単に当人に応じたカスタマイズ電動カートを改造作成できるという事が分かりましたので、これから色々と応用可能性が期待できそうです!


引き続き、改良していきたいと思います!

【リハビリおもちゃ】息子のための電子工作ピタゴラ装置α0 with M5Stack

今回の学び技術: 距離計測ToFセンサ、NeoPixel LED制御

はじめに(作成動機)

きっかけは、8月のOGIMO家族会議。
(OGIMO家は今年から毎月家族会議を実施して、議事録に各自の目標を書いていってます)

各自の目標計画を立てる際に
「父ちゃんの電子工作モノづくりと、
 母ちゃんの手芸作り&絵描き、娘のお絵かき、このコラボ作品を作りたいね」

というのがきっかけ。すぐに思いついたのは、息子が喜んでくれる様なおもちゃ作り。

どんなおもちゃを作るか悩んだ結果、「OGIMO版ピタゴラスイッチ装置」を作ろうと決めました!

・ 理由① ピタゴラスイッチが超ーーーだいすき!
息子は毎日全録で貯まっている2週間分のピタゴラスイッチを見るのが日課になるくらい大好き。コロコロとボールが転がってくるのを見るのがたまらなく好きな模様です!
 
・理由②  おうちリハビリの促進
息子氏は脳性麻痺で体のバランスが取れず、まだ立位も取れないのですが、その要因の一つが背中の筋肉が収縮して丸まってしまう事なのです。なので、背中の筋肉を伸ばす=腕を思いっきり伸ばす動作を、毎日繰り返す事が大事。しかし、子供にとって楽しくない動作を毎日する事ほど苦痛はない。なので、今回ピタゴラ装置に手を伸ばす操作を通じて、楽しく手を伸ばす仕組みを作りたいと考えました。

・理由③ 保育園の先生のやさしさ 
上記②の様な話をしていたら、保育園の担任の先生が夏休みを利用して息子のためにボールころころ器を作ってくださったのです!息子大興奮! それからというもの保育園に入ると嬉しそうに笑い、飽きずにずっと遊んでるそうです!
f:id:motokiinfinity8:20180922215320j:plain
そんな姿を見て、「これを我が家でも導入したい!」と感じました。

完成品

完成品はこちら
f:id:motokiinfinity8:20180922175142j:plain

ピタゴラ大好きな息子トモカツのために家族全員で作った(リハビリ用)電子工作ピタゴラ装置α1。名付けて「トモカツスイッチα1」
ボールを転がすとLEDが光り、最後まで転がるとM5stackにてピタゴラフィニッシュ!
デザインは、妻&娘による手作り。図工と電子工作のあたたかいコラボです。妻の上手なイラストと、娘画伯の独特なイラストセンスか面白い!


そして、息子の反応!

早速、今朝に見せてみたのですが、ものすごい食い付きぶり!
楽しそうに「きゃっはー!!」と声を揚げながら喜んでくれてる。。。
父ちゃんやみんな、頑張ったかい、あるよ。そんだけ喜んでくれたら、なんだか目頭が熱くなる……

設計構想

では、順番に中身について触れていきます。
まずは、妻と共に構想設計を実施!

f:id:motokiinfinity8:20180922175625j:plain

ギミックとしては、息子の好きな光物(LED点灯)とピタゴラスイッチ音の組み合わせ。

【ギミック1】ボール転がりに従ってLEDも順番に光っていく
【ギミック2】最終段まで落ちると「音」と「ピタゴラスイッチ画像」でピタゴラフィニッシュ!


どんだけ凝ったことをしても息子が気に入らなければ意味がないので、まずはこの最低限の機能を付けていきます。
息子の視線を出来るだけ上げたいので、LED等は比較的上側に搭載。

そんな感じで、4段構成でじわじわ試行錯誤していたら、、、
息子が作成途中にも関わらずさっそく遊び始めてしまった!(笑)

f:id:motokiinfinity8:20180922173527j:plain

これは完成を急がねば、という事で、予定変更!!
下2段の完成を最優先に、まずは日程優先で取り組みを開始しました!

ハードウェア構成

f:id:motokiinfinity8:20180922175638j:plain

レール作成

①背面
段ボールをホームセンターで購入。これに画用紙を張り付ける。
(子供でも簡単に加工できる様にするため)
f:id:motokiinfinity8:20180922173434j:plain


②レール上段
同様にホームセンターで雨どいを購入。
軒とい(ハイ丸) 2700mm パナソニック(Panasonic) 雨どい 【通販モノタロウ】 KQ4124〜
これにドリルで穴をあけて麻紐で段ボールに括り付ける。
(子供がおもいっきり引っ張っても取れない様にするため)


f:id:motokiinfinity8:20180922213440j:plain



③レール下段
2ℓペットボトルを切って加工。同様にドリルで裏穴をあけて麻紐で段ボールに括り付ける。


f:id:motokiinfinity8:20180922213449j:plain



④上段のボール検知センサ
雨どいの先端に距離測定モジュールを接続。センサーを雨どいの先端に付ける事で一つのセンサーのみで済みます。センサーはレーザー式測距センサーVL53L0Xを使用。
VL53L0X TOFレーザー測距センサモジュール - VL53L0X - ネット販売

指向性が高く(10度以下)、測定精度が高いこと、レール距離が45cmあるため、こちらを使用しました。

しかし、指向性が高いとはいえ、センサーをしっかりレーザー方向と並行に設置しないと正しく測定できないため、今回は雨どいにはまる様な固定冶具を3Dプリンタで測定しました
雨どいの3Dデータはなかったので、気合いで実測して合わせこみました(笑)


f:id:motokiinfinity8:20180922213502j:plain




⑤下段のボール検知センサ
上段と同じく上記レーザー式測距センサーの近距離版VL6180を使いました。
VL6180X TOF近距離センサモジュール - VL6180X - ネット販売

別にVL53L0Xでも構いませんが、ここでの用途は15cm以下の検出で問題なかった事、そして単に自分がいろんなセンサーを使ってみたいという興味(笑)からあえて、異なるセンサーを使いました。
こちらは、ペットボトルの底にドリルで穴をあけて固定しました。
f:id:motokiinfinity8:20180922181650j:plain

⑥LEDテープ
雨どいに並行に張り付ける想定でしたが、テープが雨どいのコーティングのせいで全く粘着テープが張り付かないので、雨どいとLEDテープの両者を固定する冶具を3Dプリンタで作成しました。

f:id:motokiinfinity8:20180922211700j:plain


⑦M5Stack
見た目をよくするために段ボールに埋め込む様にしました。プロトモジュールを2枚重ね、基板は半分にカット。これにより配線はすべて後ろに隠れるので見た目も安心!


f:id:motokiinfinity8:20180922211730j:plain
f:id:motokiinfinity8:20180922211743j:plain



ソフトウェア開発

コード全体は以下GitHubにあげました。
github.com


なお、開発する上でのポイントは以下の三点でした。

① 二つの測距センサを同時に使用
②ボール転がり位置に応じた状態遷移
③上段センサーの測定値に応じてLED表示箇所を変更

①二つの測距センサを同時に使用

VL53L0Xのライブラリは以下を参照。
GitHub - pololu/vl53l0x-arduino: Pololu Arduino library for VL53L0X time-of-flight distance sensor

一方、VL6180Xのライブラリは以下を参照
GitHub - pololu/vl6180x-arduino: Pololu Arduino library for VL6180X distance and ambient light sensor

こちらのサンプルコードを使えば、距離を測る事は出来そうです。

問題は、同じI2Cラインに乗った二つのモジュールの判別。データシートによると初期アドレスはどうやらどちらも同じ 0x52である模様。「じゃあ、I2Cアドレスをどうやって変更するのか?」と調べていたところ、ST microサイトにあるアプリケーションに対応方法が記載されていた

https://www.st.com/content/ccc/resource/technical/document/application_note/group0/0e/0a/96/1b/82/19/4f/c2/DM00280486/files/DM00280486.pdf/jcr:content/translations/en.DM00280486.pdf
VL53L0Xについて,複数接続のための調査 - yuqlidの日記

ここにはGPIOエキスパンダを使えと書いてあるが、今回は2個のみなので無視。
要は、それぞれのデバイスのXSHUTピン(VL6180Xの場合GPIO0)を使う様だ。XSHUTがトリガされている場合は内部リセット状態になっているため、以下の手順でI2Cアドレスを書き換えてやればよさそうだ。

手順① デバイス1/デバイス2共にXSHUT端子をL固定 (Non-Active)
手順② デバイス1のXSHUT端子をH(Active)に変化、その後、デバイス1のI2Cアドレスを書き換え
手順③ デバイス2のXSHUT端子をH(Active)に変化、その後、デバイス2のI2Cアドレスを書き換え 

なるほど、こういう使い方をするのか…

#include <M5Stack.h>
#include <Wire.h>
#include <VL53L0X.h>
#include <VL6180X.h>

VL53L0X m_sensor;  //上段のセンサー
VL6180X f_sensor;   // 下段のセンサー(finish確認用)
unsigned int m_sensor_val, f_sensor_val;

void setup() 
{
  M5.begin();
  Wire.begin();
  pinMode(M_SENSE_RST_PIN, OUTPUT);
  pinMode(F_SENSE_RST_PIN, OUTPUT);
  digitalWrite(M_SENSE_RST_PIN, LOW);
  digitalWrite(F_SENSE_RST_PIN, LOW);

  // センサー初期化設定
  digitalWrite(M_SENSE_RST_PIN, HIGH);
  m_sensor.init();
  delay(100);
  m_sensor.setAddress(24);
  m_sensor.setTimeout(500);
  m_sensor.startContinuous();  

  digitalWrite(F_SENSE_RST_PIN, HIGH);
  f_sensor.init();
  delay(100);
  f_sensor.setAddress(25);
  f_sensor.configureDefault();
  f_sensor.setTimeout(500);
}

void loop() 
{
  unsigned int m_sensor_tmp[5];
  unsigned int f_sensor_tmp[5];

  // センサー測定値誤差ばらつきの平均化
  m_sensor_tmp[0] = m_sensor.readRangeContinuousMillimeters();
  f_sensor_tmp[0] = f_sensor.readRangeSingleMillimeters();
  delay(5);

  m_sensor_tmp[1] = m_sensor.readRangeContinuousMillimeters();
  f_sensor_tmp[1] = f_sensor.readRangeSingleMillimeters();
  delay(5);
  
  m_sensor_tmp[2] = m_sensor.readRangeContinuousMillimeters();
  f_sensor_tmp[2] = f_sensor.readRangeSingleMillimeters();
  delay(5);

  unsigned int m_sensor_val = (m_sensor_tmp[0] +m_sensor_tmp[1]+m_sensor_tmp[2])/3;    
  unsigned int f_sensor_val = (f_sensor_tmp[0] +f_sensor_tmp[1]+f_sensor_tmp[2])/3;   

  // LCD Display
  //M5.Lcd.setTextColor(GREEN ,BLACK);
    M5.Lcd.setTextSize(3);
    M5.Lcd.setCursor(0,  48); M5.Lcd.print("Dist:");
    M5.Lcd.setCursor(128,  48); M5.Lcd.print("          ");
    M5.Lcd.setCursor(128,  48); M5.Lcd.print(m_sensor_val);
    M5.Lcd.setCursor(128,  96); M5.Lcd.print("          ");
    M5.Lcd.setCursor(128,  96); M5.Lcd.print(f_sensor_val);

}
②ボール転がり位置に応じた状態遷移

今後の拡張も考えて、少しまじめに状態遷移図を作ってみました。
f:id:motokiinfinity8:20180922190355j:plain


前回の記事で学んだタスク管理を生かして、LED専用タスク、音声再生用タスクを実施しようとしましたが、ここでトラブル発生。

失敗談①: 音声再生タスクと、LED表示タスクを同時に実施した結果、音声カクツキ & LED表示の遅延が発生。タスク優先度を変更するが状況は変わらず
 ⇒ wav再生処理量とLED処理の性能問題か? 一旦、LED処理を行う状態1では音声処理を行わない方向に変更する事で暫定対策をしたが、原因解析は継続

③上段センサーの測定値に応じてLED表示箇所を変更

NeoPixelの使い方は前記事参照。
ogimotokin.hatenablog.com

  // LED Display
  // LED位置換算(実測による補正付)  25個まで(390mm以下)で条件分け
  int g_pos, g_pos_old;
  if(m_sensor_val < 40 || m_sensor_val > 1000) g_pos = -1;
  // 390mm以下の場合
  else if(m_sensor_val<= 390){
    g_pos = (m_sensor_val-40) / 14;
  }// 390mm以上の場合
  else  g_pos = 25 + (m_sensor_val-390) / 4.5;

  if(g_pos >= PIXEL_COUNT) g_pos = PIXEL_COUNT;

  
  int i;
  switch(g_state){
    case 0: 
      if(g_state_flg == 1){
          for(i=0; i<PIXEL_COUNT; i++){
            strip.setPixelColor(i, strip.Color(0, 0, 0));
          }
      }
      break;
    case 1:
    case 2: 
      //if( g_state_flg == 1){
      //if(g_pos != g_pos_old){
      for(i=0; i<PIXEL_COUNT; i++){
         if(i < g_pos)  strip.setPixelColor(i, Wheel(i * 255 / PIXEL_COUNT));
         else strip.setPixelColor(i, strip.Color(0, 0, 0));
       //}else if(g_pos > g_pos_old){
       //   if(i > g_pos_old && i < g_pos)  strip.setPixelColor(i, Wheel(i * 255 / PIXEL_COUNT));
       //}else if(g_pos < g_pos_old){
       //   if(i > g_pos && i < g_pos_old) strip.setPixelColor(i, strip.Color(0, 0, 0));
       //}
       }
       break;
    case 3: 
       strip.setPixelColor(i, Wheel(i * 255 / PIXEL_COUNT));
       break;           
    }
    delay(1);
    portDISABLE_INTERRUPTS();
    delay(1);
    strip.show();
    portENABLE_INTERRUPTS();

失敗談②: NeoPixel LEDが40個近く並ぶとたまに無表示時でもノイズが発生
⇒ 原因調査をしたところ、下記サイトに解決策を見つけた。
じわじわ進む: ESP32で33個以上のNeoPixelを使うと制御が乱れる

LEDが増えてshow()での処理に1mSec以上かかってしまうとデータ送出の最中にインタラプトが入ってしまいフォーマットが崩れる事が原因らしいです。解決策は上記コードにマージ済みです。

まとめ

今回は 家族の得意分野を使った合作になりました。手作り感満載ですが、そんなアナログ工作とデジタル工作のコラボレーションも、また温かみがあって良いかなぁ、って思います。
何より息子がこんなに喜んでくれて、
『モノづくりをすることの喜びの原点』
を思い返させてくれました!

ありがとう息子!

時間を見つけて、残りの上段部分の開発を実施します。せっかくなので、もう少しギミックを増やして、飽きのこない作りにしたいですね!

ESP32+LED(NeoPixel)で、リハビリ支援用光るジョイスティックを作ってみた①

f:id:motokiinfinity8:20180901142652j:plain

今回の学び技術: NeoPixel LED制御、ESP32のマルチタスク処理 (別タスクでLED制御)

はじめに(作成動機)

息子向けの足の代わりになる様なモビリティの開発を構想しているのだが、まず一番大事なのは操作系!まず現状の電動車いすの標準操作であるジョイスティック操作」、これを使えるかどうかをまずは見極めたい。
息子はまだ意志ある言葉は話せないのだが、手は器用に動かせるので、、、
もしかしたらジョイスティックでの操作が詳細な行動意志(「前に行きたい」「後ろに戻りたい」)を表すツールになりえるかもしれない!

ただし、息子くんはまだ自分の搭乗している乗り物が勝手に動く事に恐怖心を覚えてしまう。
乗り物に乗る行動が、本当に生理的に嫌いな行動なのか、それとも一度楽しさを覚えればそこから新しい世界(経験)が広がるのか、現時点では不透明だが、
親としては後者の可能性を信じて広げる手助けをしていきたい!
なので、本人の可能性を広げるため、息子に楽しんでもらいながら(←ここ重要)
一歩ずつモビリティ操作の練習を進めていきたいと思う。

なので、以下の作戦でスティック操作 = 意志を伝達する練習をしていく!


◆Step1 : ジョイスティックを動かすと…何かが起こる!(物は動かない)
◆Step2 : ジョイスティックを動かすとロボットが動く!(物が動く)
◆Step3: ジョイスティックを動かすと自分の乗った乗り物が動く (自分が動く)

そこで、Step1~Step3まで使い続ける事のできる『息子専用ジョイスティック』を作ってみる事にした。

要求仕様

 ・スティックは大きく簡単な力で動く事
 ・防水性が高い事 (息子は気に入ると涎ナメナメするため)
 ・将来的にロボット/モビリティを操作できる事
    → 防水性を考えるとロボットとは無線通信
 ・ジョイスティック単体として、動かすと楽しい事が起こる事


最後の「楽しい事」ってなんだ!?
という事で、悩んでいましたが、、、

「そうだ、子供の大好きな光り物を使ってウェイウェイ感を出そう!」

という事で、フルカラーLEDをライン上に光らせる仕様を追加。

いやぁ、ちょうど夏休みの旅行に行ったハウステンボスのライトアップにて息子本人はかなり興奮!
体をフリフリしながらテンション上がっており、
「光と音のコラボ、最強!」
という体験をしてきたので、その体験をリモコンにも取り入れようかと。

完成品

完成したジョイスティックの写真は以下。

f:id:motokiinfinity8:20180901142652j:plain

完成品の動画と、息子の反応動画は以下。

遊んでくれてるー!!  作戦成功

方向ごとに色を分けていて、上が黄色、下が赤、左が緑、右が青 として、方向と色のことばも一緒に覚えてもらう作戦です。
また、上 or 下ならラインLEDの中央に、右ならラインLEDの右側に、左ならラインLEDの左側に LEDがウェーブして動く様にしてみました。
(最初はウェーブなしの静止状態だったのですが、息子の反応がイマイチだったので、ウェーブで動かしました。『光る+動く』が重要な様です)

そして、今回のあそびごころは「フィーバーモード」! スティックを中心以外の場所に5秒連続で倒しておくと、レインボーに光る様にする仕様です。
息子はこれにかなり食いついていて、一度フィーバーモードに入れるコツが分かったらその後は繰り返して遊んでくれてました!


ハードウェア構成

ハードシステム図は以下。

f:id:motokiinfinity8:20180901151750j:plain

将来的には、無線経由でHostロボットに操作命令を送るため、ESP32モジュール一択。
今回 M5stackを使わなかったのは防水性優先にしたかったから。
(現時点では開発途中なので電源はUSB、将来的には無接点充電を頑張る!)

■ESP32 ESP-32S NodeMCU開発ボード
  : ESP32搭載ボードなら何でもよいと思います
  https://www.amazon.co.jp/gp/product/B0718T232Z/ref=oh_aui_detailpage_o04_s00?ie=UTF8&psc=1

ジョイスティック
: ゲーセン用。まだ細かい手の操作が難しい息子が操作しやすい様、大きめのスティックを選びました。
   ジョイスティック: センサ一般 秋月電子通商-電子部品・ネット通販

■LEDテープ(NeoPixel)
: 業界おなじみシリアル信号でフルカラーLED制御可能なLEDテープ。今回は9個分を追加います。
   本当は5V駆動がよいのだが、3.3Vでも動いたので3.3V接続とする (レベル変換回路簡略のため)
フルカラーシリアルLEDテープ(17cm) - スイッチサイエンス

■筐体
; 息子の前に置ける様に大きい箱を購入。

■LEDテープ張り付け台
  : 3Dプリンタで自作。正面から光って見える様に斜め向きにテープ張り付けできる様に設置。
   f:id:motokiinfinity8:20180901144526p:plain

ソフトウェア開発

開発する上でのポイントは以下の三点
 ① サンプルコードをベースに LEDを専用で光らせる
 ② 独自点灯パタンへの置き換え
 ③ LED制御を別タスク処理 (ジョイスティックの変化に対する応答性アップ)

①サンプルコードをベースに LEDを専用で光らせる

ライブラリはおなじみのAdafruit_NeoPixel。
github.com

#include <Adafruit_NeoPixel.h>

// 定義 (合計9個のLEDを GPIO13から制御)
Adafruit_NeoPixel strip = Adafruit_NeoPixel(9,  13, NEO_GRB + NEO_KHZ800);

void setup()
{
  strip.begin();
 strip.setPixelColor(2, strip.Color(255, 0, 0)); //先頭から3個目(id=2)を赤色表示
 strip.setPixelColor(3, strip.Color(0, 255, 0)); //先頭から4個目(id=3)を緑色表示  
 strip.setPixelColor(4, strip.Color(0, 0, 255)); //先頭から5個目(id=4)を青色表示    
  strip.show(); //設定反映
}

上記だけのイメージで好きな位置のLEDを自由に光らせらるのでめっちゃ便利!!

失敗談①:最初Arduino Unoでは動いたがESP32では動かず、結局 Adafruit_NeoPixelのライブラリが古かったらしい (2年前に入れたきり更新してなかった)
 ⇒ ライブラリバージョンは要注意!

② 独自点灯パタンへの置き換え

これだけでもLEDは光るが、せっかくならLEDを色々と動かしたいし、カラフルにしたい!
自分で作ってもいいが、効率化のため既に作成されているサンプルを使っていくとよい。
私は以下のサイトを参考にした。

www.tweaking4all.com

私はRunning Lights効果をベースに改造した。
右側に倒した場合は右方向へ、左側へ倒した場合は左方向へ動かした。
なお、前の場合はセンター方向に光が集まる様に(前に進んでいる様にしたかったので)上記を参考に自作した。

また、フィーバーモードのLED点灯効果は、rainbowCycle効果をベースに改造した。

失敗談②:上記のRunning Lightsのソース内部では、三角関数を使いSin角をずらす事で波みたいな光量差を計算している。しかし、なぜかこれを使うと先頭LED(id=0)にノイズが乗ってしまう。原因は不明。
 ⇒ sin関数計算は使わず固定値(テーブルを持つ事)で対応した

③ LED制御を別タスクで回す

②で面白いLED効果が出来たが、Main関数でloopでLED切り替え操作を実施しているため、LED操作が終わるまでジョイスティックの状態を読みにいけず、ジョイスティックを別方向に動かしてから、LED効果が変更なるまで時間差が発生する。これだけなら別にLED更新周期とジョイスティック確認周期を作りこめばよいが、将来的にスピーカ制御やロボットとの通信やり取りを考えると、面倒くさい。そこで LED制御はMain関数から分離して別タスクで回す事にする。

なんとESP32は、無線対応マイコンでありながら、デュアルコアであり、FreeRTOS というリアルタイム OS で Arduino core プログラムが組まれているため、マルチタスク制御ができるとの事!なんてすごいんだ!!

下記サイトを参考に、LED処理を別タスクで処理する様に変更してみた!

www.mgo-tec.com


setup時点で、xTaskCreatePinnedToCore関数でタスク生成をして、Main関数ではジョイコン読み出し、LEDタスクではLED制御を行う。タスク間通信手段として一旦グローバル変数を使った。(もっと良いやり方あれば教えてください!)
LEDタスク側処理としては、該当グローバル変数の変更フラグをReadしたらLED処理を停止→ジョイコン値に従って次のLED処理に即座に切り替えるイメージですね。

失敗談③:Task内はwhile(1)で回さないとプログラムダンプで落ちるので注意!

作成したコードの一部は以下。

#include <Adafruit_NeoPixel.h>

#define UP_STICK_PIN   33
#define LEFT_STICK_PIN   35 
#define RIGHT_STICK_PIN   32 
#define DOWN_STICK_PIN   34 
#define PIXEL_PIN    13

//GLOBAL変数
int stick_pos = 0;    // [0: センター、4: 上、-4:下 ] + [+1:右、-1:左]を補正
int stick_pos_now = 0;  //初期値
bool _stick_pos_is_changed = false;
bool _fever_mode = false;
uint8_t red, green, blue;
int start_led, end_led;
uint32_t start_time = 0;



void setup()
{
  Serial.begin(115200);

  pinMode(PIXEL_PIN, OUTPUT);
  pinMode(UP_STICK_PIN, INPUT_PULLUP);
  pinMode(LEFT_STICK_PIN, INPUT_PULLUP);
  pinMode(RIGHT_STICK_PIN, INPUT_PULLUP);
  pinMode(DOWN_STICK_PIN, INPUT_PULLUP);
  strip.begin();
  strip.setBrightness(64);
  delay(1);  //安定待ち
  strip.show(); // Initialize all pixels to 'off'

  rainbow(10, 0, PIXEL_COUNT-1) 

  TaskHandle_t th; //マルチタスクハンドル定義
  xTaskCreatePinnedToCore(DisplayLED, "DisplayLED", 4096, NULL, 5, &th, 0); //マルチタスク起動
}




/////////////////////////////
// LED表示タスク処理
/////////////////////////////
void DisplayLED(void *pvParameters) {
    while(1){
     // もし5秒以上スティックが倒れていたら、フィーバーモード(レインボー)に遷移  
      if(Time_Mesure(g_start_time) > 5000){
        rainbowCycle(2, 0, PIXEL_COUNT-1);
      }else{
        // 5秒以下の場合、スティックの倒れ方に応じてモード変更
        switch(g_stick_pos){
          case  -5: //右後ろ
            RunningLights(255, 0,  0, 80,  2, 8,  0); 
            break;
          case -4:  //後ろ
            RunningLightsToCenter(255, 0,  0, 80,  0, 8,  0); 
            break;
          case  -3: //左後ろ
            RunningLights(255, 0,  0, 80,  0, 6,  1); 
            break;
          case  -1: //右
            RunningLights(0, 255, 255, 80,  2, 8,  0); 
            break;
          case  1:  //左
            RunningLights(0, 255, 0, 80,  0, 6,  1); 
            break;
          case  3:  //右前
            RunningLights(255, 255,  0, 80,  2, 8,  0); 
            break;
          case  4:  //前
            RunningLightsToCenter(255, 255,  0, 80,  0, 8,  1); 
            break;
          case  5:  //左前
            RunningLights(255, 255,  0, 80, 0, 6, 1);
            break;
          case  0:  //なし
           for(int i=0; i<strip.numPixels(); i++) {
              strip.setPixelColor(i, strip.Color(0, 0, 0));
              delay(10);  //安定待ち
           }
           break;
        }
         strip.show();
        }
        g_stick_pos_is_changed = false;
    }
}


/////////////////////////////
// Main処理
/////////////////////////////
void loop(){
  // スティック検出
  if(digitalRead(UP_STICK_PIN)==1){
    g_stick_pos = 4;
    wave_dir = 1;
  }else if(digitalRead(DOWN_STICK_PIN)==1){
    g_stick_pos = -4;
    wave_dir = 0;
  }else{
    g_stick_pos = 0;
    wave_dir = 0;
  }

  if(digitalRead(LEFT_STICK_PIN)==1){
    g_stick_pos += 1;
  }else if(digitalRead(RIGHT_STICK_PIN)==1){
    g_stick_pos -= 1;
  }

  //スティックが倒されている間の時間測定
  if(g_stick_pos == 0)  g_start_time = millis();  //時間リセット
  Serial.println(Time_Mesure(g_start_time));
  Serial.print("g_stick_pos:");
  Serial.println(g_stick_pos);

  if(g_stick_pos != g_stick_pos_now){
     g_stick_pos_is_changed = true;
  }
  g_stick_pos_now = g_stick_pos;

}

//////////////////////////////////
// サブ関数
/////////////////////////////////
void RunningLights(uint8_t red, uint8_t green, uint8_t blue, int WaveDelay, int start_led, int end_led) {
//コード省略
}

void RunningLightsToCenter(uint8_t red, uint8_t green, uint8_t blue, int WaveDelay, int start_led, int end_led) {
  int Position=100;
  
  set_led_out_of_range(start_led, end_led);
  
  for(int j=0; j<=(end_led-start_led)*2; j++)
  {
      if(wave_dir == 1)    Position++;
      else  Position--;
             
      for(int i=start_led; i<=(end_led-start_led); i++) {
        // sine wave, 3 offset waves make a rainbow!
        //float level = sin(i+Position) * 127 + 128;
        //setPixel(i,level,0,0);
        //double level = (sin((float)(i+Position)) * 127 + 128);
        double level;
        // sinを使うと先頭LEDに緑ノイズが発生する対策 (sinを使わない)
        // 周期 3.5LED分
        switch((i+Position)%7){
          case 0: level=0.5; break;
          case 1: level=0.99; break;
          case 2: level=0.30; break;
          case 3: level=0.10; break;
          case 4: level=0.87; break;
          case 5: level=0.75; break;
          case 6: level=0.05; break;        
        }
        // 2乗にする事で明暗差をつける対応
        uint8_t tmp_r = pow(level,2)*red;
        uint8_t tmp_g = pow(level,2)*green;
        uint8_t tmp_b = pow(level,2)*blue;                
        strip.setPixelColor(i,strip.Color(tmp_r, tmp_g, tmp_b));
        strip.setPixelColor(end_led-(i-start_led), strip.Color(tmp_r, tmp_g, tmp_b));
        delay(2);
        //strip.setPixelColor(i,
        //  strip.Color(((sin(i+Position) * 127 + 128)/255)*red,
        //              ((sin(i+Position) * 127 + 128)/255)*green,
        //              ((sin(i+Position) * 127 + 128)/255)*blue));
      }
      strip.setPixelColor((end_led-start_led)/2,strip.Color(red, green, blue));
      strip.show();

      if(_stick_pos_is_changed == true) return;
      delay(WaveDelay);
  }
}


void rainbowCycle(int SpeedDelay,  int start_led, int end_led) {
  uint16_t i, j;
  
  set_led_out_of_range(start_led, end_led);

  for(j=0; j<256*5; j++) { // 5 cycles of all colors on wheel
    for (int i=start_led; i<= end_led; i++) {
      strip.setPixelColor(i, Wheel(((i * 256 / (end_led-start_led)) + j) & 255));
    }
    strip.show();
    if(_stick_pos_is_changed == true) return;
    delay(SpeedDelay);
  }
}

uint32_t Wheel(byte WheelPos) {
  WheelPos = 255 - WheelPos;
  if(WheelPos < 85) {
    return strip.Color(255 - WheelPos * 3, 0, WheelPos * 3);
  }
  if(WheelPos < 170) {
    WheelPos -= 85;
    return strip.Color(0, WheelPos * 3, 255 - WheelPos * 3);
  }
  WheelPos -= 170;
  return strip.Color(WheelPos * 3, 255 - WheelPos * 3, 0);
}

void set_led_out_of_range(int start_led, int end_led){
  int i;
 
  for(i=0; i<start_led ;i++){
     strip.setPixelColor(i, strip.Color(0, 0, 0));
  }
  if(end_led < strip.numPixels()){
    for(i=(end_led+1) ; i<strip.numPixels();i++){
      strip.setPixelColor(i, strip.Color(0, 0, 0));
    }
  }
  delay(5);  //安定待ち
  strip.show();
}

uint32_t Time_Mesure( uint32_t st_time ){
  return millis() - st_time;
}


全体コードはGitにアップしました(9/23追記)
https://github.com/motokiinfinity8/esp32_led_controllergithub.com

汚いコードですが、興味ある方はご参考までにどうぞ!


まとめ

こんな感じで、日常に役に立つ開発ネタを探しつつ、それに加えて 毎回必ず一つは新しいモジュールや技術手法を試す事を目標にしています!
今回は ESP32のタスク処理の実施方法を学べて良かった! LED制御と合わせて、子供むけリハビリ器具には必須な技術なので、どんどん使っていこうとおもう!
次は、音と組み合わせるか、いよいよロボット操作と組みあわえるか、だな♪
頑張ろう!!

M5stack(ESP32)で朝顔水やりロボットを作ってみた

はじめに(動機)

前記事では、M5stackを使ってキッチンの手前に置く「お風呂沸かしボタン」押しロボットを作った。

ogimotokin.hatenablog.com

これに味を占めて、M5Stackを使って遊んでみようと企んでいた。
先日に訪問したMaker Faire TokyoでM5stack開発者のJimmyさんとお話して、M5stackに思い入れが少し入った事もある(笑)

そんな矢先、8月初旬(一週間前)、妻&娘から
「夏休みの旅行期間中、
 朝顔に自動で水をやる機械ができないか?」
といった相談が入ってきた。
今年で小一になる娘が学校で育てて持ち帰った朝顔の鉢、忘れがちながらも水やりを毎日頑張って、ここまで枯らさずに育ててきた。しかし、明日からの旅行で長期間不在にしてしまうので、不在中に水分量が足りなくなったら、下手したら朝顔の鉢が枯れてしまう。。。

娘も「枯らしたらどうしよう…」と不安な顔をしていたので、これはもう「やるしかない!」という事で、
朝顔に水をやるロボット』
の開発に乗り出した

依頼要件

朝顔の鉢に定期的に水を与える事
(2回以上/日)
・水をちゃんとあげられたかどうかを
フィードバックして欲しい
(万が一、うまくいってなかった場合、ばぁば召喚の可能性も想定するため)
・開発期間: 旅行出発まで
(実働期間は深夜3日間のみ)

要件はいいが、厳しいのが開発期間! モノは作れても検証期間が取れない。。。

だが、父の名誉と妻娘からの信頼感、そして今後の自分の趣味時間の確保&拡大のためにも、ここはなんとかやり遂げる!

完成品

という訳で、短期間での開発を実現するため、手元にあったM5stackを活用して、朝顔水やりロボットを完成させた。

完成ハードウェア写真は以下。
f:id:motokiinfinity8:20180815000733j:plainf:id:motokiinfinity8:20180815000736j:plain

完成動画は以下。

ロボット仕様

・一定時間(4時間置き)にロボット起動。
 バケツの水からポンプでくみ上げて、朝顔鉢上に放水!
 放水箇所を分散させるためホースは可動式にする
・放水動作完了したら、Lineに完了通知をお知らせ
 その際、温度&湿度&土壌乾燥度の情報も通知。
 特に土壌乾燥度は放水前後の値を取得する事で、確実に放水されている事を検知する

正直なところ、M5stackである必要はあまりない。だって、屋外に置いて放置してる(しかも留守居にしている)ので、液晶&スピーカは本来は無用なのだ。
しかし、真の目的は、娘&妻へのプレゼン&信頼感を得る事であるため、
「映像&音でデモ映えする方が、より『おおーっ!』って言ってもらえるし、
 個人的にもモチベーション上がる!」
という事だ。個人モノづくりにとってモチベーションは何よりも最優先だと思う!




ハードウェア検討

全体システム図

f:id:motokiinfinity8:20180815231717j:plain

水やりのためのポンプ&ホース選定は以下のサイトを参考にした。
2m3g1.com

「水中用ポンプ」「ホース」は時間がなかったので全く同じものをAmazonで購入。
水中用ポンプが12V駆動なので、屋外コンセントレス環境での使用を想定して
「乾電池BOX 単三×8本」も購入した。
https://www.amazon.co.jp/gp/product/B0714BK7M5/ref=oh_aui_detailpage_o02_s00?ie=UTF8&psc=1

水中用ポンプの電源ON/OFFを切り替えるのは、トランジスタを使うのがラク
なので一般的であろうNPN型2SC1815-GRを購入。

また、M5stackへは5V入力が必要なので、12V→5V変換DC/DCモジュールも使用した。
https://www.amazon.co.jp/gp/product/B010RYGGJC/ref=oh_aui_detailpage_o01_s00?ie=UTF8&psc=1
サイズ感の割に対応電圧幅が広いため、まとめ購入して重宝しているDC/DCモジュールだ。

温度/湿度センサーは、以前にM5stackのプロトキットを購入した際に付いてきたDHP-12モジュールを使用。GROOVE対応のため、接続イメージは簡単。

土壌乾燥度センサーは、園芸系電子工作ではよくつかわれているYL-69を使用。
https://www.amazon.co.jp/gp/product/B01H6WK51I/ref=oh_aui_detailpage_o04_s00?ie=UTF8&psc=1
水分量(電圧)に応じたアナログ値を出力してくるため、M5stack側はAD端子で受ける必要がある




ソフトウェア検討

詳細に記載すると大変なので、メモとポインタだけ。

Deep Sleep遷移設定

電池駆動である事を考えると、常にマイコン駆動させておくのは非効率のため、この機会にESP32の売り機能であるDeep Sleep機能を使ってみる事にした。Deep Sleepモードとは、要はRTC/割込み系等最低限のハードウェアだけ生かして他コアはSleepさせるモードの事。Deep Sleep時の待機電流は数十uAなので、電池駆動機器に使うには必須となる機能である。

d.hatena.ne.jp

上記サイト等を参考にさせてもらった。Deep Sleepからのwake条件は、RTC Timer時間指定や、特定のGPIO値の割り込みなどが選択できる様だ。今回は RTC Timer時間指定をして、一定時間毎にモジュールを起動させる様にした。

#include "esp_deep_sleep.h"

void setup() {
   (中略)
  esp_deep_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_OFF);
  esp_deep_sleep_pd_config(ESP_PD_DOMAIN_RTC_SLOW_MEM, ESP_PD_OPTION_OFF);
  esp_deep_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_OFF);
  esp_deep_sleep_pd_config(ESP_PD_DOMAIN_MAX, ESP_PD_OPTION_OFF); 
}

void loop() {
   (中略)
    // DEEP SLEEPモードに移行
    const uint32_t DEEP_SLEEP_uS = 1000*1000*60*60; //1時間置きに起動
    esp_deep_sleep_enable_timer_wakeup(DEEP_SLEEP_uS);  // wakeup(restart) after 6hours
    esp_deep_sleep_start();
}
温度/湿度センサDHT12動作

github.com
を参照。cloneデータを解凍してライブラリに入れておけばOK

Line通知

調べたところ、簡単なやり方としては、
 IFTTT連携 → Line Notify通知
になる様だ。

ESP32 Wifi経由でIFTTTへ接続しLINEへ通知 – thousandiy

上記サイトを参考にすればOK
 ・Lineへの投稿文章は、IFTTT側のアクションフィールド内で定義
  その際に、変数(String型)は3つまで設定でき、ESP32側で変数を作って投稿する

Arduino側の設定環境は以下

const char* ifttt = "maker.ifttt.com";  // Server URL
WiFiClient client;

void setup() {
   (中略)
}

void loop() {
   (中略)
      // LINEに完了通知を送信
      // Make a HTTP request:
      String  value1 = String(temper, DEC);    //value1: 外気温度表示
      String  value2 = String(humidity, DEC);  //value2: 外気湿度表示    
      String  value3 = String(dojyo_val, DEC);    //value3: 土壌乾燥値の表示(水やり前)
    
      while(!client.connect(ifttt, 80)){
         Serial.print(".");
         delay(500);
      } 
      Serial.print("client,Connect");
      String url = "/trigger/" + makerEvent + "/with/key/" + makerKey;
      url += "?value1=" + value1 + "&value2=" + value2 + "&value3=" + value3;
      client.println("GET " + url + " HTTP/1.1");
      client.print("Host: ");
      client.println(ifttt);
      client.println("Connection: close");
      client.println();

      int count = 0;
      while (!client.available()) {
        delay(50); //
        Serial.print(".");

        count++;
        if (count > 20 * 20) { // about 20s
          Serial.println("(send) failed!");
          return;
        }
      }
}

開発ハプニング事例

①定期Wake時間のSleep時間

esp_deep_sleep_enable_timer_wakeup関数の引数値を試しに6時間程度に指定してみたが、なぜか10分程度の間隔になってしまった。そこで、- Read the Docs for Businessなどのサイトを調べたところ、最大でInt32最大us=71.5分しかできない事が判明した。つまり、6時間Sleepはそのままでは不可である。
そこで、作戦を以下に変更した。
 ・モジュールは60分毎に起動
 ・起動したらRTCを取得して現在時刻を把握
 ・起動した時間情報が想定の時間であれば放水開始

RTCを取得するコードは以下。納品当日に変更したが、一発勝負で上手くいった!!

void setup() {
  //NTP設定
  configTime(9 * 3600L, 0, "ntp.nict.jp", "time.google.com", "ntp.jst.mfeed.ad.jp");
}

void loop(){
  struct tm timeInfo;
  getLocalTime(&timeInfo);
  Serial.print(timeInfo.tm_year);
  Serial.print(" ");
  Serial.print(timeInfo.tm_mon);
  Serial.print(" ");
  Serial.print(timeInfo.tm_mday);
  Serial.print(" ");
  Serial.print(timeInfo.tm_hour);
  Serial.print(" ");
  Serial.print(timeInfo.tm_min);
  Serial.print(" ");
  Serial.print(timeInfo.tm_sec);

 // 一定時刻(8時/12時/16時/20時/23時)のみ水やり動作実施
  if (timeInfo.tm_hour == 8 || timeInfo.tm_hour == 12  || timeInfo.tm_hour == 16 || timeInfo.tm_hour == 20 || timeInfo.tm_hour == 23 ){
 (中略)
 }
}
②M5stack ADC端子からanalogReadが出来ない

土壌センサの値がうまく読めず0値状態。信号の電圧値は3.3V近いのに何故!?
M5stack側の設定を疑いにかかった結果、ピン依存がある事は見えてきたが、原因解明まで至らず。。。

Pin25→〇(Speaker共用)
Pin26→〇
Pin35→×(0値)
Pin36→×(0値)

一旦時間がないのでPin26を使って回避しておくが、謎すぎる。。。
公式としてはPin35&36がADC推奨端子の様に見えるのだが、回路図を見ても、GPIO35/36共にESP32に直結してるだけだし…謎だ。

M5stack側の単品不良の可能性も踏まえて、後日追い込んでみよう。

③水回り周辺の電子機器開発トラブル

実は、初めての体験だった。やはり水と電気は相性が悪い。
事件は、納品前日の深夜3時。水を入れない状態で、一通りのモジュール結合動作を確認完了。
「さて、最後にポンプで水を吸い上げる事を確認したら寝よう」
と考えて、ベランダで最終確認をしようとしたのだが、その際に誤って、、、バケツの水をはねさせてしまい、モジュールに水がばさっとかかってもた!
突然M5stackが真っ白くざざっとノイズが乗って、その後沈黙。
これにより、M5stack本体と、DHT12モジュール/YL-69モジュールがやられたっぽい。

正直、納品前日(旅行出発前々日)の深夜3時にやらかしたので、、、もう絶望しかなかった。
幸い、M5stackは手元に予備があったので助かったが、、、DHT12モジュールは現品のみだったので、
温度/湿度/土壌乾燥度を通知する仕様はあきらざるを得なかった。

自分の不注意ではあるのだが、、、、今後は気をつける




最後に

今は、旅行先からの投稿になるのだが、無事に
朝顔の水やりが完了しました」
通知を受信しており、安定動作している様で、とても安心している。

f:id:motokiinfinity8:20180816080605j:plain

娘も「朝顔さん、枯れてなさそうでよかった」と嬉しそうにしていて、まさに『家族のためのモノづくり』が出来てる実感を持てて、嬉しい限り!


せっかく作った水やりロボットだが、この旅行を最後に使う事がなくなるのは大変もったいないので、
いっその事、『ロボットを使うために植物を育てる』事をやってやろうか!?


(これを、目的と手段の逆転、という)


本ロボットをバージョンアップさせるならば、
 ・土壌乾燥度を見ながら、
乾いてきたら自動で水を実施する
 ・M5stack→ESP32モジュール変更&
省電力機能をまじめに実装する

辺りを頑張ってみたいので、もし新たな植物を育てるネタができたら、かな。