Skip to main content

at_parser_rs/
lib.rs

1/***************************************************************************
2 *
3 * AT Command Parser
4 * Copyright (C) 2026 Antonio Salsi <passy.linux@zresa.it>
5 *
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 2.1 of the License, or (at your option) any later version.
10 *
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 * Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with this library; if not, see <https://www.gnu.org/licenses/>.
18 *
19 ***************************************************************************/
20
21//! AT Command Parser Library
22//!
23//! This library provides a flexible parser for AT commands, commonly used in
24//! embedded systems and communication devices. It supports `no_std` environments.
25//!
26//! # Architecture
27//!
28//! The library is built around three core components:
29//!
30//! - **[`AtParser`](parser::AtParser)** - The main parser that processes AT command strings
31//! - **[`AtContext`](context::AtContext)** - Trait for implementing command handlers
32//! - **[`Args`]** - Structure for accessing command arguments
33//!
34//! # Command Forms
35//!
36//! Supports all standard AT command forms:
37//! - `AT+CMD` - Execute (action without parameters)
38//! - `AT+CMD?` - Query (get current value/state)
39//! - `AT+CMD=?` - Test (get supported values/ranges)
40//! - `AT+CMD=<args>` - Set (configure with parameters)
41//!
42//! # Quick Start
43//!
44//! ```rust,no_run
45//! use at_parser_rs::context::AtContext;
46//! use at_parser_rs::parser::AtParser;
47//! use at_parser_rs::{Args, AtResult, AtError, Bytes};
48//!
49//! const SIZE: usize = 64;
50//!
51//! // 1. Define a command handler
52//! struct EchoModule { echo: bool }
53//!
54//! impl AtContext<SIZE> for EchoModule {
55//!     fn query(&mut self) -> AtResult<SIZE> {
56//!         if self.echo { Ok(Bytes::from_str("1")) } else { Ok(Bytes::from_str("0")) }
57//!     }
58//!     
59//!     fn set(&mut self, args: Args) -> AtResult<SIZE> {
60//!         match args.get(0) {
61//!             Some("0") => { self.echo = false; Ok(Bytes::from_str("OK")) }
62//!             Some("1") => { self.echo = true; Ok(Bytes::from_str("OK")) }
63//!             _ => Err(AtError::InvalidArgs),
64//!         }
65//!     }
66//! }
67//!
68//! // 2. Create parser and register commands
69//! let mut echo = EchoModule { echo: false };
70//! let mut parser: AtParser<EchoModule, SIZE> = AtParser::new();
71//!
72//! let commands: &mut [(&str, &mut dyn AtContext<SIZE>)] = &mut [
73//!     ("AT+ECHO", &mut echo),
74//! ];
75//! parser.set_commands(commands);
76//!
77//! // 3. Execute commands
78//! parser.execute("AT+ECHO=1");  // Set echo on
79//! parser.execute("AT+ECHO?");   // Query current state
80//! ```
81//!
82//! # Features
83//!
84//! - **`freertos`** (default) - Enable FreeRTOS support via osal-rs
85//! - **`posix`** - Enable POSIX support via osal-rs
86//! - **`std`** - Enable standard library support via osal-rs
87//! - **`disable_panic`** - Pass-through feature to osal-rs for panic handling
88//!
89//! # Thread Safety
90//!
91//! The library can be used in single-threaded (bare-metal) or multi-threaded (RTOS)
92//! environments. For RTOS, use appropriate synchronization primitives around
93//! command handlers (e.g., `Mutex<RefCell<Handler>>`).
94
95#![no_std]
96
97extern crate alloc;
98extern crate osal_rs;
99
100use core::iter::Iterator;
101use core::option::Option;
102use core::result::Result;
103
104use alloc::string::String;
105use osal_rs::utils::Bytes;
106
107pub mod context;
108pub mod parser;
109
110
111/// Error types that can occur during AT command processing
112#[derive(Debug)]
113pub enum AtError<'a> {
114    /// The command is not recognized
115    UnknownCommand,
116    /// The command is recognized but not supported
117    NotSupported,
118    /// The command arguments are invalid
119    InvalidArgs,
120    /// Unhandled error with description
121    Unhandled(&'a str),
122    /// Unhandled error with description owned
123    UnhandledOwned(String)
124}
125
126/// Result type for AT command operations
127/// Returns either a `Bytes<SIZE>` response buffer or an `AtError`
128pub type AtResult<'a, const SIZE: usize> = Result<Bytes<SIZE>, AtError<'a>>;
129
130/// Structure holding the arguments passed to an AT command
131pub struct Args<'a> {
132    /// Raw argument string (comma-separated values)
133    pub raw: &'a str,
134}
135
136impl<'a> Args<'a> {
137    /// Get an argument by index (0-based)
138    /// Arguments are separated by commas
139    pub fn get(&self, index: usize) -> Option<&'a str> {
140        self.raw.split(',').nth(index)
141    }
142}
143
144
145/// Wraps a value in double-quote characters (`"`).
146///
147/// Expands to a string literal `"\"<value>\""` suitable for use inside
148/// [`at_response!`] or [`at_cmd_response!`] arguments when the protocol
149/// requires quoted strings.
150///
151/// # Syntax
152///
153/// ```rust,ignore
154/// at_quoted!(value)
155/// ```
156///
157/// # Examples
158///
159/// ```rust,no_run
160/// use at_parser_rs::at_quoted;
161///
162/// let q = at_quoted!("hello");   // → `"hello"`
163/// let q = at_quoted!(42);        // → `"42"`
164/// ```
165///
166/// Inside an AT response:
167///
168/// ```rust,no_run
169/// use at_parser_rs::{at_response, at_quoted};
170///
171/// const SIZE: usize = 64;
172/// let name = "world";
173/// let resp = at_response!(SIZE, "+CMD: "; at_quoted!(name));
174/// // resp contains: +CMD: "world"
175/// ```
176#[macro_export]
177macro_rules! at_quoted {
178    ($val:expr) => {
179        ::core::format_args!("\"{}\"", $val)
180    };
181}
182
183/// Macro to format an AT response with 1–6 comma-separated parameters.
184///
185/// # Syntax
186///
187/// ```rust,ignore
188/// at_response!(SIZE, AT_RESP; arg1, arg2, ..., arg6)
189/// ```
190///
191/// - `SIZE` — const usize for the response buffer capacity
192/// - `AT_RESP` — the AT response prefix string
193/// - `arg1..arg6` — values to append, comma-separated
194#[macro_export]
195macro_rules! at_response {
196    ($size:expr, $at_resp:expr; $a1:expr) => {{
197        let mut response = osal_rs::utils::Bytes::<{$size}>::new();
198        response.format(format_args!("{}{}", $at_resp, $a1));
199        response
200    }};
201    ($size:expr, $at_resp:expr; $a1:expr, $a2:expr) => {{
202        let mut response = osal_rs::utils::Bytes::<{$size}>::new();
203        response.format(format_args!("{}{},{}", $at_resp, $a1, $a2));
204        response
205    }};
206    ($size:expr, $at_resp:expr; $a1:expr, $a2:expr, $a3:expr) => {{
207        let mut response = osal_rs::utils::Bytes::<{$size}>::new();
208        response.format(format_args!("{}{},{},{}", $at_resp, $a1, $a2, $a3));
209        response
210    }};
211    ($size:expr, $at_resp:expr; $a1:expr, $a2:expr, $a3:expr, $a4:expr) => {{
212        let mut response = osal_rs::utils::Bytes::<{$size}>::new();
213        response.format(format_args!("{}{},{},{},{}", $at_resp, $a1, $a2, $a3, $a4));
214        response
215    }};
216    ($size:expr, $at_resp:expr; $a1:expr, $a2:expr, $a3:expr, $a4:expr, $a5:expr) => {{
217        let mut response = osal_rs::utils::Bytes::<{$size}>::new();
218        response.format(format_args!("{}{},{},{},{},{}", $at_resp, $a1, $a2, $a3, $a4, $a5));
219        response
220    }};
221    ($size:expr, $at_resp:expr; $a1:expr, $a2:expr, $a3:expr, $a4:expr, $a5:expr, $a6:expr) => {{
222        let mut response = osal_rs::utils::Bytes::<{$size}>::new();
223        response.format(format_args!("{}{},{},{},{},{},{}", $at_resp, $a1, $a2, $a3, $a4, $a5, $a6));
224        response
225    }};
226}
227
228
229
230/// Declares a static `COMMANDS` table mapping AT command strings to their handlers.
231///
232/// This macro expands into a `static COMMANDS` binding of type
233/// `&[(&'static str, &mut dyn AtContext<SIZE>)]`, which can then be passed to
234/// [`AtParser::set_commands`](crate::parser::AtParser::set_commands).
235///
236/// # Syntax
237///
238/// ```rust,ignore
239/// at_modules! {
240///     SIZE;
241///     "AT+CMD1" => HANDLER1,
242///     "AT+CMD2" => HANDLER2,
243/// }
244/// ```
245///
246/// - `SIZE` — `const usize` that defines the response buffer capacity (must match the
247///   capacity used by [`AtParser`](crate::parser::AtParser) and every [`AtContext`](crate::context::AtContext) impl).
248/// - `"AT+CMDn"` — the AT command string the parser will match against.
249/// - `HANDLERn` — a `static mut` variable that implements [`AtContext<SIZE>`](crate::context::AtContext).
250///
251/// # Safety
252///
253/// The macro uses an `unsafe` block internally to obtain `&mut` references to
254/// `static mut` items.  It is the caller's responsibility to ensure:
255///
256/// - **Single-threaded access only** — do not call this in a multi-threaded or
257///   RTOS context without external synchronisation.
258/// - **One call site** — the generated `COMMANDS` symbol is `static`; defining it
259///   more than once in the same scope will cause a compile error.
260///
261/// # Limitations
262///
263/// - All handlers must implement `AtContext` with the **same** `SIZE` constant.
264/// - The generated symbol is always named `COMMANDS`; rename it after expansion
265///   if you need multiple tables.
266///
267/// # Example — basic usage
268///
269/// ```rust,no_run
270/// use at_parser_rs::at_modules;
271/// use at_parser_rs::context::AtContext;
272/// use at_parser_rs::{Args, AtResult, AtError};
273/// use osal_rs::utils::Bytes;
274///
275/// const SIZE: usize = 64;
276///
277/// struct EchoModule { echo: bool }
278/// impl AtContext<SIZE> for EchoModule {
279///     fn query(&mut self) -> AtResult<SIZE> {
280///         Ok(Bytes::from_str(if self.echo { "1" } else { "0" }))
281///     }
282///     fn set(&mut self, args: Args) -> AtResult<SIZE> {
283///         match args.get(0) {
284///             Some("0") => { self.echo = false; Ok(Bytes::from_str("OK")) }
285///             Some("1") => { self.echo = true;  Ok(Bytes::from_str("OK")) }
286///             _ => Err(AtError::InvalidArgs),
287///         }
288///     }
289/// }
290///
291/// struct ResetModule;
292/// impl AtContext<SIZE> for ResetModule {
293///     fn execute(&mut self) -> AtResult<SIZE> { Ok(Bytes::from_str("OK")) }
294/// }
295///
296/// static mut ECHO:  EchoModule  = EchoModule { echo: false };
297/// static mut RESET: ResetModule = ResetModule;
298///
299/// at_modules! {
300///     SIZE;
301///     "AT+ECHO" => ECHO,
302///     "AT+RST"  => RESET,
303/// }
304///
305/// // COMMANDS is now available in scope:
306/// // parser.set_commands(COMMANDS);
307/// ```
308///
309/// # Example — single handler
310///
311/// ```rust,no_run
312/// use at_parser_rs::at_modules;
313/// use at_parser_rs::context::AtContext;
314/// use at_parser_rs::{AtResult};
315/// use osal_rs::utils::Bytes;
316///
317/// const SIZE: usize = 32;
318///
319/// struct PingModule;
320/// impl AtContext<SIZE> for PingModule {
321///     fn execute(&mut self) -> AtResult<SIZE> { Ok(Bytes::from_str("PONG")) }
322/// }
323///
324/// static mut PING: PingModule = PingModule;
325///
326/// at_modules! {
327///     SIZE;
328///     "AT+PING" => PING,
329/// }
330/// ```
331///
332/// # Recommended alternative
333///
334/// For multi-type handler tables or when `static mut` is undesirable, prefer the
335/// explicit slice approach — it requires no `unsafe` at the call site and allows
336/// mixing handler types via trait objects:
337///
338/// ```rust,no_run
339/// use at_parser_rs::context::AtContext;
340///
341/// const SIZE: usize = 64;
342///
343/// let mut echo  = EchoModule  { echo: false };
344/// let mut reset = ResetModule;
345///
346/// let commands: &mut [(&str, &mut dyn AtContext<SIZE>)] = &mut [
347///     ("AT+ECHO", &mut echo),
348///     ("AT+RST",  &mut reset),
349/// ];
350/// parser.set_commands(commands);
351/// ```
352#[macro_export]
353macro_rules! at_modules {
354    (
355        $size:expr;
356        $( $name:expr => $module:ident ),* $(,)?
357    ) => {
358        static COMMANDS: &[(&'static str, &mut dyn AtContext<$size>)] = unsafe {
359            &[
360                $(
361                    ($name, &mut $module),
362                )*
363            ]
364        };
365    };
366}