ATAT: AT Parser for serial based device crates

repository·master·Indexed 41 days ago

https://github.com/factbirdhq/atat

A lightweight, no_std Rust crate for parsing Hayes AT commands, designed for embedded drivers for serial modules such as cellular, LoRaWAN, and Wi-Fi/BT modules. Version 0.24.1 provides support for both embassy/no-std and tokio/std environments, featuring procedural macros like #[at_cmd] and #[derive(AtatCmd)] to automate the serialization of commands and deserialization of responses and Unsolicited Result Codes (URCs).

Tokens
16.3K
Snippets
53
Records
71
Agent score
49%

What's inside ATAT

  1. Overview of ATAT

    master
    ATAT is a #![no_std] Rust crate designed for parsing AT commands (Hayes command set). It serves as a driver support crate for serial modules that communicate via AT commands, making it suitable for embedded systems where a standard library is not available.
  2. Choose between `std` and `no-std` (embassy) examples

    master

    ATAT provides two distinct sets of examples depending on your target environment:

    1. embassy / no-std: Use these for embedded environments. These examples require the embedded feature enabled in Cargo.toml.
    2. tokio / std: Use these for standard operating system environments. These examples require the std feature enabled in Cargo.toml.
  3. AT command communication best practices

    master

    When building drivers or applications using ATAT, follow these best practices for robust AT command communication:

    • Channel Management: Flush the AT channel (check for pending data) before sending a new command. Wait at least 20ms after receiving a final response or URC before issuing a new command to avoid collisions with buffered URCs.
    • Response Handling: Detect and process complete lines. The DTE (Data Terminal Equipment) should know what answer is expected and wait for the final result code (OK, ERROR, or ABORTED).
    • Error Handling: Always handle error result codes before proceeding to the next command. If a command supports abortion and a timeout occurs, attempt to abort it.
    • Parsing: It is suggested not to strictly parse all information text responses, but rather to check them for specific keywords or parameters.
    • State Management: Implement a state machine for the AT parser (e.g., states for idle, waiting_response, and data_mode).
    • Hardware Considerations: When using serial ports without hardware flow control, be aware that the first character may be used to wake the module from power saving.
  4. Run ATAT examples

    master

    The crate provides examples for both no_std environments using embassy and std environments using tokio.

    To run the embassy example (requires nightly Rust and the embedded feature):

    cargo +nightly run --bin embassy --features embedded --target thumbv6m-none-eabi

    To run the tokio example:

    cargo +nightly run --example std-tokio --features std
    cargo +nightly run --bin embassy --features embedded --target thumbv6m-none-eabi
    cargo +nightly run --example std-tokio --features std
  5. Configure ATAT for `std` applications using `tokio` examples

    master

    If you are adapting the tokio examples for your own std application, you must explicitly configure the embassy-time dependency. This prevents embassy-time (which is a dependency of atat) from attempting to depend on the embassy executor in a standard environment.

    Add the following dependency to your Cargo.toml:

    embassy-time = { version = "<current_required_version>", features = ["std", "generic-queue-8"] }
    embassy-time = {version = "<current_required_version>", features = ["std", "generic-queue-8"]}
  6. How ATAT command and response abstractions work

    master

    ATAT abstracts AT command communication by using two primary traits: AtatCmd and AtatResp.

    • AtatCmd: Represents a command to be sent. It defines how to write the command to a buffer (write) and how to parse the resulting response (parse). Each command specifies its associated Response type.
    • AtatResp: Represents the data returned by a command.

    While you can implement these traits manually, it is highly recommended to use the atat_derive crate to automatically generate the implementation using procedural macros.

    // Manual implementation example
    impl<'a> AtatCmd for SetGreetingText<'a> {
        type Response = NoResponse;
    
        fn write(&self, mut buf: &mut [u8]) -> usize {
            // ... logic to write command to buffer
        }
    
        fn parse(&self, resp: Result<&[u8], InternalError>) -> Result<Self::Response, Error> {
            // ... logic to parse response
        }
    }
  7. Manage command results with ResponseSlot

    master

    A ResponseSlot<const N: usize> is used to manage and synchronize the delivery of command results (responses) to a consumer. It uses an internal Mutex to protect the Response<N> data and a Signal to notify consumers when a new response is available.

    To use a ResponseSlot, you typically initialize it with ResponseSlot::new() and then use one of the following methods to retrieve the result:

    • get(): An async method that waits for a signal to be emitted and then returns a ResponseSlotGuard. This is the primary way to await a response.
    • try_get(): A non-async method that returns Some(ResponseSlotGuard) if a signal has been emitted, or None if no signal is currently active.

    Note that ResponseSlot is generic over a constant N, which defines the capacity of the underlying response buffer.

    // Example of awaiting a response in an async context
    let slot = ResponseSlot::<16>::new();
    
    // ... elsewhere, a signal is emitted ...
    
    let response_guard = slot.get().await;
    // Use response_guard to access the Response data
  8. Configure ATAT features

    master

    ATAT can be customized using various Cargo features:

    • derive (default): Re-exports atat_derive to allow deriving Atat__ traits.
    • bytes (default): Re-exports serde-bytes & heapless-bytes for correct serialization/deserialization of non-quoted byte slices.
    • log: Enables logging via the log crate for debugging.
    • defmt: Enables logging via the defmt crate (common in embedded environments).
    • custom-error-messages: Allows errors to contain custom messages up to 64 characters, parsed via AtDigest::custom_error.
    • hex_str_arrays: Allows hex strings to be serialized to fixed-width byte arrays (requires Nightly Rust with generic_const_exprs).
    • heapless: Enables heapless support on serde_at for specialized parsing structs.
  9. Understand DigestResult outcomes

    master

    The digest method returns a DigestResult<'a>, which categorizes the parsed content of the buffer:

    • Urc(&'a [u8]): An Unsolicited Response Code was detected.
    • Response(Result<&'a [u8], InternalError<'a>>): A standard command response was detected. The inner Result contains the response data or an InternalError (like CmeError or CmsError).
    • Prompt(u8): A data prompt (like > or @) was detected. The u8 represents the prompt character.
    • None: No complete AT pattern was recognized in the current buffer.
  10. Deserialize length-delimited AT payloads

    master

    For AT commands that include a length-prefixed payload (common in some modem protocols), use the LengthDelimited type (available when the heapless feature is enabled).

    This type can parse payloads that are:

    1. Quoted: "<length>,<payload>"
    2. Unquoted: <length>,<payload>
    3. Separated by a line break: <length>\r\n<payload>
    use serde::Deserialize;
    // Note: requires 'heapless' feature
    use atat::length_delimited::LengthDelimited;
    
    #[derive(Deserialize, Debug, PartialEq)]
    struct PayloadResponse {
        pub ctx: u8,
        pub id: i8,
        pub payload: LengthDelimited<32>,
    }
    
    // Example: parsing "1,-1,9,\"ABCD,1234\""
    // Result: ctx=1, id=-1, payload=b"ABCD,1234"
  11. Handle DigestResult outcomes

    master

    The DigestResult enum represents the state of the parsed AT stream:

    • DigestResult::Response(Result<Vec<u8>, InternalError>): A complete command response or error has been parsed. The inner Result contains the raw bytes of the response or an InternalError (such as CmeError).
    • DigestResult::Prompt(u8): The parser encountered a command prompt character (e.g., > or @). The returned u8 is the byte value of that prompt.
    • DigestResult::None: The buffer does not yet contain a complete recognizable sequence. No bytes were consumed, and you should wait for more data.
  12. Use AtDigester to process AT command streams

    master

    The AtDigester<P> is the primary tool for processing raw byte streams from an AT-compatible device. It handles the complexities of AT command protocols, including:

    • Removing command echoes (e.g., AT+CMD\r\n)
    • Distinguishing between Unsolicited Response Codes (URCs), standard responses, and prompts.
    • Handling both echo-enabled and echo-disabled modes.
    • Managing multi-line responses.

    To use it, you must provide a type P that implements the Parser trait to handle URC (Unsolicited Response Code) detection. You can then use the digest method to extract structured results from a buffer.

    // Assuming a Parser implementation exists
    let mut digester = AtDigester::<MyUrcParser>::new();
    let (result, consumed) = digester.digest(&buffer);
    
    // 'result' will be a DigestResult containing the parsed data
    // 'consumed' is the number of bytes to remove from your buffer