Sunday, June 17, 2018

Commanding a Roomba with Alexa and Raspberry Pi (6): Controlling Roomba from Arduino via IR

Last time we covered controlling the Roomba via ROI over serial.
ROI allows fine-grained control, but since it's a wired serial connection, the Arduino has to ride on the Roomba itself.
Powering the Arduino from the Roomba and embedding it is possible, but it's a lot of work.
Since all we need is simple power ON/OFF, connecting via IR without any wired connection to the Roomba is the cleaner approach.

When I tried capturing the Roomba's remote codes with lirc, it failed with errors.
The carrier seems to be 38 kHz like a TV remote, but the signal may simply be too long.

So the plan is: use Arduino to capture the IR remote codes, then replay them to the Roomba with an IR LED.
It's the same idea as Raspberry Pi + lirc, but in Arduino style — which means a bit more hands-on.
  1. Gather parts.
  2. Set up Node-RED on the main Raspberry Pi and register it with Amazon Echo Dot
  3. Headless WiFi setup for Raspberry Pi Zero W
  4. Copy IR remote codes with Raspberry Pi
  5. Call IR functions from Node-RED to control the TV
  6. Control Arduino via HTTP API using FlashAir GPIO mode
  7. (Bonus) Control Roomba from Arduino via ROI serial interface
  8. Control Roomba from Arduino via IR [This article]
  9. Control Roomba from Node-RED via FlashAir/Arduino

Building the Circuit

Connect an IR receiver module and an IR transmitter module to the Arduino.
The receiver is only needed while recording the remote codes — it can be removed afterward.


Writing Remote Codes to Arduino EEPROM

The idea is to measure the timing of HIGH/LOW transitions on the IR receiver's signal pin and store that array in EEPROM.
This sketch can record two different remote codes.

#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");
}

Replaying the Stored Code via IR Transmission

This sketch reads the timing array from EEPROM and pulses the IR LED at those intervals.
My Roomba often doesn't respond to a single transmission (might be specific to my unit),
so the code sends the signal three times in a row.
Since the loop function would keep sending forever, the Arduino enters sleep mode after finishing.

#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() );
  }
}


At this point, Arduino can send the "start cleaning" command to the Roomba via IR.
The issue is that in the current form, the signal only fires once — right when the Arduino powers on.
Next time: modify the setup so Arduino wakes from sleep when it receives a request from Node-RED via FlashAir.

No comments:

Post a Comment