Scala から Gainer mini を使う [Scala]
このところマイコンを使った電子工作に興味を持ち始めました。電子工作をやっているといっても理科の知識は中学生未満だし半田付けもできるだけ避けたいという軟派な感じのものですが。
マイコンとしては主にPICという電子工作によく使われている廉価なものを使っていて、アセンブラかCで書いたプログラムをマイコンのフラッシュメモリに書き込むのですが、ちょっとした実験のために使い捨てプログラムが必要になることがあります。しかしそうしたプログラムをいちいちアセンブラやCで書いてライターで書き込むのは面倒くさい。多少の制約(時間的な精度とか)は受け入れても超高級言語が使いたい、と思うことがあります。
そこで Gainer mini [http://gainer-mini.jp/] というデバイスを買いました。これはPCとUSBケーブルで接続され、電子回路上の電圧の制御や読み取りがPC上のプログラミング言語から行えるようにパッケージ化されたものです。フィジカルコンピューティングデバイスというカテゴリに含まれるものですが、その言葉の意味はともかくとして、これを使うと電子回路の頭脳の部分を回路上のマイコンから外に出してPC上に移すことが出来ます。
プログラミング言語として公式には Flash+ActionScript、Processing、Max/MSP が使え、私はまず Processing を使ってみたのですが、いろいろと言語やライブラリに対する不満が出てきたため、Scala から使うためのライブラリを作ってみました。
まずは単純な使用例です。Gainer mini に LED を2つ(dout 0,1)、押しボタンスイッチを1つ(din 0)、ツマミ(可変抵抗)を1つ(ain 0)取り付けます。
この回路を使って、「周期的に LED を点滅させる。点滅周期の長さはツマミで制御し、スイッチが押されていないときは dout 0 につながった LED を、押されたときは dout 1 につながった LED を点滅させる。Gainer のオンボードスイッチが押されたら終了」ということをするプログラムを Scala で書きます。
val gainer = Gainer("COM3")
try {
gainer.open()
while (!gainer.button) {
val led = if (gainer.din(0)) 0 else 1
val interval = gainer.ain(0)
gainer.dout(led) = true
Thread.sleep(interval)
gainer.dout(led) = false
Thread.sleep(interval)
}
} finally {
gainer.close()
}
実行結果です。(動画です)
以下がライブラリのソースコードです。Gainer mini のライブラリが使っているのと同じ RXTX というシリアル通信用の Java ライブラリを使っています。RXTXcomm.jar を Scala の lib ディレクトリに、rxtxSerial.dll(Windows 前提で話していますが)をパスの通った場所においてください。
import gnu.io._
import java.io._
import scala.actors._
import scala.actors.Actor._
import scala.util.DynamicVariable
import scala.collection.mutable.{ArrayBuffer,Queue}
object Gainer {
implicit def enum2Iter[T](enum: java.util.Enumeration[T]): Iterator[T] = {
new Iterator[T] {
def hasNext:Boolean = enum.hasMoreElements()
def next:T = enum.nextElement()
}
}
def getCommPortIdentifier(name: String) = {
import CommPortIdentifier._
getPortIdentifiers.asInstanceOf[java.util.Enumeration[CommPortIdentifier]].
filter{_.getPortType == PORT_SERIAL}.
filter{_.getName == name}.
toList.firstOption
}
implicit val callback = actor { loop { react { case _ => () }}}
def apply(portName: String)
= new Gainer(portName, Mode.Mode1, callback)
def apply(portName: String, mode: Mode.Value)(implicit callback: Actor)
= new Gainer(portName, mode, callback)
}
object Mode extends Enumeration {
val Mode1 = Value("1")
val Mode2 = Value("2")
val Mode3 = Value("3")
val Mode4 = Value("4")
val Mode5 = Value("5")
val Mode6 = Value("6")
// val Mode7 = Value("7")
// val Mode8 = Value("8")
}
abstract class Result
case class Button(pressed: Boolean) extends Result
case class Din(port: Int, high: Boolean) extends Result
case class Ain(port: Int, value: Int) extends Result
abstract class Command
case class DinReq(replyTo: Actor) extends Command
case class DinRes(values: Array[Boolean]) extends Command
case class AinReq(replyTo: Actor) extends Command
case class AinRes(values: Array[Int]) extends Command
case class Async(command: String) extends Command
class Gainer(portName: String, mode: Mode.Value, callback: Actor)
extends SerialPortEventListener {
import Gainer._
override def toString = portName
private val Some(portId) = getCommPortIdentifier(portName)
case class Port(port: SerialPort, input: InputStream, output: OutputStream)
private var port: Option[Port] = None
def open() = {
if (port.isEmpty) {
val port = portId.open("Scala-Gainer", 10000).asInstanceOf[SerialPort]
try {
val input = port.getInputStream()
val output = port.getOutputStream()
this.port = Some(Port(port, input, output))
port.setSerialPortParams(
38400, 8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);
agent ! Async("KONFIGURATION_" + mode + "*")
Thread.sleep(100)
port.addEventListener(this)
port.notifyOnDataAvailable(true)
} catch {
case _ => close()
}
}
this
}
def close() {
port.foreach{ case Port(port, input, output) =>
input.close()
output.close()
port.removeEventListener()
port.close()
}
port = None
}
private val agent = actor {
def send(command: String) {
port.foreach{ case Port(_, _, output) =>
output.write(command.getBytes)
output.flush()
}
}
val dinReplyTo = new Queue[Actor]()
val ainReplyTo = new Queue[Actor]()
loop {
react {
case DinReq(replyTo) => dinReplyTo += replyTo; send("R*")
case r @ DinRes(_) => dinReplyTo.dequeue ! r
case AinReq(replyTo) => ainReplyTo += replyTo; send("I*")
case r @ AinRes(_) => ainReplyTo.dequeue ! r
case Async(command) => send(command)
}
}
}
// on-board LED
private var ledStatus = false
def led = ledStatus
def led_= (led: Boolean) = agent ! Async(if (led) "h*" else "l*")
// on-board button
private var buttonStatus = false
def button = buttonStatus
// digital output pins
object dout {
private[Gainer] val values = new Array[Boolean](16)
def apply(i: Int) = values(i)
def update(i: Int, v: Boolean) {
if (i < 0 || i > 0xf) error("port number out of range: " + i)
agent ! Async((if (v) "H" else "L") + i.toHexString.toUpperCase + "*")
}
}
// digital input pins
object din {
val values = new Array[Boolean](16)
def apply(i: Int) = { peek(); values(i) }
def peek() = {
agent ! DinReq(self)
receiveWithin(1000) {
case DinRes(values) => values.copyToArray(this.values, 0)
}
}
def begin() { agent ! Async("r*") }
def end() { agent ! Async("E*") }
}
// analog output pins
object aout {
private[Gainer] val values = new Array[Int](16)
def apply(i: Int) = values(i)
def update(i: Int, v: Int) {
if (i < 0 || i > 0xf) error("port number out of range: " + i)
if (v < 0 || v > 0xff) error("analog value out of range: " + i)
agent ! Async("a%1x%02x*".format(i, v))
}
}
// analog input pins
object ain {
val values = new Array[Int](16)
def apply(i: Int) = { peek(); values(i) }
def peek() = {
agent ! AinReq(self)
receiveWithin(1000) {
case AinRes(values) => values.copyToArray(this.values, 0)
}
}
def begin() { agent ! Async("I*") }
def end() { agent ! Async("E*") }
}
// serial port event handler
private val buffer = new ArrayBuffer[Byte]()
def serialEvent(ev: SerialPortEvent) = synchronized {
ev.getEventType match {
case SerialPortEvent.DATA_AVAILABLE =>
port.foreach{ case Port(_, input, _) =>
while (input.available > 0) {
input.read() match {
case '*' => propagate(new String(buffer.toArray)); buffer.clear()
case c => buffer += c.asInstanceOf[Byte]
}
}
}
}
}
private var prevR = ""
private var prevI = ""
private def propagate(r: String) {
r.substring(0,1) match {
case "N" => buttonStatus = true; callback ! Button(true)
case "F" => buttonStatus = false; callback ! Button(false)
case "h" => ledStatus = true
case "l" => ledStatus = false
case "H" => dout.values(Integer.parseInt(r.substring(1, 2), 16)) = true
case "L" => dout.values(Integer.parseInt(r.substring(1, 2), 16)) = false
case "R" => val bitmap = Integer.parseInt(r.substring(1, 5), 16)
val ar = for (i <- 0 to 15) yield (bitmap & (1 << i)) > 0
agent ! DinRes(ar.toArray)
case "r" => if (r != prevR) {
prevR = r
val bitmap = Integer.parseInt(r.substring(1, 5), 16)
for (i <- 0 to 15) {
val newValue = (bitmap & (1 << i)) > 0
if (din.values(i) != newValue) {
din.values(i) = newValue
callback ! Din(i, newValue)
}
}
}
case "a" => val port = Integer.parseInt(r.substring(1, 2), 16)
val value = Integer.parseInt(r.substring(2, 4), 16)
aout.values(port) = value
case "I" => val ar = for (i <- 1 to r.length - 3 by 2)
yield Integer.parseInt(r.substring(i, i + 2), 16)
agent ! AinRes(ar.toArray)
case "i" => if (r != prevI) {
prevI = r
val ar = for (i <- 1 to r.length - 3 by 2)
yield Integer.parseInt(r.substring(i, i + 2), 16)
for (i <- 0 to ar.length) {
if (ain.values(i) != ar(i)) {
ain.values(i) = ar(i)
callback ! Ain(i, ar(i))
}
}
}
case _ => ()
}
}
}
最後に簡単に使い方を説明します。
* 生成
Gainer mini を使うにはまず Gainer オブジェクトを生成します。直接 new Gainer(...) するか、Gainer シングルトンオブジェクトの apply を呼ぶかします。引数は最大3つで、COMポート名、モード、コールバックを受けるアクターです。
例:
val gainer = Gainer("COM3")
* 接続・切断
Gainer と接続するには Gainer#open、切断するには Gainer#close を呼びます。
* デジタル出力
デジタル出力ピンを変化させるには Gainer#dout を配列風操作で設定します。読み取りも配列風に出来ますが、反映は非同期で、Gainer から応答が返ってきたときに更新されます。
例:
gainer.dout(0) = true // dout 0 を High にする
* デジタル入力
デジタル入力ピンを読み取るには Gainer#din を配列風操作で読み取ります。この際 Gainer に問い合わせを行い、同期的に戻り値を返します(ここが Gainer の Processing ライブラリなどとは違います)。
例:
val value: Boolean = gainer.din(0) // din 0 の状態を読み取る
この値は単純に電圧が High のとき true、Low のとき false になります。プルアップ接続のときには負論理になります。
Gainer#din#peek は Gainer に全デジタル入力ピンの状態の問い合わせを1度行い、結果を Gainer#din#values 配列に入れます。これも同期的です。Gainer#din#values には書き込みも出来ますが、しても何も起こりません。
Gainer#din#begin を呼ぶとデジタル入力ピンの状態が持続的に Gainer#din#values に更新され続けるようになります。またコールバック用のアクターを指定している場合にはアクターに Din(port, high) というメッセージ(port はピン番号、high は状態)が送信されます(変化があったときのみ)。この間中、Gainer と PC の間を(状態の変化があろうがなかろうが)データが流れ続けています(Gainer から出てくる時点で更新時のみになってくれたほうがいいのですが…)。やめるには Gainer#din#end を呼びます。
* アナログ出力・アナログ入力
API はデジタルの場合と同様で、aout, ain となります。値は 0 から 255 までの Int として表現されます。Gainer#ain#begin を使った場合は Ain(port, value) メッセージがコールバックアクターに送られます。
* オンボードLED
Gainer mini 上の LED を点灯・消灯させたいときは Gainer#led プロパティを使います。設定時の反映は非同期です。
例:
gainer.led = true // LED点灯
* オンボードスイッチ
Gainer mini 基板上のボタンの状態は Gainer#button に更新されます。またコールバックアクターに Button(pressed) メッセージが送信されます。
ところでこのライブラリを使うと、終了時に JVM がいつまでも終わってくれないということがままあります。多分シリアル通信ライブラリがらみだと思うのですがよくわかりません。
コメント 0