こんばんは。
普段は組み込み系(主にWindowsCEやOSレス)のC, C++やってます。
Goはほぼ趣味でたまに触っている程度なので、お手柔らかに…。
対象の方
- Goでシリアル通信してみたい方
- Goで組み込みっぽい事をやってみたい方
はじめに
Goでお手軽にサブGHz帯通信をしたかったので、go-im920 をつくりました。
インタープラン株式会社様の920MHz無線モジュール、IM920用の制御ライブラリです。
シリアル通信のライブラリは tarm/serial を使わせて貰ったのですが、データ受信でハマったのでご紹介します。
開発はWindows上でおこなっています。
最初はリードタイムアウト
sc := &serial.Config{Name: "COM4", Baud: 19200, ReadTimeout: 1 * time.Second}
buf := make([]byte, 128)
n, _ = sc.Read(buf)
上記のコードだと受信データサイズが128byteとなるかReadTimeoutするまで、Read()から復帰しません。
IM920は送信したコマンドに対し毎回応答を返してくるのですが、その度にReadTimeoutを待つのは時間がかかりすぎです。
かといって、ReadTimeoutを短くし過ぎると応答を受信できない可能性があります。
いくつかのコマンドを除き応答は固定長(4byte)だったので、とりあえず buf := make([]byte, 4) で逃げました。
次は応答受信しきる前に復帰
tarm/serial の このコミット から、受信データがあれば即座に復帰するように挙動が変わりました。
IM920はほとんどのコマンドに対して "OK\r\n" か "NG\r\n" を返してきますが、この変更で応答を1byte受信しただけ(例:"O")でRead()が復帰するようになってしまいました。困った。
受信インターバルタイマを有効にしようと思ったけど方針転換
Windowsの場合、例えば「受信処理トータルでのタイムアウト」と「データ受信間隔に対してのタイムアウト」が 設定 できます。
Linuxの場合、TERMIOS でタイムアウト周りの設定が出来るようですが、Windowsのように「受信処理トータルでのタイムアウト」と「データ受信間隔に対してのタイムアウト」の両立はこれ単体ではできないようです。1
当初はtarm/serialを変更して受信インターバルタイマを有効にしようと考えてましたが、Linuxで同等の挙動を実現するのは難しそう&tarm/serialのREADME.mdに
Please note that this is the total timeout the read operation will wait and not the interval timeout between two bytes.
と記載があったので、受信インターバルタイマ無しがtarmさんの期待値だと判断し、go-im920 内部でどうにかすることにしました。
1byteずつRead()することでtarm/serialのReadTimeoutは受信インターバルタイマとしてのみ使用し、time.NewTimer()で受信処理トータルでのタイムアウトを検知します。
func (im *IM920) receive(p []byte) (readed int, err error) {
timer := time.NewTimer(im.readTimeout)
defer timer.Stop()
readedInitialbyte := false
for {
select {
case <-timer.C:
if readed == 0 {
err = fmt.Errorf("error: Read failed: no data")
}
return
default:
n, rerr := im.s.Read(p[readed : readed+1])
if rerr != nil {
err = fmt.Errorf("error: Read failed: %s", rerr)
return
}
if n > 0 && !readedInitialbyte {
readedInitialbyte = true
}
if n == 0 && readedInitialbyte {
return
}
readed += n
if readed >= len(p) {
return
}
}
}
}
もっとうまいやり方があるのかもしれませんが、正常に動作しているようなので良しとします。
最後に
このネタ全然Goっぽくない…。
-
実はselect()併用すればできそう? ↩