Goでのシリアル通信でハマった事

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

こんばんは。
普段は組み込み系(主にWindowsCEやOSレス)のC, C++やってます。
Goはほぼ趣味でたまに触っている程度なので、お手柔らかに…。

対象の方

  • Goでシリアル通信してみたい方
  • Goで組み込みっぽい事をやってみたい方

はじめに

Goでお手軽にサブGHz帯通信をしたかったので、go-im920 をつくりました。
インタープラン株式会社様の920MHz無線モジュール、IM920用の制御ライブラリです。

シリアル通信のライブラリは tarm/serial を使わせて貰ったのですが、データ受信でハマったのでご紹介します。
開発はWindows上でおこなっています。

最初はリードタイムアウト

main.go
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()で受信処理トータルでのタイムアウトを検知します。

go-im920.go
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っぽくない…。


  1. 実はselect()併用すればできそう? 

この投稿は Go その3 Advent Calendar 201513日目の記事です。