ファームウェアエンジニアの中林 (id:tomo-wait-for-it-yuki) です。大好評の組込みRustで使えるライブラリをゆるく紹介していく「ぶらり組込みRustライブラリ探索の旅」シリーズ、第2弾はATコマンドクライアントライブラリのatat
です。
私ごとですが、拙著「基礎から学ぶ組込みRust」ではネットワーク接続して遊ぶ、という内容が書けておらず、ずっとリベンジの機会を伺っています。atat
は組込みRustでネットワーク接続して遊ぶ上で有力な選択肢になりそうなcrateです。
atat
本エントリ内で紹介する使い方や内部実装は、v0.16.1をもとにしています*1。
https://docs.rs/atat/0.16.1/atat/index.html
特徴
atat
はno_std
環境で使用できるATコマンドクライアントライブラリです。embedded-halのtraitを使って、ATコマンドベースのシリアルモジュール制御ドライバを書くことができます。
しかもatatを使うと動的なメモリ確保なしに、可変長のATコマンドを送受信できます。またATコマンドには、URC (Unsolicited Response Code) と呼ばれる非同期イベントがありますが、こちらもハンドリングが可能です。
メモリの動的確保なしに可変長のデータを送受信するために、heaplessと前回紹介したbbqueueをうまく活用しています。この活用が見事で組込みRustでメモリ確保どうすれば良いねん?に対する1つの解答だと感じたので、そのあたりも紹介していきます。
ATコマンド
ATコマンドはHayes社が自社のモデムを制御するために開発したコマンド体系です。コマンドの多くがAT
で始まることから、ATコマンドと呼ばれています。
例えば、ATD
はダイアルするコマンド、ATE
はクライアントが発行したATコマンドをエコーするか制御するコマンド、といった具合です。
2022年現在でも、セルラーモジュールの制御や、WiFi通信モジュール*2の制御に使われています。多くの場合UART
を始めとするシリアル通信でコマンドを送受信します。ATコマンド自体についての説明は他リソースを参照ください*3。
atatのソフトウェア構成とおおよその使い方
主要なモジュール
主要なモジュールは以下の3つです。
モジュール | 概要 |
---|---|
client | ATコマンドクライアント。ATコマンドをシリアライズして送信したり、レスポンスやURCをデシリアライズする。 |
ingress manager | レスポンスやURCの受信を担当する。レスポンスやURCを受信したらbbqueueを通してclientに送る。 |
digester | ingress managerの一部。受信データをレスポンスやURC単位のバイト列に分割する(受信データの詳細は見ない)。 |
これらのモジュールは、次のシーケンスに沿って使用します。
client
はATコマンドをシリアライズして送信する- 受信スレッド (もしくは受信割り込み) で、
ingress manager
に受信データを書いていき、digester
がレスポンス / URC単位のバイト列に分割する ingress manager
はdigester
が分割したレスポンス / URCをclient
に送るclient
は受信したレスポンス / URCをデシリアライズする
使い方
おおよそのデータの流れは次の通りです。client
やingress manager
, digester
の詳細は追って説明していきますので、ここではデータの流れをイメージできればOKです。
図中のAT command module
が制御対象のモジュールであり、シリアル通信でATコマンドとレスポンス / URCをやり取りします。
main thread
のclient
はRustの構造体として表現されたATコマンドをバイト列にシリアライズしてモジュールに送信します。また、ingress manager
がqueueに詰めたレスポンス / URCを受け取り、そのバイト列をRustの構造体にデシリアライズします。
rx thread
はモジュールからデータを受信して、レスポンス / URCの単位でバイト列を分割し、spsc (single producer single consumer) のqueueに詰めていきます*4。
これをコードレベルに落とすと次のようになります (このコードはコンパイルできません) 。これが理解できると、あとは応用が色々あるだけです。
// レスポンスをデシリアライズした結果を格納する構造体 (今回の場合、空) #[derive(AtatResp)] pub struct NoResponse; // ATコマンドにシリアライズされる構造体 #[derive(AtatCmd)] #[at_cmd("", NoResponse)] pub struct AT; fn main() { // client と ingress manager を初期化する // 中略... // Launch reading thread std::thread::Builder::new() .spawn(move || loop { let mut buffer = [0; 32]; // モジュールからシリアル通信でデータを受信する match serial_rx.read(&mut buffer[..]) { Ok(bytes_read) => { // ingress manager に受信データを書き込む ingress.write(&buffer[0..bytes_read]); // 受信データをレスポンスやURCの単位に分割して、clientに送る ingress.digest() } Err(e) => { // error handling }, } }) .unwrap(); // ATコマンド (`AT`) をシリアライズ & 送信して、受信 & デシリアライズしたレスポンス (`NoResponse`) を得る let res: NoResponse = client.send(&AT{})?; }
コマンド例: AT
先程のサンプルコードではAT
というコマンドを例にしました。このコマンドはクライアントが対向モジュールと通信できているかどうか検査するためのテストコマンドとして利用できます。AT
に対してはレスポンスはなく、OK
というレスポンスコードだけが返ってきます。
コマンド | レスポンス / URC | 説明 |
---|---|---|
AT | テストコマンドをクライアントからモジュールに送信 | |
OK | レスポンスコード |
先程のコードでこのことが表現されています。構造体AT
は"AT"
という文字列にシリアライズされて、そのレスポンスはNoRenponse
になります。このコードでは、#[derive(AtatCmd)]
によってシリアライズ / デシリアライズの処理が自動で実装されています。
#[derive(AtatResp)] pub struct NoResponse; #[derive(AtatCmd)] #[at_cmd("", NoResponse)] pub struct AT;
コマンド例: AT+USORD
もう1つ具体的なATコマンドの例を用いて、それぞれの役割をもう少し説明します。ubloxのATコマンドでsocketからデータを読み込むAT+USORD
コマンドの実行を考えます。AT+USORD
コマンドを使うシーケンスは次の通りです。
コマンド | レスポンス / URC | 説明 |
---|---|---|
AT+USORD=0,4 | socket 0から4バイト読み出すATコマンドを送信 | |
+USORD: 0,4,"90030002" | 4バイトのデータを読み出す | |
OK | レスポンスコード |
このコマンドはatatで次のように実装できます。"AT"
は常にデフォルトのプレフィックスとして追加される*5ので、"+USORD"
という文字列を追加しています。
/// 25.12 Read Socket Data +USORD #[derive(Clone, AtatResp)] pub struct SocketData { pub socket: usize, pub length: usize, pub data: Option<String<SIZE>>, } #[derive(Clone, AtatCmd)] #[at_cmd("+USORD", SocketData)] pub struct ReadSocketData { pub socket: usize, pub length: usize, }
ATコマンドを送信して、レスポンスを受け取るコードは次のようになります。
let cmd = ReadSocketData { socket: 0, length: 4 }; let res: SocketData = client.send(&cmd)?;
ここでReadSocketData
のコマンドを送信してから、ScoketData
の値を得るまでの工程をもう少し詳細に見てみましょう。
- モジュールから
AT+USORD
コマンドのエコー、レスポンス、レスポンスコードを受信します*6 ingress manager
の受信バッファに受信データを保持しますdigester
に受信バッファの内容を渡して、レスポンスを作りますdiester
はエコーを削除し、レスポンスコードOK
までに受信した内容をレスポンスとみなして、レスポンス+USORD: 0,4,"90030002"
を返しますingress manager
はdigester
から返ってきたレスポンスをresponse queue
に書き込みます*7main thread
側のclient
ではresponse queue
に書かれたレスポンスを読み込みます*8- レスポンスはこのままでは文字列 (バイト列) なので、デシリアライズしてRustの構造体 (
SocketData
) として表現された値にします
少し細かい話ですが、client.send()
はジェネリック関数になっており、各ATコマンドに対して、異なる型のレスポンスを受け取ることができます。また、非同期イベントのURCについても、基本的な動作はレスポンス受信時と同じです。
atatの使い方を知るのに最も参考になるのは、atatの開発者が開発しているublox cellular module向けのクライアント実装です。より詳細に実践的な使い方を知りたい場合は、ublox-cellular-rs
を読んでみてください。
dive into atat
では、atat
の実装を深堀りしていきます。#[derive(AtatCmd)]
の裏で何が起こっているのか、を把握しましょう。
crates
atat
は次の3つのcrateから構成されています。
crate | 説明 |
---|---|
atat | 中心となるcrateでコアとなるtraitや、ingress manager, digester, clientのデフォルト実装があります |
serde_at | atatのserde実装で、Rustの構造体⇔ATコマンド文字列のシリアライズ、デシリアライズを実装します |
atat_derive | atatのtraitを自動導出するderive macroが定義されています |
リストの上から順番に、ボトムアップで説明します。
atat & traits
atatで定義されている主なtraitの一覧です。
trait | 概要 |
---|---|
AtatCmd | 各ATコマンド型が実装しなければならないトレイトです。#[derive(AtatCmd)] で実装することもできます。 |
AtatResp | レスポンス型が実装しなければならないマーカートレイトです。 |
AtatUrc | URC型が実装しなければならないトレイトです。 |
AtatLen | #[derive(AtatCmd)] を使う時に、シリアライズ後のATコマンドの長さを、コンパイル時に計算するためのトレイトです。 |
上記の内、AtatCmd
とAtatLen
についてさらに説明します。
AtatCmd
AtatCmd
はatat/src/traits.rs
に以下の通り定義されています (説明を簡略化するため抜粋しています) 。
pub trait AtatCmd<const LEN: usize> { /// The type of the response. Must implement the `AtatResp` trait. type Response: AtatResp; // 中略 /// Return the command as a heapless `Vec` of bytes. fn as_bytes(&self) -> Vec<u8, LEN>; /// Parse the response into a `Self::Response` or `Error` instance. fn parse(&self, resp: Result<&[u8], InternalError>) -> Result<Self::Response, Error>; }
as_bytes()
はこの構造体をATコマンドのバイト列にシリアライズするメソッドです。heapless:Vec
はスタック上に最大容量LEN
の可変長配列を作る構造体で、これがas_bytes()
の戻り値型になっています。つまり、AtatCmd
を実装する構造体をシリアライズすると、最大LEN
バイトのバイト列になる、ということです。シリアライズ処理は、手動でゴリゴリ書くこともできますし、serde_at
を使ってserde
のシリアライズに乗っかることもできます。
ジェネリックパラメータ<const LEN: usize>
は、シリアライズ後のバイト列が最大で何バイトになるか、を示すパラメータになっています。これは手動で計算するか、#[derive(AtatCmd)]
でコンパイル時に計算させることができます。
parse()
はレスポンスのバイト列を構造体にデシリアライズするメソッドです。デシリアライズ処理もシリアライズ処理と同様に、手動で書くことも、serde_at
を使うこともできます。
例として、手動でAT
コマンドにAtatCmd
を実装すると次のようになります。
use heapless::Vec; pub struct NoResponse; impl AtatResp for NoResponse{}; pub struct At{}; // 改行コード含めて`"AT\r\n"`の4バイトになるので、`LEN`は`4`です impl AtatCmd<4> for At { type Response = NoResponse; fn as_bytes(&self) -> Vec<u8, 4> { // 手動でシリアライズ処理を書いています Vec::from_slice(b"AT\r\n").unwrap() } fn parse(&self, resp: Result<&[u8], InternalError>) -> Result<Self::Response, Error> { let _ = resp?; // レスポンスはないので無視する Ok(NoResponse) } }
AT
コマンドの場合はレスポンスがないため、parse()
がほとんど空です。もしレスポンスが返ってくるコマンドのデシリアライズ処理を書くのであれば、次のように書くこともできます。
fn parse(&self, resp: Result<&[u8], InternalError>) -> Result<Self::Response, Error> { let bytes = resp?; if !bytes.starts_with(b"+USORD:") { return Error::Mismatched; } let body = &bytes[b"+recv:".len()..]; // バイト列をパースする (詳細は省略) Ok(SocketData{ /* ... */ }) }
AtatLen
AtatCmd
にはシリアライズ後のバイト数をパラメータLEN
として与える必要があります。AT
コマンド程度であれば簡単に計算できますが、ATコマンドのパラメータが多かったり、構造体を持つ構造体にAtatCmd
トレイトを実装したい、となるとLEN
を計算するのが大変です。
そこでAtatLen
トレイトの登場です。AtatLen
はその型をATコマンドにシリアライズした時に、何バイトのバイト列になるか、を示します。AtatLen
はatat/src/derive.rs
に定義されており、その定義は非常に単純です。
pub trait AtatLen { const LEN: usize; }
Rustのプリミティブ型やheapless::String<L>
、heapless::Vec<T, L>
などatat
を使う上で頻出の型に対して、AtatLen
が実装されています。
macro_rules! impl_length { ($type:ty, $len:expr) => { #[allow(clippy::use_self)] impl AtatLen for $type { const LEN: usize = $len; } }; } impl_length!(char, 1); impl_length!(bool, 5); // 中略 impl_length!(f32, 42); impl_length!(f64, 312); impl<const T: usize> AtatLen for String<T> { const LEN: usize = T; } impl<T: AtatLen> AtatLen for Option<T> { const LEN: usize = T::LEN; } impl<T: AtatLen> AtatLen for &T { const LEN: usize = T::LEN; } impl<T, const L: usize> AtatLen for Vec<T, L> where T: AtatLen, { const LEN: usize = L * <T as AtatLen>::LEN; }
AtatLen
があることで、構造体をシリアライズした結果が何バイトになるか、簡単に計算できます。
const ATAT_READSOCKETDATA_LEN = usize::LEN + usize::LEN; pub struct ReadSocketData { pub socket: usize, pub length: usize, }
ATAT_READSOCKETDATA_LEN
に"AT+USORD"
+ "\r\n"
の長さを加えれば計算完了です!heapless::Vec
を使う上で、事前に最大長を指定しなければならない、という面倒臭さがこれできれいに解消されています。
serde_at
AtatCmd
トレイトを実装する際、as_bytes()
で構造体をバイト列にシリアライズしていました。またparse()
でバイト列を構造体にデシリアライズしていました。このシリアライズ / デシリアライズ処理を簡単化するためにserde
実装が提供されています。それがserde_at
です。
構造体にserde::Serialize
トレイトが実装されていれば、serde_at::to_vec()
で構造体をバイト列にシリアライズすることができます。同様に、構造体にserde::Deserialize
トレイトが実装されていれば、serde_at::from_slice()
でバイト列を構造体にデシリアライズできます。先程のAT+USORD
を例にすると、次のように書けます。
// レスポンスは`serde::Deserialize`トレイトを実装 #[derive(serde::Deserialize)] pub struct SocketData { pub socket: usize, pub length: usize, pub data: Option<String<SIZE>>, } const ATAT_READSOCKETDATA_LEN = usize::LEN + usize::LEN; // ATコマンドは`serde::Serialize`トレイトを実装 #[derive(serde::Serialize)] pub struct ReadSocketData { pub socket: usize, pub length: usize, } // シリアライズ後のバイト数 = 構造体をシリアライズしたバイト数 + 固定文字列のバイト数 impl AtatCmd<{ ATAT_READSOCKETDATA_LEN + b"AT+USORD\r\n".len() }> for ReadSocketData { type Response = SocketData; fn as_bytes(&self) -> Vec<u8, { ATAT_READSOCKETDATA_LEN + b"AT+USORD\r\n".len() }> { // `serde_at::to_vec()`でATコマンドのバイト列にシリアライズ match atat::serde_at::to_vec(self, "+USORD", atat::serde_at::SerializeOptions::default() { Ok(s) => s, Err(_) => panic!("Failed to serialize command") } } fn parse(&self, res: Result<&[u8], atat::InternalError>) -> Result<Self::Response, atat::Error> { match res { Ok(resp) => atat::serde_at::from_slice::<Self::Response>(resp).map_err(|e| { atat::Error::Parse }), Err(e) => Err(e.into()) } } }
パターンが見えてきましたか?
- レスポンスは
serde::Deserialize
を実装する - ATコマンドは
serde::Serialize
を実装する - シリアライズ後のバイト数は、
AtatLen
と固定文字列のバイト数とを使ってコンパイル時に計算する as_bytes()
ではserde_at::to_vec()
でシリアライズするparse()
ではserde_at::from_slice()
でデシリアライズする
atat_derive
ここまでで、構造体をシリアライズした後のバイト数の計算、およびserde
を使ったシリアライズ / デシリアライズ処理が、完全にパターン化されました。そこで、atat_derive
でこれらのパターンを自動的に導出できるようにします。
#[derive(AtatCmd)]
は、構造体にserde::Serialize
トレイト、AtatCmd
トレイト、AtatLen
トレイトを実装します。AtatCmd
トレイトの実装にあたっては、これまで説明したAtatLen
によるシリアライズ後のバイト数の計算、および、serde_at
によるシリアライズ / デシリアライズを行います。
#[derive(AtatResp)]
は、構造体にserde::Deserialize
トレイトとAtatResp
トレイトを実装します。レスポンスの構造体にserde::Deserialize
が実装されているため、AtatCmd
のparse()
でserde_at
によるデシリアライズが可能となります。
Appendix
digester
Appendixその1は、digesterについてです。モジュールから受信したバイト列を、レスポンス / URC単位に分割する、という役割を果たしています。その内部ではパーサーコンビネータnomを使用しています。
さらにデフォルトのdigester実装では対応できないパターンも処理できるように、カスタマイズしたパーサーをdigesterに登録できます。例えば、デフォルトのdigester実装では"ERROR: <error message>"
のようなエラーレスポンスは処理できません。そこでカスタムパーサーを用意し、digester初期化時に与えます。
use nom::{ combinator::recognize, bytes::streaming::{tag, take_until}, character::complete::line_ending, }; /// 正規表現 "\r\nERROR:(.*)\r\n" にマッチする /// "\r\nERROR"と"\r\n"を除いたエラーメッセージを含むバイト列を返す pub fn custom_error_parser(buf: &[u8]) -> Result<(&[u8], usize), atat::digest::ParseError> { let (i, tag) = recognize(tag("\r\nERROR:"))(buf)?; let (i, error_msg) = recognize(take_until("\r\n"))(i)?; let (_, end_line) = line_ending(i)?; Ok((error_msg, tag.len() + error_msg.len() + end_line.len())) } // digesterの初期化 let digester = atat::AtDigester::new().with_custom_error(custom_error_parser); let (client, mut ingress) = atat::ClientBuilder::new(serial_tx, timer, digester, config).build(queues);
custom-error-messages
featureを有効にするとatat::Error::CustomMessage(Vec<u8, 64>)
として、カスタムパーサーで返したバイト列を受け取ることができます。
logging
ログ周りも整備されており、Rustのlogを使っています。フォーマット文字列を使うところは組込み向けの軽量フォーマット文字列crateであるdefmtを選択的に使うことができます。下のようにfeatureで切り替えできるようになっています。
#[cfg(any(feature = "defmt", feature = "log"))] match &resp { Ok(r) => { if r.is_empty() { debug!("Received OK") } else { debug!("Received response: \"{:?}\"", LossyStr(r.as_ref())); } } Err(e) => { error!("Received error response {:?}", e); } };
おわりに
atat
はno_std
で使えるATコマンドクライアントライブラリです。本エントリではatatのおおよその構成や使い方、主要な実装を紹介しました。
ESP32
シリーズのようなWi-Fiモジュールのドライバをatat
で書けば、組込みRustでネットワーク接続して存分に遊ぶことができそうです。atatでドライバを書いたあとは、embedded-nalを抽象レイヤーとして繋ぎ込むと良いでしょう。
そして組込みRustでネットワークプログラミングライフを満喫しましょう!
Natureでは組込みRustでネットワークプログラミングするのが面白そう、と思えるエンジニアを募集しています。
カジュアル面談も実施していますので、興味がある方はお気軽にご応募ください!