Windows Remote Arduino を利用して Arduino 戦車を動かす | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/7168
では、Windows がオープンソース化している Firmata ライブラリを利用して Arduino に接続したわけですが、Firmata プロトコル自体は公開されているので、どのような言語でも誰でも作れます。
firmata/arduino
https://github.com/firmata/arduino
github の readme を眺めると、.NET 実装もあります。ソースを見ていくと COM 経由で Arduino に USB ケーブルを刺して使うライブラリになっていますが、これをちょっと修正すれば Bluetooth のシリアル通信対応にできますよね。ということで、Bluetooth 2.0 のシリアル通信である RFCOMM を使って書き換えていきます。
Firmata.NET | imagitronics.org
http://www.imagitronics.org/projects/firmatanet/
2 つある中では、Firmata.NET のほうがコードが短かったので、これを利用します。このコードを使って、Android 上から Firmata を通して Arduino を操作できるようにしましょう。確か、ブラウザや Node.js から使うパターンが多いのですが、Xamarin.Android から C# で扱えるとネイティブアプリとして作れるので便利でしょう。ちなみに、コード自体は、短いので Xamarin の Starter 版(無償版)でも動作確認ができました。無償版の場合 128KB 制限なので、そのなかで収まっていると思われます(正確な大きさはわからない)。
RFCOMM 版の Firmata を作る
ざっくりと移植したのが以下です。Android の Bluetooth を使うために、BluetoothAdapter.DefaultAdapter を利用しています。
class Arduino
{
public static int INPUT = 0;
public static int OUTPUT = 1;
public static int LOW = 0;
public static int HIGH = 1;
private const int MAX_DATA_BYTES = 32;
private const int DIGITAL_MESSAGE = 0x90; // send data for a digital port
private const int ANALOG_MESSAGE = 0xE0; // send data for an analog pin (or PWM)
private const int REPORT_ANALOG = 0xC0; // enable analog input by pin #
private const int REPORT_DIGITAL = 0xD0; // enable digital input by port
private const int SET_PIN_MODE = 0xF4; // set a pin to INPUT/OUTPUT/PWM/etc
private const int REPORT_VERSION = 0xF9; // report firmware version
private const int SYSTEM_RESET = 0xFF; // reset from MIDI
private const int START_SYSEX = 0xF0; // start a MIDI SysEx message
private const int END_SYSEX = 0xF7; // end a MIDI SysEx message
// private SerialPort _serialPort;
private int delay;
private int waitForData = 0;
private int executeMultiByteCommand = 0;
private int multiByteChannel = 0;
private int[] storedInputData = new int[MAX_DATA_BYTES];
private bool parsingSysex;
private int sysexBytesRead;
private volatile int[] digitalOutputData = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
private volatile int[] digitalInputData = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
private volatile int[] analogInputData = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
private int majorVersion = 0;
private int minorVersion = 0;
// private Thread readThread = null;
private object locker = new object();
/*
Guid serviceGuid = Guid.Parse("00001101-0000-1000-8000-00805f9b34fb");
RfcommDeviceService rfcommService;
StreamSocket socket;
DataWriter writer;
DataReader reader;
*/
BluetoothSocket _socket;
/// <summary>
///
/// </summary>
/// <param name="serialPortName">String specifying the name of the serial port. eg COM4</param>
/// <param name="baudRate">The baud rate of the communication. Default 115200</param>
/// <param name="autoStart">Determines whether the serial port should be opened automatically.
/// use the Open() method to open the connection manually.</param>
/// <param name="delay">Time delay that may be required to allow some arduino models
/// to reboot after opening a serial connection. The delay will only activate
/// when autoStart is true.</param>
public Arduino(string serialPortName, Int32 baudRate, bool autoStart, int delay)
{
/*
_serialPort = new SerialPort(serialPortName, baudRate);
_serialPort.DataBits = 8;
_serialPort.Parity = Parity.None;
_serialPort.StopBits = StopBits.One;
*/
if (autoStart)
{
this.delay = delay;
this.Connect();
this.Open();
}
}
/// <summary>
/// Creates an instance of the Arduino object, based on a user-specified serial port.
/// Assumes default values for baud rate (115200) and reboot delay (8 seconds)
/// and automatically opens the specified serial connection.
/// </summary>
/// <param name="serialPortName">String specifying the name of the serial port. eg COM4</param>
public Arduino(string serialPortName) : this(serialPortName, 115200, true, 8000) { }
/// <summary>
/// Creates an instance of the Arduino object, based on user-specified serial port and baud rate.
/// Assumes default value for reboot delay (8 seconds).
/// and automatically opens the specified serial connection.
/// </summary>
/// <param name="serialPortName">String specifying the name of the serial port. eg COM4</param>
/// <param name="baudRate">Baud rate.</param>
public Arduino(string serialPortName, Int32 baudRate) : this(serialPortName, baudRate, true, 8000) { }
/// <summary>
/// Creates an instance of the Arduino object using default arguments.
/// Assumes the arduino is connected as the HIGHEST serial port on the machine,
/// default baud rate (115200), and a reboot delay (8 seconds).
/// and automatically opens the specified serial connection.
/// </summary>
public Arduino() : this(Arduino.list().ElementAt(list().Length - 1), 115200, false, 8000) { }
public void Connect()
{
BluetoothAdapter adapter = BluetoothAdapter.DefaultAdapter;
if (adapter == null)
{
throw new Exception("No Bluetooth adapter found.");
}
if (!adapter.IsEnabled)
{
throw new Exception("Bluetooth adapter is not enabled.");
}
BluetoothDevice device = (from bd in adapter.BondedDevices
where bd.Name == "HC-06"
select bd).FirstOrDefault();
if (device == null)
{
throw new Exception("Named device not found.");
}
_socket = device.CreateRfcommSocketToServiceRecord(UUID.FromString("00001101-0000-1000-8000-00805f9b34fb"));
_socket.Connect();
return;
}
/// <summary>
/// Opens the serial port connection, should it be required. By default the port is
/// opened when the object is first created.
/// </summary>
public void Open()
{
// _serialPort.Open();
// Thread.Sleep(delay);
byte[] command = new byte[2];
for (int i = 0; i < 6; i++)
{
command[0] = (byte)(REPORT_ANALOG | i);
command[1] = (byte)1;
// _serialPort.Write(command, 0, 2);
_socket.OutputStream.Write(command, 0, command.Length);
}
for (int i = 0; i < 2; i++)
{
command[0] = (byte)(REPORT_DIGITAL | i);
command[1] = (byte)1;
// _serialPort.Write(command, 0, 2);
_socket.OutputStream.Write(command, 0, command.Length);
}
command = null;
/*
if (readThread == null)
{
readThread = new Thread(processInput);
readThread.Start();
}
*/
}
/// <summary>
/// Closes the serial port.
/// </summary>
public void Close()
{
// readThread.Join(500);
// readThread = null;
// _serialPort.Close();
_socket.Close();
_socket = null;
}
/// <summary>
/// Lists all available serial ports on current system.
/// </summary>
/// <returns>An array of strings containing all available serial ports.</returns>
public static string[] list()
{
// return SerialPort.GetPortNames();
return new string[] { "HC-06" };
}
/// <summary>
/// Returns the last known state of the digital pin.
/// </summary>
/// <param name="pin">The arduino digital input pin.</param>
/// <returns>Arduino.HIGH or Arduino.LOW</returns>
public int digitalRead(int pin)
{
return (digitalInputData[pin >> 3] >> (pin & 0x07)) & 0x01;
}
/// <summary>
/// Returns the last known state of the analog pin.
/// </summary>
/// <param name="pin">The arduino analog input pin.</param>
/// <returns>A value representing the analog value between 0 (0V) and 1023 (5V).</returns>
public int analogRead(int pin)
{
return analogInputData[pin];
}
/// <summary>
/// Sets the mode of the specified pin (INPUT or OUTPUT).
/// </summary>
/// <param name="pin">The arduino pin.</param>
/// <param name="mode">Mode Arduino.INPUT or Arduino.OUTPUT.</param>
public void pinMode(int pin, int mode)
{
byte[] message = new byte[3];
message[0] = (byte)(SET_PIN_MODE);
message[1] = (byte)(pin);
message[2] = (byte)(mode);
// _serialPort.Write(message, 0, 3);
_socket.OutputStream.Write(message, 0, message.Length);
message = null;
}
/// <summary>
/// Write to a digital pin that has been toggled to output mode with pinMode() method.
/// </summary>
/// <param name="pin">The digital pin to write to.</param>
/// <param name="value">Value either Arduino.LOW or Arduino.HIGH.</param>
public void digitalWrite(int pin, int value)
{
int portNumber = (pin >> 3) & 0x0F;
byte[] message = new byte[3];
if (value == 0)
digitalOutputData[portNumber] &= ~(1 << (pin & 0x07));
else
digitalOutputData[portNumber] |= (1 << (pin & 0x07));
message[0] = (byte)(DIGITAL_MESSAGE | portNumber);
message[1] = (byte)(digitalOutputData[portNumber] & 0x7F);
message[2] = (byte)(digitalOutputData[portNumber] >> 7);
// _serialPort.Write(message, 0, 3);
_socket.OutputStream.Write(message, 0, message.Length);
}
/// <summary>
/// Write to an analog pin using Pulse-width modulation (PWM).
/// </summary>
/// <param name="pin">Analog output pin.</param>
/// <param name="value">PWM frequency from 0 (always off) to 255 (always on).</param>
public void analogWrite(int pin, int value)
{
byte[] message = new byte[3];
message[0] = (byte)(ANALOG_MESSAGE | (pin & 0x0F));
message[1] = (byte)(value & 0x7F);
message[2] = (byte)(value >> 7);
// _serialPort.Write(message, 0, 3);
_socket.OutputStream.Write(message, 0, message.Length);
}
private void setDigitalInputs(int portNumber, int portData)
{
digitalInputData[portNumber] = portData;
}
private void setAnalogInput(int pin, int value)
{
analogInputData[pin] = value;
}
private void setVersion(int majorVersion, int minorVersion)
{
this.majorVersion = majorVersion;
this.minorVersion = minorVersion;
}
/*
private int available()
{
return _serialPort.BytesToRead;
}
*/
} // End Arduino class
接続あたりは、
Android から Bluetooth+RFCOMM を利用してモーター制御をする | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/6826
と同じように書いています。独自に RFCOMM を使った場合は自前で Android/Arduino のプロトコルを作らなければいけませんが(とはいえ、自分の場合は 8 バイト固定にしてあるの簡単)、Firmata プロトコルを使うと、GPIO 等をそのまま使う分には手軽です。
バイナリ送信をしているとこもそのまま移植。今回はテスト的なものなので、Android の受信側は省略しました。もうちょっと整理して、そのうち github へ。
マニフェストを設定する
Bluetooth を扱うので、パーミッションを設定しておきます。
たぶん、”BLUETOOTH” だけチェックすれば ok です。
UI と MainActivity
こんな画面を作っておきます。
5ピンに LED をつけるので、pinMode などを設定します。
public class MainActivity : Activity
{
Arduino arduino;
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
// Set our view from the "main" layout resource
SetContentView(Resource.Layout.Main);
// Get our button from the layout resource,
// and attach an event to it
arduino = new Arduino();
FindViewById<Button>(Resource.Id.buttonConnect).Click += (s, e) => {
arduino.Connect();
FindViewById<Button>(Resource.Id.buttonConnect).Text = "connected.";
};
FindViewById<Button>(Resource.Id.buttonOpen).Click += (s, e) => {
arduino.Open();
FindViewById<Button>(Resource.Id.buttonOpen).Text = "Firmata opend";
arduino.pinMode(5, Arduino.OUTPUT);
arduino.digitalWrite(5, Arduino.LOW);
};
FindViewById<Button>(Resource.Id.buttonLEDon).Click += OnClickLedOn;
FindViewById<Button>(Resource.Id.buttonLEDoff).Click += OnClickLedOff;
}
void OnClickLedOn(object sender, EventArgs e)
{
arduino.digitalWrite(5, Arduino.HIGH);
}
void OnClickLedOff(object sender, EventArgs e)
{
arduino.digitalWrite(5, Arduino.LOW);
}
}
Connect と Open は同時にやってもいいと思います。RFCOMM へのアクセスを Sync のほうの非同期関数を使えばよかったのですが、ひとまず同期的に作っています。まあ、受信回り(温度や湿度データとか)をきちんと作って、await/async を使えば結構すっきりするハズです。
実行してみる
ビルドをして実機で実行してみます。うちの Android は 4.1.2 という古いタイプなのですが正常に動作しました。Bluetooth 経由なので、アクセスポイントとかが必要ないので戸外でも使えますよね。まあ、戸外で使って、どうということはないのですが。
これはこれで整理して、後で Arduino 戦車も動かせるように組み直しいきましょう。あと、適当な距離センサーや加速度センサーを付けて、値をとれるようにいておきます。



