ファームウェアエンジニアの中林 (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でネットワークプログラミングするのが面白そう、と思えるエンジニアを募集しています。
カジュアル面談も実施していますので、興味がある方はお気軽にご応募ください!