2018年6月17日日曜日

Alexa と Raspberry Piでルンバに掃除してもらう:(6)Arduinoから赤外線でルンバを動かす

前回、ROIを使ってルンバをシリアル経由で操作する方法を紹介しました。
ROIを使うとかなり細かいコントロールができる一方、シリアル接続なのでルンバにArduinoを載せる必要があります。
頑張ってルンバから電源を取りつつArduinoをルンバに内蔵することも可能ではありますが。。。
今回やりたいことは単なる電源ON/OFFだけなので、ルンバとは有線接続せずに赤外線で済ませたいところです。

lircでやろうとしたところ、リモコンコードを読み取れずエラーになってしまいました。
テレビと同様に38Hzのようですが、信号が長すぎるのかもしれません。

そこで、Arduinoで赤外線のリモコンコードを読み取って赤外線LEDでルンバに送ります。
やっていることはRaspberry Pi+licdと同じですが、Arduinoなのでもっと泥臭い感じになります。
  1. 材料を揃える。
  2. 母艦のRaspberry PiにNode-REDをセットアップしてAmazon Echo Dotに認識させる
  3. Raspberry Pi Zero WをヘッドレスでWiFi対応セットアップする
  4. Raspberry Piで赤外線リモコンのコードをコピーする
  5. Node-REDから赤外線機能を呼び出してテレビを操作する
  6. FlashAirのGPIO機能でArduinoをHTTP API経由で制御する
  7. (おまけ)ArduinoからROI経由でルンバを動かす
  8. Arduinoから赤外線経由でルンバを動かす【今回】
  9. Node-REDからFlashAir/Arduino経由でルンバを動かす

回路を組む

Arduinoに赤外線受信モジュールと赤外線送信モジュールを繋ぎます。
受信モジュールは赤外線リモコンを覚えさせる際に使用するだけなので、覚えた後は外しても構いません。


リモコンコードをArduinoのEEPROMに書き込む

赤外線受信モジュールからの入力信号がHIGHとLOWの変化する時間間隔を計測します。
計測結果を配列に詰めてEEPROMに書き込めばOK。
このコードでは2種類のリモコンコードを覚えられるようになっています。
#include <EEPROM.h>

#define PIN_IR_SENSOR 11

#define SCAN_INITIAL_TIMEOUT 10000000
#define SCAN_TIMEOUT 10000000
#define IR_BUF_LEN 64

volatile int IR_BUF[IR_BUF_LEN];
volatile bool SETUP_FLAG = false;

void setup() {
  Serial.begin(9600);
  SETUP_FLAG = true;
  pinMode(PIN_IR_SENSOR, INPUT);
  updateIR(0);
  updateIR(1);
}

int updateIR(int pos) {
  Serial.println("SCAN START");
  memset(IR_BUF,0,IR_BUF_LEN);
  for( int i=0; i<3; i++ ){
    Serial.print("PUSH BUTTON ");
    Serial.println(pos);
    int result = scanIR(i);
    for(int j=0; j<result; j++){
      Serial.print(IR_BUF[j]);
      Serial.print(" ");
    }
    Serial.print("\n");
    Serial.print(result);
    Serial.println(" bytes were read");
    delay(3000);
  }
  saveBuffer(pos * IR_BUF_LEN*2);
  Serial.println("SCAN END");
  return 1;
}


int scanIR(int repeat) {
  int idx=0;
  int ir_status = HIGH;  
  unsigned long lastStatusChanged = 0;
  unsigned long timeout = SCAN_INITIAL_TIMEOUT;
  while(1){
    if ( ir_status == LOW ){
      while( digitalRead(PIN_IR_SENSOR) == LOW){
        // wait
        ;
      }
    } else {
      if( wait_high_signal( micros() + timeout ) < 0 ){
        break;
      }
    }
  
    unsigned long now = micros();
    if( lastStatusChanged > 0 ){
      int val = (int)((now - lastStatusChanged) / 10);
      if( repeat > 0 ){
        if( IR_BUF[idx] > 1 ){ break; }
        if( abs((int)(IR_BUF[idx] - (int)val)) >  IR_BUF[idx]*0.3 ){ idx = 0; break; }
        IR_BUF[idx] = (int)( IR_BUF[idx] * repeat + val ) / (repeat + 1);
      } else {
        IR_BUF[idx] = val;
      }
      idx++;
      if( idx == IR_BUF_LEN -1 ){ break; }
    }
    lastStatusChanged = now;
    if (ir_status == HIGH) {
      ir_status = LOW;
    } else {
      ir_status = HIGH;
    }
    timeout = SCAN_TIMEOUT;
  } // while 1
  
  if( idx > 0 ){ IR_BUF[idx] = -1; }
  return idx;
}

int wait_high_signal(unsigned long timeout) {
  while( digitalRead(PIN_IR_SENSOR) == HIGH ){
    if( micros() > timeout ) { return -1; }
  }
  return 1;
}

void saveBuffer(int pos) {
  Serial.println("SAVE START");
  byte buf;
  for( int i=0; i < IR_BUF_LEN; i++ ){
    buf = lowByte(IR_BUF[i]);
    EEPROM.write( pos+i*2, buf );
    delay(5);
    buf = highByte(IR_BUF[i]);
    EEPROM.write( pos+i*2+1, buf );
    delay(5);
    if( buf < 0 ){ break; }
  }
  Serial.println("SAVE END");
}


EEPROMから読み込んだコードで赤外線送信する

今度はEEPROMから配列を読み込んで、覚えていた時間間隔でLEDを点滅させます。
我が家のRoombaが具合悪いだけかもしれませんが、
1回送信しただけでは正常に反応しないことが多いので、3回連続で送信しています。
また、loop関数で繰り返し送信されてしまうので送信し終わったらスリープさせます。
#include <EEPROM.h>

#define PIN_IR 10

#define IR_BUF_LEN 64

volatile int IR_BUF[IR_BUF_LEN];

void setup() {
  Serial.begin(9600);
  pinMode(PIN_IR, OUTPUT);
  Serial.println("Start!");
}

void loop() {
  Serial.println("Run");
  loadBuffer(0);
  executeIR();
  executeIR();
  executeIR();
  check_sleep(true);
}

void wakeup(){
  Serial.println("Wakeup");
}
void check_sleep(bool flag) {
  if ( ! flag ) { return; }
  Serial.println("Sleep");
  delay(1000);
  attachInterrupt(0, wakeup, CHANGE);
  set_sleep_mode(SLEEP_MODE_STANDBY);
  sleep_enable();
  sleep_mode();
  sleep_disable();
}

void loadBuffer(int pos) {
  Serial.println("LOAD START");
  int h, l;
  for( int i=0; i<IR_BUF_LEN; i++ ){
    l = EEPROM.read(pos+i*2);
    h = EEPROM.read(pos+i*2+1);
    IR_BUF[i] = (h << 8) + l;
    
    Serial.print(IR_BUF[i]);
    Serial.print(" ");
    if( IR_BUF[i] < 0 ){ break; }
  }
  Serial.println("\nLOAD END");
}

void executeIR() {
  for(int i=0; IR_BUF[i] > 0; i++){
    unsigned long len = (unsigned long)IR_BUF[i] * 10;
    unsigned long now = micros();
    
    do {
      digitalWrite(PIN_IR, 1-i&1);
      delayMicroseconds(8);
      digitalWrite(PIN_IR, 0);
      delayMicroseconds(7);
    } while( now + len > micros() );
  }
}


ここまででArduinoからRoombaに掃除をスタートするコマンドを送れるようになりました。
ところが、このままではArduinoの電源を入れた直後にしか信号が飛びません。
次回、Node-REDからのリクエストをFlashAirで受けて、Arduinoをスリープから復帰させるように改造します。

0 件のコメント:

コメントを投稿