TWE-liteで簡単リモートなんとか~

  • 11
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

この記事は、第2のドワンゴ Advent Calendar 2015 12日目の記事です。

はじめに。最近の工作とTWE-lite

ここ半年ぐらい遊んでるTWE-liteを使うとたのしい、というお話をします。
元々はPCから制御できる水上ラジコンを作ろう、という同僚との共同工作あそびのためにTWE-liteチップをいじり始めたのですが、いろいろと知見が溜まってきたのでこれからTWE-liteを初めて見たい人向けにまとめよう、というわけです。

ラジコン艦じたばた丸

開発中の、半潜水型のラジコン艦「じたばた丸」。中央基板上にいるTWE-liteを中核にして、タミヤ水中モーター2軸を主機とする船です。船体は3Dプリント。
軍艦モデルを上に乗せ、ギリギリ浮くように浮力を調整すると…

航行風景

浅い角度や逆光では水面下の潜水部はほとんど見えなくなり、模型だけがスーッと走ってるように見える。という寸法です。(この写真でもよーく見ると模型の舳先下に、薄っすらとラジコン艦のシルエットが見えます)

TWE-liteとは?

TWE-liteはTOCOSが製造販売している、マイコン付き無線チップです。表面実装版もありますが、工作するにはDIP版がいいでしょう。秋月電子等で扱っており、半完成やアンテナバリエーションありますがだいたい1つ1600-2000円弱で買えます。

TWE-liteはマイコン付き無線チップの名の通り、マイコンのアプリを書き換えて多用途に使えるのがウリの一つですが、電子工作初心者にとっては逆にハードルを上げている印象を受けます。アプリを開発しなければいけないのか、チップの特性の一つなのか、ライターは使うのに必要なのかetc...。公式サイトが入り組んでいるのも、ちょっと分かりにくさを助長してしまっているかなと思いました。
というわけで、今回のこの記事はTWE-lite公式ページへのリンク集になってくれるといいかな、という目論見もあります。

他の特徴としては電源を3Vとしていること、消費電力に優れる点があります。
3V電源は電子工作する際、特にUSB等を電源として使うときにはちょっと面倒ですが、最悪百均で売っているボタン電池CR2032でも動きます(ボタン電池の規格より要求する電力は多いのですが、たいてい動くようです。公式ではボタン電池で動かす場合は間欠駆動をするための蓄電回路を繋ぐことが推奨されています。公式のセット商品 )
消費電力は公式ページによるとCR2032を電源として標準動作で半年以上、間欠駆動では年単位の稼働が可能だそうです

超簡単!TWE標準アプリ

最初に触るであろう、超簡単!TWE標準アプリがなかなか高機能で、これを使うだけでラジコンやリモートセンサ、省電力リモコンを作ることができます。

TWE-liteチップはデジタル入力/出力を4ピンずつ、アナログ入力/PWM出力を4ピンずつ、シリアル入出力を1個持っています。設定で親機子機になり、親子間の無線通信によって、ピン同士の状態を同期します。つまり、親機でデジタル入力をいじれば、子機のデジタル出力が変更できる、もうこれだけラジコンができてしまっていのです。まさにラジコンを作るためにあるかのような仕様ですね。なお子機側のピン入力も同様に親側へ通知され、これだけで相互通信ができます。公式のアプリ説明

TWE-lite同士の通信イメージ図

試すのに必要なミニマムで必要なパーツは、

  • TWE-liteを2つ(送信側と受信側)
  • 電源3V
  • Arduinoで使うようなLEDや抵抗などのetc…

でよいです。公式の初級編記事を参考するとよいでしょう。単純なラジコンリモコンならこの知識だけでイケるでしょう。

PCからTWE-liteをいじりたい

と思うでしょう?思いますよね?ということでここから本題です。
それに応える製品がToCoSTICKです。TWE-liteチップとUSB-シリアル通信モジュールをくっつけたような製品で、秋月電子では3000円程度で販売されています。

ToCoStickでTWE-liteと通信する場合は、シリアル通信としてピンの状態を書き込むと、それが子機へ送り出されます。同様に子機の状態変化もToCoStick側のシリアル通信で受け取ることができます。実際のコマンドはTWE-lite DIP上級編に纏められていて、送信コマンドは相手端末の出力を変更:0x80、受信コマンドは相手端末からの状態通知:0x81を見るとよいでしょう

TWE-liteとToCoStickの通信イメージ

nodejsからToCoStickを経由して、TWE-liteを制御する

では実際にシリアル通信をしてみましょう。ここではnodejsのserialportライブラリを使います。

var serialport = require('serialport');
var sp = new serialport.SerialPort(portName, {
    baudRate: 115200,
    dataBits: 8,
    parity: 'none',
    stopBits: 1,
    flowControl: false,
    parser: serialport.parsers.readline("\n")
});

sp.write(":7880010F0F0380030002800200DF");

とこんな感じで書き込むことができます。portNameはUnix/Linux系であれば"/dev/tty-usbserial1"、Windowsであれば"COM3"などの文字列で指定します。最後の文字列こと、相手端末の状態変更は以下のようなユーティリティ関数作っておいて通すようにしています。

var TweliteSendPacket = function() {
    this.toDeviceId = 0x78; // default: target
    this.digialOut = 0x00;
    this.digialOutChanged = 0x00;
    this.pwm = [ 0xFFFF,0xFFFF,0xFFFF,0xFFFF ];

    this.toEncoded = function() {
        var buf = ":";
        buf += ("00" + this.toDeviceId.toString(16)).slice(-2);
        buf += '80';    // command fixed
        buf += '01';    // protcol fixed
        buf += ("00" + this.digialOut.toString(16)).slice(-2);
        buf += ("00" + this.digialOutChanged.toString(16)).slice(-2);
        buf += ("0000" + this.pwm[0].toString(16)).slice(-4);
        buf += ("0000" + this.pwm[1].toString(16)).slice(-4);
        buf += ("0000" + this.pwm[2].toString(16)).slice(-4);
        buf += ("0000" + this.pwm[3].toString(16)).slice(-4);
        buf += "X";     // TODO:replace to valid checksum
        return buf.toUpperCase();
    };

    /** 
     * index : 0-3
     * value : boolean
     */ 
    this.setDigitalOut = function(index, value) {
        if( value == true ) {
            this.digialOut |= 1 << index;
            this.digialOutChanged |= 1 << index;
        } else {
            this.digialOutChanged |= 1 << index;
        }
    };

    /**
     * index : 0 - 3
     * value : 0 - 1024
     */
    this.setPWM = function(index, value) {
        this.pwm[index] = value;
    }
}

引っかかりやすい罠としては、

  • これは16進数のバイナリではなくて、コードを見ると分かるようにテキストデータ
  • 頭のコロンは要る
  • 16進数のABCDEFは大文字

あたりでしょうか。今回ここではチェックサムを省略していますが、将来的にはちゃんとチェックサムを付けたいところです

TWE-liteの状態をnodejsで受け取る

送信同様にシリアル通信の形で飛んでくるため、それを受け取るだけです。なお子機は初期設定では、毎秒状態を通知してきます

var serialport = require('serialport');
var sp = new serialport.SerialPort(portName, {
    baudRate: 115200,
    dataBits: 8,
    parity: 'none',
    stopBits: 1,
    flowControl: false,
    parser: serialport.parsers.readline("\n")
});
sp.on('data', function(input) {
  var buffer = new Buffer(input, 'utf8');
  try {
    console.log("received:" + buffer);
  } catch(e) {
    console.log("error:"+e);
    return;
  }    
});

送られてくるコマンド、相手端末の状態通知をデコードするコードはこんな感じになりました

var TweliteReceievedPacket = function(buffer) {
    this._rawBuffer = buffer;
    this.deviceId = parseInt(buffer.slice(1,3).toString(), 16);
    this.datatype = buffer.slice(3,5).toString();   // fixed 0x81
    this.packetId = buffer.slice(5,7).toString();
    this.protocol = buffer.slice(7,9).toString();
    this.signal = parseInt(buffer.slice(9,11).toString(), 16);
    this.terminalId = parseInt(buffer.slice(11,19).toString(), 16);
    this.toId = parseInt(buffer.slice(19,21).toString(), 16);
    this.timestamp = parseInt(buffer.slice(21,25).toString(), 16);
    this.repeater_flag = parseInt(buffer.slice(25,27).toString(), 16);
    this.battery = parseInt(buffer.slice(27,31).toString(), 16);

    var rawDigitalIn = parseInt(buffer.slice(33,35).toString(), 16);
    this.digialIn = [
        (rawDigitalIn >> 0 & 1) ? true : false,
        (rawDigitalIn >> 1 & 1) ? true : false,
        (rawDigitalIn >> 2 & 1) ? true : false,
        (rawDigitalIn >> 3 & 1) ? true : false,
    ];

    var rawDigitalChanged = parseInt(buffer.slice(35,37).toString(), 16);
    this.digialChanged = [
        (rawDigitalChanged >> 0 & 1) ? true : false,
        (rawDigitalChanged >> 1 & 1) ? true : false,
        (rawDigitalChanged >> 2 & 1) ? true : false,
        (rawDigitalChanged >> 3 & 1) ? true : false, 
    ]
    this.analogIn = [
        parseInt(buffer.slice(37,39).toString(), 16),
        parseInt(buffer.slice(39,41).toString(), 16),
        parseInt(buffer.slice(41,44).toString(), 16),
        parseInt(buffer.slice(43,45).toString(), 16),
    ]
    this.analogOffset = parseInt(buffer.slice(45,47).toString(), 16);
    this.checksum = parseInt(buffer.slice(47,49).toString(), 16);
};

動作テスト
お風呂試運転の図。手前のSurfaceProに刺さっているToCoStickから、ラジコン艦を動かしています

通信Tips

と、だいたい通信できるようになりました。他にやってる間に気付いたこと、知っておくと役立つネタを上げておきます。

相手端末の状態通知から、電波強度と電源電圧が乗っている

相手端末の状態通知を見ると分かるのですが、この2つのステータスが分かります。特に電波強度はどうやっても測りづらいものですから、通信チェックには役立ちます。TWE-liteは中継器を作ることも可能ですから、対処可能かが分かるでしょう

電源電圧はあくまでTWE-liteチップに供給されているものなので、例えばレギュレータIC等を経由して降圧している場合はその後の値にはなってしまいますが取れます。TWE-liteの動作電圧は2.0Vですから、これを監視すれば最低限バッテリー切れで通信途絶する前に、バッテリー消耗を親機側から検知できると思います
なお、降圧前の電源電圧を取りたい場合はアナログ入力を1つ使えば代用できるでしょう

TWE-liteの通信はUDP

TWE-lite/ToCoStick同士の通信はUDPのようです。子機からの受信に失敗しているタイミングでも、親機から子機への送信が成功するときが時々ありました。

通信途絶時は最後の状態を維持する

ラジコンという用途には厳しい仕様。動いてる状態で電波が途絶すると、暴走してしまうという…
が、これにも対応方法が用意されてました。設定(インタラクティブ)モードの説明最後、オプションビット・押しボタン中のみ送信を行う の設定項目です。これを親機子機に設定することで、通信途絶時は停止する動作が実装できるようですが、いかんせん回路を組んでしまって動作試験をしている時に見つけたオプションだったため、回路が合わず検証していません。PWM出力の状態が途絶時にどうなるか書かれていないのも気がかりです。
この設定をするには、TWE-lite-Rが必要でした。ここでやっと登場

子機ID設定

親機に複数の子機をぶら下げた運用をするときには、子機にIDを設定します。設定にはTWE-lite-Rが必要。

まとめ・TWE-liteは簡単にPC連携ラジコンリモコンが作れる

TWE-liteチップでラジコンを作る過程で、チップの特性や便利機能が分かってきました。そこまで高くないお値段で、省電力な無線デバイスが作れ、無線ネットワークが作れるのは面白いですね。

これをどう使うかがまた難しいところですが、例えばおうちIoT。各種ドアや窓、ライト等にセンサーやリモートスイッチを組み込むのは比較的楽ですが、如何せんセンサーやスイッチを仕込む度にACアダプタを生やしたり、PC等とそのスイッチを繋ぐケーブルを引くのは大変です。それなら、電池駆動のTWE-liteをセンサーやスイッチ側に仕込み、PCにはToCoStickを指して無線ネットワーク化してしまう、といったことはサックリできそうですね。

この投稿は 第2のドワンゴ Advent Calendar 201512日目の記事です。