02 8月

うちのルンバにTwitterやらせてみた

一人暮らしを初めてはや5年目。
以前からルンバには興味があったのだが、先日のAmazonプライムデーでルンバ870が安くなっていたのでついつい買ってしまった。今週月曜日に届いて早速セットアップしたところ、使い勝手(というかほぼ何もしなくて良いところ)、掃除性能とも基本的に満足していたのだが、一点だけ不満が。

ルンバは、基本的に自分がいない時に掃除をしてもらう掃除機だ。
曜日ごとに、掃除の時間を指定しておき、その時間になったらルンバが自動的に掃除をしておいてくれる。
これは非常に便利なのだが、一方で問題でもある。そう、ルンバが正しく掃除をしたかどうか、帰宅時に見てもわからないのである。
実際今週の木曜と金曜は、何故か内蔵時計が狂っていた(設定をミスった?)おかげで掃除を行っておらず、部屋に落ちていた糸くずでそれに気づいたりした。

これを防ぐため、掃除の開始時、終了時にルンバにつぶやいてもらうことにした。
最初はSDカードなどを繋いで詳細にログを取ることも考えたが、設計に手間が掛かるし、何より面倒ですぐにログを見なくなる気がする。
そこで考えついたのがTwitterとの連携だ。

Twitter対応したルンバ

Twitter対応したルンバ

以下、適当に解説してみる。

事前準備

データの取得

ルンバにはRoomba Open Interface(ROI)というインターフェースがついている。
詳細はメーカーであるiRobotが公開しているドキュメントを見ると分かるが、本体に設けてある7ピンのminiDIN端子を使ってルンバの状態取得や制御ができるのだ。

iRobot® Roomba 500 Open Interface (OI) Specificationより

ROI端子にはUARTのTX、RXおよびバッテリ端子、そしてGNDが出ているので、これを自作の回路と繋げば良い。
なおUARTの信号は5V系なので必要に応じてレベル変換が必要。
バッテリ電圧はニッケル水素電池タイプの870では14.4Vだったが、最新のリチウムイオン電池モデルに関しては不明。

ROIの通信に関しては、少々面倒なところがある。

  1. コマンドの体系や長さに一貫性がない
  2. フローコントロールが無い
  3. 高レベルな情報が取れない

1.、2.は組み込み制御の観点では非常に厄介だ。コマンドを送ってもAckが無いし、引数の数などを間違ったり、ルンバが何かの都合でコマンドを受け取りそびれると、そこからコマンドがズレる。よって、条件によって取得する情報を変えるといった複雑な制御は向かず、毎回固定のコマンドを送りつつ、正常にレスポンスが帰ってきたらそれを読み込むといった方法が関の山だ。

3.はこれまた厄介で、ROIのコマンドでは今回取得したい”ルンバが掃除中かどうか”という情報は取れない。センサ情報やバッテリ電圧などの低レベルな情報は取れるのだが、現状ルンバの頭脳が何を考えているのかは取得できないのだ。よって今回はメインブラシモータに流れる電流を監視し、これが一定の値を超えたら掃除中と見なすことにした。

Twitterへの投稿

ルンバからのデータ取得方法が決まったところで、Twitterとの連携方法を考える。
これには最近(というかちょっと前に)流行ったEspressif Systems社のWi-Fiモジュール、ESP-WROOM-02を使用した。 ESP-WROOM-02はArduino互換機として使用することができ、Sketch(Arduinoのプログラム)を書くことでWi-Fiを使った通信が可能なモジュールだ。また、Twitterへの投稿は、ThingSpeakというサービスを通して行っている。

この辺りは先人が詳しく解説してくれているので、以下を参照のこと。
秋月電子で買えるパーツでESP-WROOM-02のArduino開発環境セットアップ
ESP-WROOM-02(ESP8266)からTwitter投稿

Arduino化したESP-WROOM-02では、OTA(On The Air)アップデート対応のファームウェアを書き込むことでWi-Fi経由でファームウェアをアップデートすることも可能なのだが、今回は何でだかアップデートできたりできなかったり、不安定だった(極稀に動くが確立低め)。
一応メモがてらリンクを貼っておく。
ESP-WROOM-02 + ArduinoOTAでスケッチのWiFi経由アップロード

実装

回路

前述の内容を簡単にブレボ上で実験した上、最終的に作った回路はこんな感じ。ESP-WROOM-02がマイコンを兼ねるので、あとは電源回路を足すだけのお手軽さ。レベルシフトは面倒なので下りは分圧、上りは直結。

製作した回路

製作した回路

秋月電子で部品を調達し、ユニバーサル基板で実装。一瞬基板を起こそうかとも思ったのだけど、今回のように不便を解消する系の工作は、なるべく早く導入するのに越したことはない。
RFモジュールの下はアンテナ放射用のGNDを確保するために銅箔を貼り、その上にカプトンテープを敷設。
GNDはカプトンテープをくり抜いて配線。その他は銅箔、ウレタン線などを使用。

実装した基板

実装した基板

部品購入の際、秋月のC基板用のアクリルパネルが売られているのを見つけたので、基板外形はC基板を活かすことにし、アクリルパネルを被せてルンバに装着。メカメカしさをちょっと低減。

ルンバに取り付け

ルンバに取り付け

ファームウェア

今までPICやらSTM8/32やらいろいろマイコンを触ってきたけど、Arduinoを触るのは実は初めて。いろいろ調べながら実装を行ったが、想像以上に簡単に使えた。普通のマイコンだと躊躇しちゃう浮動小数点演算、文字列処理なんかをうまいこと処理してくれるのは本当に便利。(もちろん速度を考えて一部にしか使わないようにはするけど)

以下、コードをペタり。

2016/8/6 追記:
長くなるのでここにはloop()の中身のみ記載。コード全文はこちら。
http://git.kashiken.net/kashiken/Roombatter/blob/master/Roombatter.ino

void loop() {  
  ArduinoOTA.handle();

  tick = millis();
  if((tick - last) > PERIOD)
  {
    last = tick;
    
    if(Serial.available() != 11)
    {
      outputMsg("Only " + String(Serial.available()) + "bytes received.", MSG_DEBUG_ONLY);
      digitalWrite(PIO_LED, LOW);
    }
    else
    {
      byte rbuffer[16];
      for(int count = 0; count < 11; count++)
      {
        rbuffer[count] = Serial.read();
      }

      int oiMode = rbuffer[0];
      int chargeSource = rbuffer[1];
      int batteryVoltage = (rbuffer[2] << 8) | rbuffer[3];
      int temp = rbuffer[4];
      int dist = (rbuffer[5] << 8) | rbuffer[6];
      if(dist > 32767) dist -= 65536;
      traveled_mm += abs(dist);
      int mCurrent = (rbuffer[7] << 8) | rbuffer[8];
      if(mCurrent > 32767) mCurrent -= 65536;
      int sCurrent = (rbuffer[9] << 8) | rbuffer[10];
      if(sCurrent > 32767) sCurrent -= 65536;

      outputMsg("OIMode: " + String(oiMode), MSG_DEBUG_ONLY);
      outputMsg("ChargeSource: " + String(chargeSource), MSG_DEBUG_ONLY);
      outputMsg("Battery: " + String(batteryVoltage), MSG_DEBUG_ONLY);
      outputMsg("Temperature: " + String(temp), MSG_DEBUG_ONLY);
      outputMsg("Distance: " + String(dist), MSG_DEBUG_ONLY);
      outputMsg("Traveled: " + String(traveled_mm), MSG_DEBUG_ONLY);
      outputMsg("Main Brush Motor Current: " + String(mCurrent), MSG_DEBUG_ONLY);
      outputMsg("Side Brush Motor Current: " + String(sCurrent), MSG_DEBUG_ONLY);
      
      if(stat == STAT_STOP)
      {
        if(mCurrent > MOTOR_CURRENT_TH)  // main brush motor works
        {
          stat = STAT_CLEAN;
          time_start = millis();
          traveled_mm = 0;
          outputMsg("I've started to clean. (Battery: " + String(batteryVoltage) + "mV, Temp: " + String(temp) + "degC) by #Roomba", MSG_ALL);
          digitalWrite(PIO_LED, HIGH);
        }
      }
      else if(stat == STAT_CLEAN)
      {
        if(mCurrent == 0)  // main brush motor works stops
        {
          stat = STAT_STOP;
          float time_elapsed = (millis() - time_start) / 60000.0;  //convert to minute
          float traveled = traveled_mm / 1000.0;  // convert to m
          outputMsg("I finished cleaning in " + String(time_elapsed) + "minutes. The distance traveled is " + String(traveled) + "m. (Battery: " + String(batteryVoltage) + "mV, Temp: " + String(temp) + "degC) by #Roomba", MSG_ALL);
          digitalWrite(PIO_LED, LOW);
        }
      }
    }

    while(Serial.available() != 0) Serial.read();
    byte wbuffer[] = {
      byte(ROI_CMD_START),
      byte(ROI_CMD_QUERY_LIST),
      byte(7),  // number of packets
      byte(ROI_PACKET_OIMODE),
      byte(ROI_PACKET_CHARGE_SOURCE),
      byte(ROI_PACKET_BATTERY),
      byte(ROI_PACKET_TEMP),
      byte(ROI_PACKET_DISTANCE),
      byte(ROI_PACKET_MAIN_BRUSH_MOTOR),
      byte(ROI_PACKET_SIDE_BRUSH_MOTOR)
    };
    Serial.write(wbuffer, 10);
  }
}

余談だが、デバッグ中はシリアルケーブルを接続するため、ペットの散歩のような風景になる。

デバッグ風景

デバッグ風景

完成

そんなこんなでファームウェアも書き上がり、お掃除開始!
これでルンバが掃除の開始と終了を教えてくれるようになった。めでたしめでたし。

ルンバのツイート

ルンバのツイート

ちなみに掃除開始をモーターの電流で見ているせいか、掃除開始時に2回ツイートしちゃう事があったりする。
この辺りは少し稼働させてみてから修正するとしよう。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です