[mbedbot]リモコン機能の追加

October 13, 2016

ボットに機械学習をどう活用するか、なかなか思いつかないので、後回しにしてリモコン機能を追加することにしました。例えば朝、勝手にテレビとか照明をつけて起こしてくれる、といったことをさせたかったからです。

リモコン送信に対応するために、赤外線を発光するためのLED、それを駆動するためのトランジスタ、及び電流調整のための抵抗をつけました。回路図的には下記になります。

上記のV2のところがmbedの出力ピンです。D1が発光ダイオードですが、型番は適当です(実際は、不要なリモコンを分解して発光ダイオードを取り出して使いました)。どのくらいの電流が必要なのかいまいち分からなかったので、抵抗値も適当です。シミュレータ(LTSpice)ではLEDを流れる電流は94mAとなっていましたが、実際はどうだかわかりません。。

受信する方は、送信より簡単で、赤外線受光モジュール(これまた不要な機器から部品取り)とセラミックコンデンサだけで受信できます。赤外線受光モジュールには3つの端子がありますが、それぞれをVcc、GND、mbedの入力ピンにつなげばOKです。コンデンサは、VccとGNDの間につけます。見えにくいですが、下の写真のmbedの右横手前にある黒くて丸っこいのが受光モジュールです。

ちなみに、受信機能は最初は不要かな、と思っていたのですが、リモコン信号を調べるために後でつけることにしました。

SWの方は、RemoteIRというライブラリを使わせていただきました。使い方はいたって簡単なのですが、一点だけハマったことがあります。このライブラリの送信する方のモジュールは、PwmOutを使用しているのですが、HRM1017ではPwmOutは3つまで、という制約があります。すでにモータ駆動で3つ使っていたので、結局モーターを駆動するPwmOutを2つに減らしました(1つは両腕を動かすのに使用)。また、(pwmoutのソースコードを見た所)PwmOutのライブラリではパルスの周期が各PwmOutで共通になっているため、モーターを駆動するときと、リモコンを送信するときとで、周波数をいちいちperiodメソッドで設定し直す必要がありました。それと、これはライブラリとは関係ないのですが、送信してもなかなか受信してくれない状況が続いていたのですが、送信を0.2s間隔で5回くらい繰り返すとかなりの精度で送れるようになりました。

余談ですが、mbedのデバッグをするのにprintfによる出力をmbedをUSBでつないだmacでscreenコマンドなどの端末エミュレータソフトを立ち上げて表示する、という方法がありますが、HRM1017の表側にある2つのソルダジャンパを取っていた(UARTを使うため)ため、出力されませんでした。ソルダジャンパをつけたら、出力されるようになりました。赤外線LEDは光っても目に見えないので動作確認が難しい(一部のスマホのカメラは赤外線LEDの発光を画面に表示させることができる)ですが、printfが使えると非常にデバッグが捗って、助かりました。

受信の方は特にはまらずに実装できました。今回は、受信したリモコン信号をスマホアプリに表示するようにしました。そのため、リモコンデータをBLEで送受信するために新たにキャラクタリスティックを追加しました。mbedのソースは下記ですが、IrTransCharというのが追加したキャラクタリスティックです。送信と、通知に対応するように設定しています。

#include "mbed.h"
#include "BLE.h"
#include "AT3011/AT3011.h"
#include "Servo.h"
#include "RoboController.h"
#include "TransmitterIR.h"
#include "ReceiverIR.h"

BLEDevice  ble;
DigitalOut sw(P0_29);
AT3011 atp(TX_PIN_NUMBER, RX_PIN_NUMBER, P0_30);
Servo myservo1(P0_7); // 頭
Servo myservo2(P0_28); // 両腕
TransmitterIR ir_tx(P0_23);
ReceiverIR ir_rx(P0_21);

/* RoboController Service */
static const uint16_t RoboController_service_uuid = 0xFFF0;
static const uint16_t Text_Characteristic_uuid = 0xFFF1;
static const uint16_t Motion_Characteristic_uuid = 0xFFF2;
static const uint16_t Command_Characteristic_uuid = 0xFFF3;
static const uint16_t IrTrans_Characteristic_uuid = 0xFFF4;
static const uint16_t uuid16_list[]        = { RoboController_service_uuid };
uint8_t TextCharPayload[10] = {0,};
uint8_t MotionCharPayload[10] = {0,};
uint8_t CommandCharPayload[10] = {0,};
uint8_t IrTransCharPayload[10] = {0,};

GattCharacteristic  TextChar (Text_Characteristic_uuid,TextCharPayload,10, 10,
                                GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_WRITE);
GattCharacteristic  MotionChar (Motion_Characteristic_uuid,MotionCharPayload,10, 10,
                                GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_WRITE);                                
GattCharacteristic  CommandChar (Command_Characteristic_uuid,CommandCharPayload,10, 10,
                                GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_WRITE | 
                                GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_WRITE_WITHOUT_RESPONSE);
GattCharacteristic  IrTransChar (IrTrans_Characteristic_uuid,IrTransCharPayload,10, 10,
                                GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_WRITE |
                                GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_NOTIFY);                            
GattCharacteristic *Chars[] = {&TextChar, &MotionChar, &CommandChar, &IrTransChar};
GattService         RoboControllerService(RoboController_service_uuid, Chars, sizeof(Chars) / sizeof(GattCharacteristic *));

PropertyBuf property_buf; // 受信したプロパティを格納するバッファ

/* IR Transmitter */
void ir_wait_idle()
{
    while(ir_tx.getState() != TransmitterIR::Idle) {
        wait_ms(500);
    }
}
void ir_send()
{
    if(property_buf.ir_send_flag) {
        ir_tx.awake();
        RemoteIR::Format format;
        switch(property_buf.ir_data.ir_data.ir_type) {
        case 0:
            format = RemoteIR::NEC;
            break;
        case 1:
            format = RemoteIR::AEHA;
            break;
        case 2:
            format = RemoteIR::SONY;
            break;
        default:
            format = RemoteIR::NEC;
            break;
        }
        for(int i = 0; i < 5; i++) {
            ir_wait_idle();
            ir_tx.setData(format, property_buf.ir_data.ir_data.data, property_buf.ir_data.ir_data.bitcount);
            ir_wait_idle();
            wait(0.2);
        }
        ir_tx.sleep();
    }
}

/* IR Receiver */
void notify_irdata_sub(int format_id, uint8_t *buf, int bitlength) {
    const int n = bitlength / 8 + (((bitlength % 8) != 0) ? 1 : 0);
    IrTransCharPayload[0] = format_id;
    IrTransCharPayload[1] = bitlength;
    for (int i = 0; i < n; i++) {
        if(i>=8) break;
        IrTransCharPayload[2+i] = buf[i];
    }
    ble.updateCharacteristicValue(IrTransChar.getValueAttribute().getHandle(),
                                    (uint8_t *)&IrTransCharPayload, sizeof(IrTransCharPayload));
}

void notify_irdata() {
    RemoteIR::Format format;
    uint8_t buf[32];
    int bitcount;
    int format_id;
    while(1) {
        if (ir_rx.getState() == ReceiverIR::Received) {
            bitcount = ir_rx.getData(&format, buf, sizeof(buf) * 8);
            switch (format) {
            case RemoteIR::UNKNOWN:
                format_id = 5;
                break;
            case RemoteIR::NEC:
                format_id = 0;
                break;
            case RemoteIR::NEC_REPEAT:
                format_id = 3;
                break;
            case RemoteIR::AEHA:
                format_id = 1;
                break;
            case RemoteIR::AEHA_REPEAT:
                format_id = 4;
                break;
            case RemoteIR::SONY:
                format_id = 2;
                break;
            }
            notify_irdata_sub(format_id, buf, bitcount);
            break;
        }
        else {
            wait(1);
        }
    }
}

/* servo */
void servos_sleep() {
    myservo1.sleep();
    myservo2.sleep();
    wait(1);
}

void servos_awake() {
    myservo1.awake();
    myservo2.awake();
    wait(1);
}

void move()
{
    if(property_buf.motion_flag) {
        servos_awake();
        for(int i=0; i < 10; i++ ){
         switch(property_buf.motion.property[i].action) {
             case 0: // Sleep
                 wait(property_buf.motion.property[i].parm / 10.0);
                 break;
             case 1: // 頭回転
                 myservo1 = property_buf.motion.property[i].parm / 64.0;
                 break;
             case 2: // 両腕回転
                 myservo2 = property_buf.motion.property[i].parm / 64.0;
                 break;
         }
        }
        wait(1);
        servos_sleep();
    }
}

/* AquesTalk */
void speak()
{
    if(property_buf.text_num > 0) {
        wait(1);
        char * msg = (char *)malloc(sizeof(TextProperty) * property_buf.text_num + 1);
        for(int i=0;i < property_buf.text_num; i++) {
            memcpy(msg + sizeof(TextProperty) * i, &property_buf.text[i].data, sizeof(TextProperty));
        }
        msg[sizeof(TextProperty) * property_buf.text_num] = '\0';
        atp.speak(msg);
        free(msg);
    }
}

void speak_preset_recvir(){
    wait(1);
    atp.speak("rimokonnno/botanno/o_shite'ne.");
}

/* Power supply */
void switchOn()
{
    sw = 1;
}

void switchOff()
{
    sw = 0;
}

/* Robot Action */

/* 通常アクション */
void do_action()
{
    switchOn();
    move();
    speak();
    ir_send();
    switchOff();
}

/* リモコン信号取得アクション */
void ir_rcv_action()
{
    switchOn();
    speak_preset_recvir();
    notify_irdata();
    switchOff();
}

/* BLE */
void reset_property_buf()
{
    property_buf.motion_flag = 0;
    property_buf.text_num = 0;
    property_buf.ir_send_flag = 0;
}

void onConnectionCallback(const Gap::ConnectionCallbackParams_t *params)   //Mod
{
    
}

void disconnectionCallback(const Gap::DisconnectionCallbackParams_t *params)    // Mod
{
    ble.startAdvertising();
}

// GattEvent
void DataWrittenCallback(const GattWriteCallbackParams *params)
{
    uint16_t bytesRead;
    if(params->handle == TextChar.getValueAttribute().getHandle()) {
        if(property_buf.text_num < sizeof(property_buf.text) / sizeof(TextProperty)) {
            ble.readCharacteristicValue(params->handle, property_buf.text[property_buf.text_num].data, &bytesRead);
            property_buf.text_num++;
        }
    }
    else if(params->handle == MotionChar.getValueAttribute().getHandle()) {
        ble.readCharacteristicValue(params->handle,  property_buf.motion.data, &bytesRead);
        property_buf.motion_flag = 1;
    }
    else if(params->handle == IrTransChar.getValueAttribute().getHandle()) {
        ble.readCharacteristicValue(params->handle, property_buf.ir_data.data, &bytesRead);
        property_buf.ir_send_flag = 1;
    }
    else if(params->handle == CommandChar.getValueAttribute().getHandle()) {    
        ble.readCharacteristicValue(params->handle, property_buf.command.data, &bytesRead);
        switch(property_buf.command.property.cmd) {
        case 1: // 通常のアクション
            do_action();
            reset_property_buf();
            break;
        case 2: // リモコン信号取得アクション
            ir_rcv_action();
            break;
        }
        
    }
}

/* initialize */
void init_robot_devices()
{
    switchOn();
    atp.reset();
    servos_sleep();
    ir_tx.sleep();
    switchOff();
    wait(1);
}

void init_ble()
{
    reset_property_buf();
    ble.init(); 
    ble.onDisconnection(disconnectionCallback);
    ble.onConnection(onConnectionCallback);
    ble.onDataWritten(DataWrittenCallback);
    
    /* setup advertising */
    ble.accumulateAdvertisingPayload(GapAdvertisingData::BREDR_NOT_SUPPORTED);
    ble.setAdvertisingType(GapAdvertisingParams::ADV_CONNECTABLE_UNDIRECTED);
    ble.accumulateAdvertisingPayload(GapAdvertisingData::SHORTENED_LOCAL_NAME,
                                    (const uint8_t *)"mbed HRM1017", sizeof("mbed HRM1017") - 1);
    ble.accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LIST_16BIT_SERVICE_IDS,
                                    (uint8_t *)uuid16_list, sizeof(uuid16_list));

    ble.setAdvertisingInterval(160); /* 100ms; in multiples of 0.625ms. */
    ble.startAdvertising();

    ble.addService(RoboControllerService);
}

void init()
{
    init_robot_devices();
    init_ble();
}    

int main(void)
{
    init();

    while (true) {
        ble.waitForEvent();
    }
}

mbed側のHW、SWの実装は以上で完成です。