℘ make now just

if you wanna break free you better listen to me

AprMayJunJulAugSepOctNovDecJanFebMar/post/2025-03-23-diary//post/2025-03-24-diary//post/2025-03-25-diary//post/2025-03-26-diary//post/2025-03-27-diary//post/2025-03-28-diary//post/2025-03-29-diary//post/2025-03-30-diary//post/2025-03-31-diary//post/2025-04-01-diary//post/2025-04-02-diary//post/2025-04-03-diary//post/2025-04-04-diary//post/2025-04-05-diary//post/2025-04-06-diary//post/2025-04-07-diary//post/2025-04-08-diary//post/2025-04-09-diary//post/2025-04-10-diary//post/2025-04-11-diary//post/2025-04-12-diary//post/2025-04-13-diary//post/2025-04-14-diary//post/2025-04-15-diary//post/2025-04-16-diary//post/2025-04-17-diary//post/2025-04-18-diary//post/2025-04-19-diary//post/2025-04-20-diary//post/2025-04-21-diary//post/2025-04-22-diary//post/2025-04-23-diary//post/2025-04-24-diary//post/2025-04-25-diary//post/2025-04-26-diary//post/2025-04-27-diary//post/2025-04-28-diary//post/2025-04-29-diary//post/2025-04-30-diary//post/2025-05-01-diary//post/2025-05-02-diary//post/2025-05-03-diary//post/2025-05-04-diary//post/2025-05-05-diary//post/2025-05-06-diary//post/2025-05-07-diary//post/2025-05-08-diary//post/2025-05-09-diary//post/2025-05-10-diary//post/2025-05-11-diary//post/2025-05-12-diary//post/2025-05-13-diary//post/2025-05-14-diary//post/2025-05-15-diary//post/2025-05-16-diary//post/2025-05-17-diary//post/2025-05-18-diary//post/2025-05-19-diary//post/2025-05-20-diary//post/2025-05-21-diary//post/2025-05-22-diary//post/2025-05-23-diary//post/2025-05-24-diary//post/2025-05-25-diary//post/2025-05-26-diary//post/2025-05-27-diary//post/2025-05-28-diary//post/2025-05-29-diary//post/2025-05-30-diary//post/2025-05-31-diary//post/2025-06-01-diary//post/2025-06-02-diary//post/2025-06-03-diary//post/2025-06-04-diary//post/2025-06-05-diary//post/2025-06-06-diary//post/2025-06-07-diary//post/2025-06-08-diary//post/2025-06-09-diary//post/2025-06-10-diary//post/2025-06-11-diary//post/2025-06-12-diary//post/2025-06-13-diary//post/2025-06-14-diary//post/2025-06-15-diary//post/2025-06-16-diary//post/2025-06-17-diary//post/2025-06-18-diary//post/2025-06-19-diary//post/2025-06-20-diary//post/2025-06-21-diary//post/2025-06-22-diary//post/2025-06-23-diary//post/2025-06-24-diary//post/2025-06-25-diary//post/2025-06-26-diary//post/2025-06-27-diary//post/2025-06-28-diary//post/2025-06-29-diary//post/2025-06-30-diary//post/2025-07-01-diary//post/2025-07-02-diary//post/2025-07-03-diary//post/2025-07-04-diary//post/2025-07-05-diary//post/2025-07-06-diary//post/2025-07-07-diary//post/2025-07-08-diary//post/2025-07-09-diary//post/2025-07-10-diary//post/2025-07-11-diary//post/2025-07-12-diary//post/2025-07-13-diary//post/2025-07-14-diary//post/2025-07-15-diary//post/2025-07-16-diary//post/2025-07-17-diary//post/2025-07-18-diary//post/2025-07-19-diary//post/2025-07-20-diary//post/2025-07-21-diary//post/2025-07-22-diary//post/2025-07-23-diary//post/2025-07-24-diary//post/2025-07-25-diary//post/2025-07-26-diary//post/2025-07-27-diary//post/2025-07-28-diary//post/2025-07-29-diary//post/2025-07-30-diary//post/2025-07-31-diary//post/2025-08-01-diary//post/2025-08-02-diary//post/2025-08-03-diary//post/2025-08-04-diary//post/2025-08-05-diary//post/2025-08-06-diary//post/2025-08-07-diary//post/2025-08-08-diary//post/2025-08-09-diary//post/2025-08-10-diary//post/2025-08-11-diary//post/2025-08-12-diary//post/2025-08-13-diary//post/2025-08-14-diary//post/2025-08-15-diary//post/2025-08-16-diary//post/2025-08-17-diary//post/2025-08-18-diary//post/2025-08-19-diary//post/2025-08-20-diary//post/2025-08-21-diary//post/2025-08-22-diary//post/2025-08-23-diary//post/2025-08-24-diary//post/2025-08-25-diary//post/2025-08-26-diary//post/2025-08-27-diary//post/2025-08-28-diary//post/2025-08-29-diary//post/2025-08-30-diary//post/2025-08-31-diary//post/2025-09-01-diary//post/2025-09-02-diary//post/2025-09-03-diary//post/2025-09-04-diary//post/2025-09-05-diary//post/2025-09-06-diary//post/2025-09-07-diary//post/2025-09-08-diary//post/2025-09-09-diary//post/2025-09-10-diary//post/2025-09-11-diary//post/2025-09-12-diary//post/2025-09-13-diary//post/2025-09-14-diary//post/2025-09-15-diary//post/2025-09-16-diary//post/2025-09-17-diary//post/2025-09-18-diary//post/2025-09-19-diary//post/2025-09-20-diary//post/2025-09-21-diary//post/2025-09-22-diary//post/2025-09-23-diary//post/2025-09-24-diary//post/2025-09-25-diary//post/2025-09-26-diary//post/2025-09-27-diary//post/2025-09-28-diary//post/2025-09-29-diary//post/2025-09-30-diary//post/2025-10-01-diary//post/2025-10-02-diary//post/2025-10-03-diary//post/2025-10-04-diary//post/2025-10-05-diary//post/2025-10-06-diary//post/2025-10-07-diary//post/2025-10-08-diary//post/2025-10-09-diary//post/2025-10-10-diary//post/2025-10-11-diary//post/2025-10-12-diary//post/2025-10-13-diary//post/2025-10-14-diary//post/2025-10-15-diary//post/2025-10-16-diary//post/2025-10-17-diary//post/2025-10-18-diary//post/2025-10-19-diary//post/2025-10-20-diary//post/2025-10-21-diary//post/2025-10-22-diary//post/2025-10-23-diary//post/2025-10-24-diary//post/2025-10-25-diary//post/2025-10-26-diary//post/2025-10-27-diary//post/2025-10-28-diary//post/2025-10-29-diary//post/2025-10-30-diary//post/2025-10-31-diary//post/2025-11-01-diary//post/2025-11-02-diary//post/2025-11-03-diary//post/2025-11-04-diary//post/2025-11-05-diary//post/2025-11-06-diary//post/2025-11-07-diary//post/2025-11-08-diary//post/2025-11-09-diary//post/2025-11-10-diary//post/2025-11-11-diary//post/2025-11-12-diary//post/2025-11-13-diary//post/2025-11-14-diary//post/2025-11-15-diary//post/2025-11-16-diary//post/2025-11-17-diary//post/2025-11-18-diary//post/2025-11-19-diary//post/2025-11-20-diary//post/2025-11-21-diary//post/2025-11-22-diary//post/2025-11-23-diary//post/2025-11-24-diary//post/2025-11-25-diary//post/2025-11-26-diary//post/2025-11-27-diary//post/2025-11-28-diary//post/2025-11-29-diary//post/2025-11-30-diary//post/2025-12-01-diary//post/2025-12-02-diary//post/2025-12-03-diary//post/2025-12-04-diary//post/2025-12-05-diary//post/2025-12-06-diary//post/2025-12-07-diary//post/2025-12-08-diary//post/2025-12-09-diary//post/2025-12-10-diary//post/2025-12-11-diary//post/2025-12-12-diary//post/2025-12-13-diary//post/2025-12-14-diary//post/2025-12-15-diary//post/2025-12-16-diary//post/2025-12-17-diary//post/2025-12-18-diary//post/2025-12-19-diary//post/2025-12-20-diary//post/2025-12-21-diary//post/2025-12-22-diary//post/2025-12-23-diary//post/2025-12-24-diary//post/2025-12-25-diary//post/2025-12-26-diary//post/2025-12-27-diary//post/2025-12-28-diary//post/2025-12-29-diary//post/2025-12-30-diary//post/2025-12-31-diary//post/2026-01-01-diary//post/2026-01-02-diary//post/2026-01-03-diary//post/2026-01-04-diary//post/2026-01-05-diary//post/2026-01-06-diary//post/2026-01-07-diary//post/2026-01-08-diary//post/2026-01-09-diary//post/2026-01-10-diary//post/2026-01-11-diary//post/2026-01-12-diary//post/2026-01-13-diary//post/2026-01-14-diary//post/2026-01-15-diary//post/2026-01-16-diary//post/2026-01-17-diary//post/2026-01-18-diary//post/2026-01-19-diary//post/2026-01-20-diary//post/2026-01-21-diary//post/2026-01-22-diary//post/2026-01-23-diary//post/2026-01-24-diary//post/2026-01-25-diary//post/2026-01-26-diary//post/2026-01-27-diary//post/2026-01-28-diary//post/2026-01-29-diary//post/2026-01-30-diary//post/2026-01-31-diary//post/2026-02-01-diary//post/2026-02-02-diary//post/2026-02-03-diary//post/2026-02-04-diary//post/2026-02-05-diary//post/2026-02-06-diary//post/2026-02-07-diary//post/2026-02-08-diary//post/2026-02-09-diary//post/2026-02-10-diary//post/2026-02-11-diary//post/2026-02-12-diary//post/2026-02-13-diary//post/2026-02-14-diary//post/2026-02-15-diary//post/2026-02-16-diary//post/2026-02-17-diary//post/2026-02-18-diary//post/2026-02-19-diary//post/2026-02-20-diary//post/2026-02-21-diary//post/2026-02-22-diary//post/2026-02-23-diary//post/2026-02-24-diary//post/2026-02-25-diary//post/2026-02-26-diary//post/2026-02-27-diary//post/2026-02-28-diary//post/2026-03-01-diary//post/2026-03-02-diary//post/2026-03-03-diary//post/2026-03-04-diary//post/2026-03-05-diary//post/2026-03-06-diary//post/2026-03-07-diary//post/2026-03-08-diary//post/2026-03-09-diary//post/2026-03-10-diary//post/2026-03-11-diary//post/2026-03-12-diary//post/2026-03-13-diary//post/2026-03-14-diary//post/2026-03-15-diary//post/2026-03-16-diary//post/2026-03-17-diary//post/2026-03-18-diary//post/2026-03-19-diary//post/2026-03-20-diary//post/2026-03-21-diary//post/2026-03-22-diary//post/2026-03-23-diary//post/2026-03-24-diary//post/2026-03-25-diary/
  • Less
  • More

2016-12-10: Crystalでシリアルポートに接続するライブラリを作った

この記事はCrystal Advent Calendar 2016の10日目の記事です。9日目はいなかったみたいです、残念。

一行で

Crystalでシリアルポートに接続するライブラリを作ったよ

MakeNowJust/serialport: serial port library for Crystal

本編

流れ

本来ならボクはAdvent CalendarにCrystalでmemcachedを実装したものを記事にして投稿するつもりだったのですが、今一面白い記事が書けそうになかったので挫折しました。Hashの再実装とかしたくない‥‥。

そして今日になって、バイトの最中で微妙にやることがなくなって手持ち無沙汰にしていたところ社長にAdvent Calendarのことを話したら、会社のことを絡めるなら仕事中に書いてもいいと言われたのでどうにかして会社を絡めたネタを考えました。

で、どうしてシリアルポートなのかというと、ボクのバイトしているニャンパスではIoTデバイスの開発もいくつか行なっています。その中にはいくつかシリアルポートを介してPCから信号を受け取り動作するデバイスや、その逆にシリアルポートでデータをPCへと送信デバイスがあります。Crystalを使ってそれらを制御できたら面白そうだったので、これをAdvent Calendarのテーマとしようと決めました。

それで少し調べてみたところ、serial.crというlibserialportのバインディングらしき何かは見つけたのですが、どうもcrystal-libを適用する前のコードしか置かれていなくて全く使いものにならなそうなので、諦めて自作することにしたという次第です。というかこのMacBook Airにはlibserialportはインストールされていません。

どんなライブラリを作るか考える

CrystalにはIO::FileDescriptorというクラスがあります。これはファイルデスクリプタをラップしてIOクラスを実装したものです。つまり、ファイルデスクリプタがあればgetsメソッドや<<メソッドを使うことができるようになります。
(余談ですが、このIO::FileDescriptorクラスはFileクラスやSocketクラスのスーパークラスです)

IOクラスを実装しているので、to_sに渡したりto_jsonに渡したりと様々なことができるようになります。IOクラスは本当にCrystalの骨幹をなしています。IOクラスを制するものはCrystalを制すると言っても過言ではないでしょう。Crystalにおけるリバウンドであるというわけです。

また、C言語でシリアルポートを扱う際にはopen関数で/dev以下にあるデバイスファイルをオープンし、シリアルポート独特の設定をしたファイルデスクリプタに対してwritereadをするみたいです。ちょうどいいですね。

というわけで、IO::FileDescriptorを継承し、initializeでファイルデスクリプタを設定してsuperに渡すようなクラスを作ることにしました。

IO::FileDescriptorを継承したクラスの作り方

基本的にはこんな形になります。

class HogeFile < IO::FileDescriptor
  def initialize(path : String)
    # C言語の`open`関数を呼び出して、ファイルデスクリプタを得る
    # 引数の`path`に対して`check_no_null_byte`を呼び出して、C言語に渡しても問題のない文字列かチェックしている
    fd = LibC.open(path.check_no_null_byte, LibC::O_RDWR)

    # C言語の関数は失敗したからと言って例外を投げてくれないので、自前でチェックする必要がある
    if fd < 0
      raise Error.new "cannot open '#{path}'"
    end

    # (この辺で各クラス固有のファイルデスクリプタの設定をする)

    # `IO::FileDescriptor`をさっき得たファイルデスクリプタで初期化する
    super fd
  end
end

事実、これだけのコードでFileクラスのようにファイルを読み書きできるようになります。

そして、実際のSerialPortクラスはこうなりました。

class SerialPort < IO::FileDescriptor
  def initialize(@path : String, baudrate : Termios::BaudRate, blocking = false)
    oflag = LibC::O_RDWR | LibC::O_NOCTTY | LibC::O_SYNC | LibC::O_CLOEXEC

    fd = LibC.open(path.check_no_null_byte, oflag)
    if fd < 0
      raise Errno.new("Error opening serial port '#{path}'")
    end

    self.sync = true # no buffering

    set_interface_attributes(fd, baudrate, blocking)
    super fd, blocking
  end
end

さきほどのHogeFileと比べると、initializeの引数にbaudrateblockingが増えていることが分かります。baudrateはシリアルポートのボードレートを指定するもので、blockingIO::FileDescriptorにも元から存在する引数で、IOがブロックするかどうかを指定します。基本的には非同期の方がいいでしょう。

また、途中にself.sync = trueという行がありますが、これはIO::Bufferedincludeしているので、バッファリングしないようにするためです。シリアルポートでバッファリングされるといつデータが送られるのか分からなくなってちょっと困ります。

最後の方にset_interface_attributesというメソッドの呼び出しがありますが、これが今回の実装のキモです。ここでfdをシリアルポート向けに設定します。

set_interface_attributesの実装

set_interface_attributesはC言語でシリアルポートに接続するコードを参考にして、次のようになりました。
(もちろんこのコードは実際にはclass SerialPort < IO::FileDescriptorの中にあります)

  private def set_interface_attributes(fd, baudrate, blocking)
    if LibC.tcgetattr(fd, out mode) != 0
      raise Error.new("initialize serial port: tcgetaddr")
    end

    LibC.cfsetospeed(pointerof(mode), baudrate)
    LibC.cfsetispeed(pointerof(mode), baudrate)

    mode.c_cflag |= (Termios::ControlMode::CLOCAL |
                     Termios::ControlMode::CREAD).value # ignore modem controls
    mode.c_cflag &= ~Termios::ControlMode::CSIZE.value
    mode.c_cflag |= Termios::ControlMode::CS8.value     # 8-bit characters
    mode.c_cflag &= ~Termios::ControlMode::PARENB.value # no parity bit
    mode.c_cflag &= ~Termios::ControlMode::CSTOPB.value # only need 1 stop bit
    mode.c_cflag &= ~LibC::CRTSCTS                      # no hardware flowcontrol

    mode.c_iflag &= ~(Termios::InputMode::IGNBRK |
                      Termios::InputMode::BRKINT |
                      Termios::InputMode::PARMRK |
                      Termios::InputMode::ISTRIP |
                      Termios::InputMode::INLCR |
                      Termios::InputMode::IGNCR |
                      Termios::InputMode::ICRNL |
                      Termios::InputMode::IXON).value
    mode.c_lflag &= ~(Termios::LocalMode::ECHO |
                      Termios::LocalMode::ECHONL |
                      Termios::LocalMode::ICANON |
                      Termios::LocalMode::ISIG |
                      Termios::LocalMode::IEXTEN).value
    mode.c_oflag &= ~Termios::OutputMode::OPOST.value

    mode.c_cc[LibC::VMIN] = blocking ? 1_u8 : 0_u8
    mode.c_cc[LibC::VTIME] = 5_u8

    if LibC.tcsetattr(fd, Termios::LineControl::TCSANOW, pointerof(mode)) != 0
      raise Error.new("initialize serial port: tcsetattr")
    end
  end

特徴的なのは、冒頭でLibC.tcgetattr(fd, out mode)outパラメーターを使ってmodeを受け取っていることでしょうか。

さて、ここで一つ問題が発生しました。Crystalは標準ライブラリにtermios.hに対するバインディングをいくつか提供しているのですが、今回利用したい全てに対応しているというわけではありません。
しかし、幸いにもCrystalにはC言語の関数や定数を簡単に定義できる仕組みがあります。さくっとlibc.crというファイルを作って、足りない関数や定数を補うことにしました。
(定数の値や関数のプロトタイプ宣言などは、実際にヘッダファイルを見たりmanページを参考にして記述しました)

lib LibC
  # from fnctl.h
  O_NOCTTY = 0x20000

  # from termios.h
  CCTS_OFLOW = 0x00010000
  CRTS_IFLOW = 0x00020000
  CDTR_IFLOW = 0x00040000
  CDSR_OFLOW = 0x00080000
  CCAR_OFLOW = 0x00100000
  CRTSCTS    = (CCTS_OFLOW | CRTS_IFLOW)

  VTIME = 17

  fun cfsetospeed(termios_p : Termios*, speed : SpeedT) : Int
  fun cfsetispeed(termios_p : Termios*, speed : SpeedT) : Int

  fun tcdrain(fd : Int) : Int
end

さりげなくinitializeの方にも本来は定義されていない定数があったことがバレてしまいましたね。

とりあえず、こんな風にしてCrystalでシリアルポートに接続するライブラリは完成しました、一応。

それで、作ったもの。

これです。

なんなのか‥‥。

ちなみに、これを動かしたCrystalのスクリプトはこんな感じです。

require "./src/serialport"

serial = SerialPort.new "/dev/なんとかかんとか", Termios::BaudRate::B9600

i = 0
loop do
  serial << "v #{i}"
  i = (i + 100) % 1024
  sleep 0.1
end

シリアルポートに「v 0-1023の数値」の形式で書き込むと数値に対応した位置までサーボモーターが動くというものが会社にあったので、それを動かしまくってみました。なんともシュールな光景になったと思います。このデバイスを作った人にはサーボモーターに負荷がかかるからやめろと言われました‥‥。ごめんなさい。

あと、シリアルポートを読み取る方のプログラムも書いたことには書いたのですが、センサーが不調だったようで上手く結果が取れなかったので非公開としておきます。

今後の課題とか

  • Linuxや他の環境でも動くようにする(手元で動けばいいという気持ちで作ったから他の環境のことを考えていない。多分Cバインディング周りで、定数の値が違うからコケる)
  • テストを書く(どうやって書けばいいんすか‥‥)
  • シリアルポートに対してもっと細かく設定できるようにする(この辺は他のライブラリを参考にしたい)
  • というかもっとシリアルポートについて理解を深めろ(ぶっちゃけよく分かってない)

誰かLinuxに移植したりテスト書いといたりしといてください。Pull Requestはいつでも待っています。

最後に

CrystalではC言語と簡単に連携できると言いますが、その例は大抵がC言語の関数を呼び出すところで終わってしまいます。しかし、本来ならばその先に、C言語の関数をCrystalらしく使えるようにする、という作業が残っているはずです。今回の記事では、その部分に少しフォーカスを当ててみたつもりです。

あと勢いでシリアルポートに繋ぐライブラリを作ってみましたが、これって使い途あるんでしょうか? Crystalでシリアルポートにつなぎたい機会ってあります? スクリプト言語の方がよくありません? あ、でも組み込みで動かすときとかだと話は別なのかな‥‥。いや、Crystal組み込みで動かねえし‥‥。

とまあ使い途がよく分からなくなっているのですが、Crystalでシリアルポートに繋ぎたくなったらぜひ使ってみてください。

あと、もしもこの記事でニャンパスに興味を持った方がいたら、Halake(ニャンパスが運営しているコワーキングスペースです)に来たときに「MakeNowJustの記事を読んだよ」と言ってください。そうするとボクの給料が増えて日本経済を回すきっかけになる可能性があります。いや、ねーよ。

最後にどうしてボクがひたすらCrystalにコントリビュートしているのか書いたらかっこいいなとか思ったのですが、特に理由がありませんでした。強いて言えばpineさんがcrystal-jpとかやってるのについかっとなってやったとかそんな感じでしょうか(本当です)。完全にキレる若者ですね。若者ってこえー。

というわけでここまで読んでいただきありがとうございました。ありがとうございます。


2016-12-11: ノイシュバンシュタイン城というボドゲで遊んだ

2016-12-10: Crystalでシリアルポートに接続するライブラリを作った

2016-12-10: bashcachedにPull Requestがきたり、crystal init libで生成されるshard.ymlを直したり