Sysexコマンドを実装する為にArduino IDE 1.0.3内のコードを色々読んだ。
日本語どころか英語でもちゃんと解説している文書が見当たらなかったので、忘れる前にメモしておく。
Firmata
Arduinoを操り人形モードにしてPCから指令だして逐次動作させるプロトコルのこと。Midiを元にしている。
http://firmata.org/wiki/V2.1ProtocolDetails
使う分には、だいたいRPCのような物だと思っていて良い。
パソコンとArduinoを連動させるプログラムを作る時、両側のプログラムを書いてシリアル通信で連動させるのは面倒なので、パソコン側プログラムの中にArduinoのプログラムを埋め込めるようにしよう!!ということ。
Arduino本体のコード読むと、Atmel AVRマイコン専用ではなく他社のマイコンでも動かせるように物凄い実装が抽象化されているカッコいいコードな事がわかる。
実際基本的な命令セットのラッパー部分を書き換えれば
MSP430 LaunchPadもArduino化する。
Firmataも同じように、Arduino専用プロトコルではない。例えば
GainerやFunnelでも使っている。
Arduinoを呼び出す側も色々な言語での実装がある。俺が作った物では
がある。
Sysex Message
PC-Firmata間で2byte以上のデータをやりとりする時に使う。
Arduino IDE付属のStandardFirmataでは、Arduino→PCへのDIGITAL_MESSAGE, ANALOG_MESSAGE, REPORT_VERSIONの3つで既に使われている。
自作のコマンドをArduinoに実行させるのにも使える。
例えばサーボモーターを角度0〜180まで3秒間かけて動かす処理をする時、普通にFirmata上のservoWrite(サーボ番号, 角度)関数を使うとsleepを駆使して小刻みにPCからArduinoにコマンドを送り続ける。
「n秒かけて角度aからbまで回す」というSysexコマンドを定義すれば、命令は1回で済む。
あとは赤外線リモコンのような38KHzをきっちり送るような処理にも使える。LEDのon/off指令をPCから逐次送信していては間に合わないので、sysexコマンドを自作する。(
FlashとArduinoで赤外線リモコン)
[START_SYSEX, コマンド名, 7bitを31個分のデータ, END_SYSEX] を1セットとして送信する。
8bit目が立っているbyteはMidiではコマンドなので、7bitになっている。
http://firmata.org/wiki/V2.1ProtocolDetails#Sysex_Message_Format
/* Generic Sysex Message
* 0 START_SYSEX (0xF0)
* 1 sysex command (0x00-0x7F)
* x between 0 and MAX_DATA_BYTES 7-bit bytes of arbitrary data
* last END_SYSEX (0xF7)
*/
MAX_DATA_BYTESは Arduino.app/Contents/Resources/Java/libraries/Firmata/Firmata.h で定義されている。
デフォルトでコマンド1つ + 7bitデータ31個の合計32個が送れる。
#define MAX_DATA_BYTES 32 // max number of data bytes in non-Sysex messages
コメントにsysex以外のメッセージって書いてあるけど、Firmata.cppのコードを追っていくとsysexのデータを格納する配列サイズはMAX_DATA_BYTESで指定されている。
Firmata.hで定義されているSysexコマンド。0x00-0x0Fはユーザが自分のアプリ内で定義していいらしい。
// extended command set using sysex (0-127/0x00-0x7F)
/* 0x00-0x0F reserved for user-defined commands */
#define SERVO_CONFIG 0x70 // set max angle, minPulse, maxPulse, freq
#define STRING_DATA 0x71 // a string message with 14-bits per char
#define SHIFT_DATA 0x75 // a bitstream to/from a shift register
#define I2C_REQUEST 0x76 // send an I2C read/write request
#define I2C_REPLY 0x77 // a reply to an I2C read request
#define I2C_CONFIG 0x78 // config I2C settings such as delay times and power pins
#define EXTENDED_ANALOG 0x6F // analog write (PWM, Servo, etc) to any pin
#define PIN_STATE_QUERY 0x6D // ask for a pin's current mode and value
#define PIN_STATE_RESPONSE 0x6E // reply with pin's current mode and value
#define CAPABILITY_QUERY 0x6B // ask for supported modes and resolution of all pins
#define CAPABILITY_RESPONSE 0x6C // reply with supported modes and resolution
#define ANALOG_MAPPING_QUERY 0x69 // ask for mapping of analog to pin numbers
#define ANALOG_MAPPING_RESPONSE 0x6A // reply with mapping info
#define REPORT_FIRMWARE 0x79 // report name and version of the firmware
#define SAMPLING_INTERVAL 0x7A // set the poll rate of the main loop
#define SYSEX_NON_REALTIME 0x7E // MIDI Reserved for non-realtime messages
#define SYSEX_REALTIME 0x7F // MIDI Reserved for realtime messages
// these are DEPRECATED to make the naming more consistent
#define FIRMATA_STRING 0x71 // same as STRING_DATA
#define SYSEX_I2C_REQUEST 0x76 // same as I2C_REQUEST
#define SYSEX_I2C_REPLY 0x77 // same as I2C_REPLY
#define SYSEX_SAMPLING_INTERVAL 0x7A // same as SAMPLING_INTERVAL
自作SysexコマンドをArduino側に登録する
登録すると、PC側からSysexコマンド発行すればArduino上で実行できる。
Arduino.app/Contents/Resources/Java/libraries/Firmata/Firmata.cpp にあるattach関数にsysex command (0x00-0x7F)を引数として、コールバックを登録する。
http://firmata.org/wiki/V2.1ProtocolDetails#Sysex_Message_Format を見ると、0x00~0x0Fはユーザが自由に使って良い様に予約されているんだけど、登録しても現在Arduino IDEに付属しているFirmata.cpp内では呼び出されるように実装されてない。
方法は2つで、
1. SysexのSTRING_DATAコマンドが来た時のイベントを登録する
2. Sysexを受信した時の関数を呼び出すように、自分でswitch文に追加する
のどちらか。
1. STRING_DATA (0x71)コマンドが来た時のイベントを登録
STRING_DATAは自作アプリで使える。
STRING_DATA (0x71)はFirmata.cpp内でコールバックとして登録できるようになっているが、Arduino IDE付属のStandardFirmata.inoからは使われていない。
Arduino IDEに入っているスケッチ例のEchoStringで使ってる。
#include <Firmata.h>
byte analogPin;
void stringCallback(char *myString)
{
Firmata.sendString(myString);
}
void sysexCallback(byte command, byte argc, byte*argv)
{
Firmata.sendSysex(command, argc, argv);
}
void setup()
{
Firmata.setFirmwareVersion(0, 1);
Firmata.attach(STRING_DATA, stringCallback);
Firmata.attach(START_SYSEX, sysexCallback);
Firmata.begin(57600);
}
void loop()
{
while(Firmata.available()) {
Firmata.processInput();
}
}
2. Sysexを受信した時のコールバック関数を登録
StandardFirmataを改造して、 void sysexCallback(byte command, byte argc, byte *argv) 関数内のswitch文に書き足すといい。
I2C関係もここで定義されているので破壊しないように注意しつつ追加する。
Arduino IDEに入っている
StandardFirmataにLED点滅コマンドを追加してみた。
追加した箇所は
差分を見ればわかりやすいと思う。
自作したSysexコマンドを呼び出すPC側のプログラム
試しに、指定したLEDを指定回数、指定間隔(ミリ秒)で点滅させるsysexコマンド(0x01)を作る。
Ruby用Firmataライブラリで、sysexコマンドを送るコード。
require 'rubygems'
require 'arduino_firmata'
arduino = ArduinoFirmata.connect ARGV.shift
puts "firmata version #{arduino.version}"
## regist event
arduino.on :sysex do |command, data|
puts "command : #{command}"
puts "data : #{data.inspect}"
end
## send sysex command
arduino.sysex 0x01, [13, 5, 2] # pin13, blink 5 times, 200 msec interval
arduino.sysex 0x01, [11, 3, 10] # pin11, blink 3 times, 1000 msec interval
loop do
sleep 1
end
Arduino側。
EchoStringのサンプルを改造した。
受け取ったSysexコマンドとデータをそのまま返すようにしてある。
#include <Firmata.h>
void setup()
{
Firmata.setFirmwareVersion(FIRMATA_MAJOR_VERSION, FIRMATA_MINOR_VERSION);
Firmata.attach(START_SYSEX, sysexCallback);
Firmata.begin(57600);
}
void loop()
{
while(Firmata.available()) {
Firmata.processInput();
}
}
void sysexCallback(byte command, byte argc, byte*argv)
{
switch(command){
case 0x01: // LED Blink Command
if(argc < 3) break;
byte blink_pin;
byte blink_count;
int delayTime;
blink_pin = argv[0];
blink_count = argv[1];
delayTime = argv[2] * 100;
pinMode(blink_pin, OUTPUT);
byte i;
for(i = 0; i < blink_count; i++){
digitalWrite(blink_pin, true);
delay(delayTime);
digitalWrite(blink_pin, false);
delay(delayTime);
}
Firmata.sendSysex(command, argc, argv); // callback
break;
}
}
StandardFirmataに同様の機能を追加してみた。この方法なら、いつものanalogReadやdigitalWrite機能が使えるままのArduinoに、赤外線学習リモコン機能を追加したりとかできる。
追加した箇所は
差分を見るとわかりやすいと思う。
Android版からもRubyと同様にコマンドを送れる。
byte[] data = {11, 3, 10}; // pin11, blink 3 times, 1000 msec interval
arduino.sysex((byte)0x01, data);
Firmata.sendSysex
Arduino側からPCにsysexコマンドを送るFirmata.sendSysex(byte command, byte argc, byte*argv)関数は、内部でsendValueAsTwo7bitBytes(int value)を呼び出している。
sysexで送れるデータは7bitまでなので、8bit以上と7bit以下を2byteに分けて送信するために使われている。
sendSysexに14bitの値を入れれるならいいんだけど、byte配列しか渡せなくて効率が良くないのでFirmata.send関数を直接使いたいがこっちはprivateになっていて使えない。
幸い、PC側はバッファサイズは無限に余裕があるので何byteでも1度に送って問題ない。