macOSからPi Picoを使う - その13 [Pi Pico]
ということで、Pi PicoとmacOSをUSBを通して通信するテストアプリを作ってみることにした。Pi Picoはstdio_usbの入出力機能を使って、macOS側はCDCデバイスとしてのデバイスファイル経由でお互い文字列をやり取りする方式でやってみる。特有の制限を回避する必要があって、どうも美しくないけど....
ずっと以前にRaspberry Pi ZeroWを使ってパワーメータを作った。これをPi Picoで動かしてみる。回路基板は流用できる前提で、Pi ZeroWはローカルなWiFiでサーバとして動作して、macOSがクライアントとして接続してデータを受け取る、というやりかただった。
DHCP環境なので、アドレスが決め打ちできない。そこでまずクライアントはブロードキャストしてサーバを探す。サーバはブロードキャストに応答してお互い必要な相手を知って、そのあとTCPで接続し直してデータをやり取りする、という方法。これは他のRaspberry PiとmacOSとでも同じ方法を使った。Raspberry PiもmacOSも全く同じsocketのAPIが使えるので簡単だった。
Pi Picoにするとその手間は完全に省ける。
しかしCDCでは制限も多いようである。
Pi Pico側はstdinからは1キャラクタずつ、stdoutへはprintfしかsdkには公開されていない。そしてどうもC文字列が前提になっているようで、たとえばNULLキャラクタ(0x00)は受け取りも送れもしないようである。
また、Pi Pico側のC文字列のデリミタ(改行文字)がCR(0x0D)になってるみたいで、そのままmacOSで読むとfgets()なんかが正常に機能しない。CRからLF(0x0A)に変更できるようなんだけどうまくいかない。
また、大きな問題としてmacOS側でコマンドラインではなくウィンドウを持った普通のアプリとしてビルドしようとすると、sandboxのおかげで/dev/以下のデバイスファイルにアクセスできない。特定のデバイスファイルのアクセスを許すようなsandboxの設定はないので、sandboxそのものを外したアプリにしないといけない。現状macOS11.4ではsandboxを外したアプリもビルドできて、実行もできるけど、いつまでそういうアプリが許されるかわからない。いずれ比較的早く禁止されるのではないかと思っている。
従っていずれはTinyUSBとIOUSBHost(あるいはlibusb)でプログラムしないといけなくなるようである。とりあえず現状でお互いstdioを使えるようにして、それがちゃんと動いてから考えることにする。
stdioを使う限りUSBには文字列しか流せない、として通信のプロトコルを決めてしまおう。OSIモデルで言うとプレゼンテーション層だな。
まず純粋なテキストはそのまま送ればいい。そしてデリミタを決めてそのデリミタが来るまでが1つの文字列だと言うことにする。
単純なスカラデータも送れるようにしたい。printf文字列で送ってatod()とかで戻してもいいんだけど、さすがに冗長なのでバイナリをテキストエンコードしよう。すぐ思い浮かぶのはBASE64とかuuencodeだけど、自前で実装するにはめんどくさい。2割3割をケチってもしょうがないので、hexでエンコードしよう。USB上ではサイズが倍になるけど、エンコードデコードは簡単で、macOSとPi Picoは同じlittle endianなのでバイナリはすべてフラットに1バイトを2文字に置き換えるだけのコーデックにする。
通信の最初は内容を表すキャラクタ1文字で始めることにする。2文字目に長さを決められる情報を付加する。スカラデータは要素数1の配列ということにしよう。charとshortはintに符号拡張することにしよう。配列の要素数は16進をそのまま進めて36進で表そう。十分な長さとは言えないけど、Pi Picoを使う上ではそれほど困らないだろう。
つまりこんな感じ。
文字列以外はバイナリをhexキャラクタにエンコードする。queryというのは内部状態の確認、stateは内部状態の変更で、2文字目で対象を指定する。
binaryというのはデータをバイト列とみなしてそれをエンコードする。2文字目は長さで、32ビット境界にアラインされることを前提に4バイトの倍数で指定する。したがって最大サイズは144バイトで、エンコードした結果のバイト数は最大288になる。definite structというのはPi Pico側とmacOS側でサイズが同じになるようにした構造体で、構造体に通し番号でもつけて、それを指定することで長さを確定させる。
binaryやdefinite structに文字列が含まれていても、バイト列としてそのままエンコードする。そうしないとめんどくさい。
14 お試し実装
昔話はいいとして、これまでの前知識を得てこの方針でいけるかどうか試してみる。ずっと以前にRaspberry Pi ZeroWを使ってパワーメータを作った。これをPi Picoで動かしてみる。回路基板は流用できる前提で、Pi ZeroWはローカルなWiFiでサーバとして動作して、macOSがクライアントとして接続してデータを受け取る、というやりかただった。
DHCP環境なので、アドレスが決め打ちできない。そこでまずクライアントはブロードキャストしてサーバを探す。サーバはブロードキャストに応答してお互い必要な相手を知って、そのあとTCPで接続し直してデータをやり取りする、という方法。これは他のRaspberry PiとmacOSとでも同じ方法を使った。Raspberry PiもmacOSも全く同じsocketのAPIが使えるので簡単だった。
Pi Picoにするとその手間は完全に省ける。
14.1 macOSアプリでの問題
USB1.1なので通信エラーやパケットの着順反転なんかは考えなくていい。しかしCDCでは制限も多いようである。
Pi Pico側はstdinからは1キャラクタずつ、stdoutへはprintfしかsdkには公開されていない。そしてどうもC文字列が前提になっているようで、たとえばNULLキャラクタ(0x00)は受け取りも送れもしないようである。
また、Pi Pico側のC文字列のデリミタ(改行文字)がCR(0x0D)になってるみたいで、そのままmacOSで読むとfgets()なんかが正常に機能しない。CRからLF(0x0A)に変更できるようなんだけどうまくいかない。
また、大きな問題としてmacOS側でコマンドラインではなくウィンドウを持った普通のアプリとしてビルドしようとすると、sandboxのおかげで/dev/以下のデバイスファイルにアクセスできない。特定のデバイスファイルのアクセスを許すようなsandboxの設定はないので、sandboxそのものを外したアプリにしないといけない。現状macOS11.4ではsandboxを外したアプリもビルドできて、実行もできるけど、いつまでそういうアプリが許されるかわからない。いずれ比較的早く禁止されるのではないかと思っている。
従っていずれはTinyUSBとIOUSBHost(あるいはlibusb)でプログラムしないといけなくなるようである。とりあえず現状でお互いstdioを使えるようにして、それがちゃんと動いてから考えることにする。
14.2 上位層
ということで、まずCDC経由で実装することにする。stdioを使う限りUSBには文字列しか流せない、として通信のプロトコルを決めてしまおう。OSIモデルで言うとプレゼンテーション層だな。
まず純粋なテキストはそのまま送ればいい。そしてデリミタを決めてそのデリミタが来るまでが1つの文字列だと言うことにする。
単純なスカラデータも送れるようにしたい。printf文字列で送ってatod()とかで戻してもいいんだけど、さすがに冗長なのでバイナリをテキストエンコードしよう。すぐ思い浮かぶのはBASE64とかuuencodeだけど、自前で実装するにはめんどくさい。2割3割をケチってもしょうがないので、hexでエンコードしよう。USB上ではサイズが倍になるけど、エンコードデコードは簡単で、macOSとPi Picoは同じlittle endianなのでバイナリはすべてフラットに1バイトを2文字に置き換えるだけのコーデックにする。
通信の最初は内容を表すキャラクタ1文字で始めることにする。2文字目に長さを決められる情報を付加する。スカラデータは要素数1の配列ということにしよう。charとshortはintに符号拡張することにしよう。配列の要素数は16進をそのまま進めて36進で表そう。十分な長さとは言えないけど、Pi Picoを使う上ではそれほど困らないだろう。
つまりこんな感じ。
機能 | header | secound | trailer |
query | Q | ? | - |
state | S | ? | - |
text | T | delimitor | ccccc... delimitor |
integer | i | length | xxxx |
long | I | length | xxxxxxxx |
uinteger | u | length | xxxx |
ulong | U | length | xxxxxxxx |
float | f | length | xxxx |
double | F | length | xxxxxxxx |
binary | b | size/4 | xxxx .... |
definite struct | s | type | xxxx ... |
binaryというのはデータをバイト列とみなしてそれをエンコードする。2文字目は長さで、32ビット境界にアラインされることを前提に4バイトの倍数で指定する。したがって最大サイズは144バイトで、エンコードした結果のバイト数は最大288になる。definite structというのはPi Pico側とmacOS側でサイズが同じになるようにした構造体で、構造体に通し番号でもつけて、それを指定することで長さを確定させる。
binaryやdefinite structに文字列が含まれていても、バイト列としてそのままエンコードする。そうしないとめんどくさい。
2021-06-30 20:35
nice!(0)
コメント(0)
コメント 0