From 70283becd0afcff4cb60837dcb716f82fa2636ee Mon Sep 17 00:00:00 2001 From: Rasmus Melchior Jacobsen Date: Sat, 25 Mar 2023 09:53:32 +0100 Subject: [PATCH] Async client (#142) * Move blocking parts to blocking module * Fix compile error in blocking client * Use Frame type using bincode in favor of custom Encoded * Add async client * Move buffers to shared * Async should not be a default feature * Fix compile error * Formatting * Let Ingress be shared between blocking and async * Add some docs to async client trait * Forward defmt feature to embedded-io * Remove old async client trait (now moved to mod) * Let ingress expose its buffer * Remove overflow error * Add async ingress advance * Migrate all timing/delay/timeout handling to embassy-time * Run GH actions tests with std feature flag, to enable time-driver * Add missing start_cooldown_timer to blocking client * Remove unused embedded-hal dependency * Add compile checks on buffer capacities * Remove embedded-time * Let tests run with cargo --test * Revert "Run GH actions tests with std feature flag, to enable time-driver" This reverts commit b52355c6db06d769333f4e39c3662de266bf62bd. * Formatting * Add example test on why we need double buffer capacity * Fix ingress regression for processing available bytes in buffer * Add write() convenience function to AtatIngress * Fix digest buffer slice * Let timeouts in config be Duration * Let error response be a warning - it may be deliberate * Add std example based on tokio * Move examples to seperate workspace member, and add embassy async example * Fix clippy recommendations * Use nightly toolchain in most of CI * Add thumbv6 feature in CI * Update readme with async feature and new examples --------- Co-authored-by: Mathias --- .cargo/config.toml | 11 + .github/workflows/ci.yml | 27 +- .vscode/settings.json | 3 +- Cargo.toml | 2 +- README.md | 17 +- atat/Cargo.toml | 46 +- atat/examples/common/timer.rs | 66 -- atat/examples/cortex-m-rt.rs | 149 ---- atat/examples/rtic.rs | 177 ---- atat/rust-toolchain | 1 + atat/src/asynch/client.rs | 134 +++ atat/src/asynch/mod.rs | 79 ++ atat/src/blocking/client.rs | 557 ++++++++++++ atat/src/blocking/mod.rs | 82 ++ atat/src/blocking/timer.rs | 38 + atat/src/buffers.rs | 143 +++ atat/src/builder.rs | 91 -- atat/src/client.rs | 821 ------------------ atat/src/clock.rs | 3 - atat/src/config.rs | 48 + atat/src/digest.rs | 2 +- atat/src/error/mod.rs | 88 -- atat/src/frame.rs | 136 +++ atat/src/ingress.rs | 213 +++++ atat/src/ingress_manager.rs | 217 ----- atat/src/lib.rs | 79 +- atat/src/queues.rs | 14 - atat/src/traits.rs | 104 +-- examples/Cargo.toml | 72 ++ examples/src/bin/embassy.rs | 87 ++ examples/src/bin/std-tokio.rs | 57 ++ .../src}/common/general/mod.rs | 0 .../src}/common/general/responses.rs | 4 +- .../src}/common/general/urc.rs | 0 {atat/examples => examples/src}/common/mod.rs | 1 - examples/src/lib.rs | 2 + 36 files changed, 1725 insertions(+), 1846 deletions(-) delete mode 100644 atat/examples/common/timer.rs delete mode 100644 atat/examples/cortex-m-rt.rs delete mode 100644 atat/examples/rtic.rs create mode 100644 atat/rust-toolchain create mode 100644 atat/src/asynch/client.rs create mode 100644 atat/src/asynch/mod.rs create mode 100644 atat/src/blocking/client.rs create mode 100644 atat/src/blocking/mod.rs create mode 100644 atat/src/blocking/timer.rs create mode 100644 atat/src/buffers.rs delete mode 100644 atat/src/builder.rs delete mode 100644 atat/src/client.rs delete mode 100644 atat/src/clock.rs create mode 100644 atat/src/config.rs create mode 100644 atat/src/frame.rs create mode 100644 atat/src/ingress.rs delete mode 100644 atat/src/ingress_manager.rs delete mode 100644 atat/src/queues.rs create mode 100644 examples/Cargo.toml create mode 100644 examples/src/bin/embassy.rs create mode 100644 examples/src/bin/std-tokio.rs rename {atat/examples => examples/src}/common/general/mod.rs (100%) rename {atat/examples => examples/src}/common/general/responses.rs (89%) rename {atat/examples => examples/src}/common/general/urc.rs (100%) rename {atat/examples => examples/src}/common/mod.rs (96%) create mode 100644 examples/src/lib.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index c1e5c90d..8e32ede7 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -8,3 +8,14 @@ rustflags = [ # See https://github.com/rust-embedded/cortex-m-quickstart/pull/95 "-C", "link-arg=-nmagic", ] + +[target.thumbv6m-none-eabi] +runner = "probe-run --chip RP2040" +rustflags = [ + "-C", "link-arg=-Tlink.x", + "-C", "link-arg=-Tdefmt.x", + + # This is needed if your flash or ram addresses are not aligned to 0x10000 in memory.x + # See https://github.com/rust-embedded/cortex-m-quickstart/pull/95 + "-C", "link-arg=-nmagic", +] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4d79792d..fc53294a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,7 +77,7 @@ jobs: uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: stable + toolchain: nightly override: true components: clippy @@ -92,7 +92,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - target: ["x86_64-unknown-linux-gnu", "thumbv7em-none-eabihf"] + target: ["x86_64-unknown-linux-gnu", "thumbv6m-none-eabi"] features: [ "", @@ -107,11 +107,8 @@ jobs: - target: "x86_64-unknown-linux-gnu" features: "derive, log" std: ", std" - - target: "x86_64-unknown-linux-gnu" - features: "derive, defmt" - std: ", std" - - target: "thumbv7em-none-eabihf" - examples: "--examples" + - target: "thumbv6m-none-eabi" + thumb: ", thumbv6" steps: - name: Checkout source code uses: actions/checkout@v2 @@ -120,15 +117,15 @@ jobs: uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: stable - target: thumbv7em-none-eabihf + toolchain: nightly + target: thumbv6m-none-eabi override: true - name: Build uses: actions-rs/cargo@v1 with: command: build - args: --all --target '${{ matrix.target }}' --features '${{ matrix.features }}${{ matrix.std }}' ${{ matrix.examples }} + args: --all --target '${{ matrix.target }}' --features '${{ matrix.features }}${{ matrix.thumb }}${{ matrix.std }}' test: name: Test @@ -140,14 +137,14 @@ jobs: uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: stable - target: thumbv7em-none-eabihf + toolchain: nightly + target: thumbv6m-none-eabi override: true - name: Library tests uses: actions-rs/cargo@v1 with: command: test - args: --all + args: --all --features std grcov: name: Coverage @@ -161,7 +158,7 @@ jobs: with: profile: minimal toolchain: nightly - target: thumbv7em-none-eabihf + target: thumbv6m-none-eabi override: true - name: Install grcov @@ -178,7 +175,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: test - args: --lib --no-fail-fast + args: --lib --no-fail-fast --features std env: CARGO_INCREMENTAL: "0" RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=unwind -Zpanic_abort_tests" diff --git a/.vscode/settings.json b/.vscode/settings.json index 5aafdeb7..3ef9239f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,7 +3,8 @@ // "can't find crate for `test`" when the default compilation target is a no_std target // with these changes RA will call `cargo check --bins` on save "rust-analyzer.checkOnSave.allTargets": false, - "rust-analyzer.cargo.target": "thumbv7em-none-eabihf", + "rust-analyzer.cargo.target": "thumbv6m-none-eabi", + "rust-analyzer.cargo.features": ["async", "thumbv6"], "rust-analyzer.diagnostics.disabled": [ "unresolved-import" ] diff --git a/Cargo.toml b/Cargo.toml index 98589dfc..60e3eebb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["serde_at", "atat_derive", "atat"] +members = ["serde_at", "atat_derive", "atat", "examples"] # cargo build/run diff --git a/README.md b/README.md index 8285dd5c..4e49dd90 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,7 @@ `#![no_std]` crate for parsing AT commands ([Hayes command set](https://en.wikipedia.org/wiki/Hayes_command_set)) -A driver support crate for AT-command based serial modules, using the [embedded-hal] traits. - -[embedded-hal]: https://crates.io/crates/embedded-hal +A driver support crate for AT-command based serial modules. ## AT Best practices @@ -44,14 +42,14 @@ This crate attempts to work from these AT best practices: ## Examples -The crate has examples for usage with [cortex-m-rt] and [cortex-m-rtic] crates. +The crate has examples for usage with [embassy] for `#![no_std]` and [tokio] for `std`. -The samples can be built using `cargo build --example cortex-m-rt --target thumbv7em-none-eabihf` and `cargo build --example rtic --target thumbv7em-none-eabihf`. +The samples can be built using `cargo +nightly run --bin embassy --features embedded --target thumbv6m-none-eabi` and `cargo +nightly run --example std-tokio --features std`. -Furthermore I have used the crate to build initial WIP drivers for uBlox cellular modules ([ublox-cellular-rs]) and uBlox short-range modules ([ublox-short-range-rs]) +Furthermore the crate has been used to build initial drivers for U-Blox cellular modules ([ublox-cellular-rs]) and U-Blox short-range modules ([ublox-short-range-rs]) -[cortex-m-rt]: https://crates.io/crates/cortex-m-rt -[cortex-m-rtic]: https://crates.io/crates/cortex-m-rtic +[embassy]: https://crates.io/crates/embassy-executor +[tokio]: https://crates.io/crates/tokio [ublox-short-range-rs]: https://github.com/BlackbirdHQ/ublox-short-range-rs [ublox-cellular-rs]: https://github.com/BlackbirdHQ/ublox-cellular-rs @@ -100,10 +98,11 @@ The following dependent crates provide platform-agnostic device drivers built on - `log`: Disabled by default. Enable log statements on various log levels to aid debugging. Powered by `log`. - `defmt`: Disabled by default. Enable defmt log statements on various log levels to aid debugging. Powered by `defmt`. - `custom-error-messages`: Disabled by default. Allows errors to contain custom error messages up to 64 characters, parsed by `AtDigest::custom_error`. +- `async`: Enable the async interfaces on both `Ingress` and `Client`. ## Chat / Getting Help -If you have questions on the development of AT-AT or want to write a driver +If you have questions on the development of ATAT or want to write a driver based on it, feel free to join our matrix room at `#atat:matrix.org`! ## License diff --git a/atat/Cargo.toml b/atat/Cargo.toml index a71806f2..c1acc51e 100644 --- a/atat/Cargo.toml +++ b/atat/Cargo.toml @@ -17,20 +17,15 @@ maintenance = { status = "actively-developed" } [lib] name = "atat" -[[example]] -name = "rtic" -required-features = ["defmt"] - -[[example]] -name = "cortex-m-rt" -required-features = ["defmt"] - [dependencies] -embedded-hal-nb = { version = "=1.0.0-alpha.1" } -fugit = "0.3.3" -fugit-timer = "0.1.2" +bincode = { version = "2.0.0-rc.2", default-features = false, features = [ + "derive", +] } +embedded-io = "0.4" +futures = { version = "0.3", default-features = false, optional = true } +embassy-time = "0.1.0" heapless = { version = "^0.7", features = ["serde"] } -bbqueue = "0.5" +bbqueue = { git = "https://github.com/jamesmunns/bbqueue", rev = "refs/pull/95/head" } serde_at = { path = "../serde_at", version = "^0.18.0", optional = true } atat_derive = { path = "../atat_derive", version = "^0.18.0", optional = true } serde_bytes = { version = "0.11.5", default-features = false, optional = true } @@ -41,24 +36,21 @@ nom = { version = "^7.1", default-features = false } log = { version = "^0.4", default-features = false, optional = true } defmt = { version = "^0.3", optional = true } -[dev-dependencies] -cortex-m = { version = "0.7.6", features = ["critical-section-single-core"] } -cortex-m-rt = "0.7.1" -cortex-m-rtic = "1.0.0" -defmt-rtt = "0.3.1" -panic-probe = { version = "0.3.0", features = ["print-defmt"] } -dwt-systick-monotonic = "1.0.0" -embassy-stm32 = { git = "https://github.com/embassy-rs/embassy", rev = "99284b8", features = [ - "stm32l475vg", - "unstable-traits", - "unstable-pac", -] } - [features] default = ["derive", "bytes"] +defmt = ["dep:defmt", "embedded-io/defmt"] derive = ["atat_derive", "serde_at"] +bytes = ["heapless-bytes", "serde_bytes"] custom-error-messages = [] -bytes = ["heapless-bytes", "serde_bytes"] +async = ["embedded-io/async", "futures"] + +thumbv6 = ["bbqueue/thumbv6"] -std = ["serde_at/std", "nom/std"] +std = [ + "serde_at/std", + "nom/std", + "embassy-time/std", + "embedded-io/std", + "embedded-io/tokio", +] diff --git a/atat/examples/common/timer.rs b/atat/examples/common/timer.rs deleted file mode 100644 index ecd04391..00000000 --- a/atat/examples/common/timer.rs +++ /dev/null @@ -1,66 +0,0 @@ -use cortex_m::{interrupt, peripheral::DWT}; -use embedded_hal_nb::nb; - -pub struct DwtTimer { - end_time: Option>, -} - -impl DwtTimer { - pub fn new() -> Self { - Self { end_time: None } - } - - pub fn now() -> u64 { - static mut DWT_OVERFLOWS: u32 = 0; - static mut OLD_DWT: u32 = 0; - - interrupt::free(|_| { - // Safety: These static mut variables are accessed in an interrupt free section. - let (overflows, last_cnt) = unsafe { (&mut DWT_OVERFLOWS, &mut OLD_DWT) }; - - let cyccnt = DWT::cycle_count(); - - if cyccnt <= *last_cnt { - *overflows += 1; - } - - let ticks = (*overflows as u64) << 32 | (cyccnt as u64); - *last_cnt = cyccnt; - - ticks - }) - } -} - -impl Default for DwtTimer { - fn default() -> Self { - Self::new() - } -} - -impl fugit_timer::Timer for DwtTimer { - type Error = core::convert::Infallible; - - fn now(&mut self) -> fugit::TimerInstantU32 { - fugit::TimerInstantU32::from_ticks(Self::now() as u32) - } - - fn start(&mut self, duration: fugit::TimerDurationU32) -> Result<(), Self::Error> { - let end = self.now() + duration; - self.end_time.replace(end); - Ok(()) - } - - fn cancel(&mut self) -> Result<(), Self::Error> { - self.end_time.take(); - Ok(()) - } - - fn wait(&mut self) -> nb::Result<(), Self::Error> { - let now = self.now(); - match self.end_time { - Some(end) if end <= now => Ok(()), - _ => Err(nb::Error::WouldBlock), - } - } -} diff --git a/atat/examples/cortex-m-rt.rs b/atat/examples/cortex-m-rt.rs deleted file mode 100644 index 25b872f6..00000000 --- a/atat/examples/cortex-m-rt.rs +++ /dev/null @@ -1,149 +0,0 @@ -#![no_main] -#![no_std] -mod common; - -use defmt_rtt as _; -use embassy_stm32::interrupt; -// global logger -use embassy_stm32::peripherals::USART3; -use embassy_stm32::{dma::NoDma, gpio}; -use panic_probe as _; - -use atat::{clock::Clock, AtatClient, ClientBuilder, Queues}; -use bbqueue::BBBuffer; -use common::{timer::DwtTimer, Urc}; -use cortex_m_rt::entry; -use fugit::ExtU32; - -use embedded_hal_nb::nb; - -#[cfg(feature = "defmt")] -defmt::timestamp!("{=u64}", { DwtTimer::<80_000_000>::now() / 80_000 }); - -const RX_BUFFER_BYTES: usize = 512; -// Response queue is capable of holding one full RX buffer -const RES_CAPACITY_BYTES: usize = RX_BUFFER_BYTES; -// URC queue is capable of holding up to three full RX buffer -const URC_CAPACITY_BYTES: usize = RX_BUFFER_BYTES * 3; - -static mut INGRESS: Option< - atat::IngressManager< - atat::AtDigester, - RX_BUFFER_BYTES, - RES_CAPACITY_BYTES, - URC_CAPACITY_BYTES, - >, -> = None; -static mut RX: Option> = None; -// static mut TIMER: Option> = None; - -#[entry] -fn main() -> ! { - // Create static queues for ATAT - static mut RES_QUEUE: BBBuffer = BBBuffer::new(); - static mut URC_QUEUE: BBBuffer = BBBuffer::new(); - - let p = embassy_stm32::init(Default::default()); - - let mut wifi_nrst = gpio::OutputOpenDrain::new( - p.PD13, - gpio::Level::Low, - gpio::Speed::Medium, - gpio::Pull::None, - ); - wifi_nrst.set_high(); - - let serial = embassy_stm32::usart::Uart::new( - p.USART3, - p.PD9, - p.PD8, - // p.PB1, - // p.PA6, - NoDma, - NoDma, - embassy_stm32::usart::Config::default(), - ); - - let (tx, rx) = serial.split(); - - // Instantiate ATAT client & IngressManager - let queues = Queues { - res_queue: RES_QUEUE.try_split_framed().unwrap(), - urc_queue: URC_QUEUE.try_split_framed().unwrap(), - }; - - let (mut client, ingress) = ClientBuilder::new( - tx, - DwtTimer::<80_000_000>::new(), - atat::AtDigester::new(), - atat::Config::new(atat::Mode::Timeout), - ) - .build(queues); - - // configure NVIC interrupts - // let mut timer = Timer::tim7(p.TIM7, 100.hz(), clocks, &mut rcc.apb1r1); - // unsafe { cortex_m::peripheral::NVIC::unmask(hal::stm32::Interrupt::TIM7) }; - unsafe { cortex_m::peripheral::NVIC::unmask(embassy_stm32::pac::Interrupt::USART3) }; - // timer.listen(Event::TimeOut); - - unsafe { INGRESS = Some(ingress) }; - unsafe { RX = Some(rx) }; - // unsafe { TIMER = Some(timer) }; - - let mut state = 0; - let mut loop_timer = DwtTimer::<80_000_000>::new(); - - loop { - #[cfg(feature = "defmt")] - defmt::debug!("\r\n\r\n\r\n"); - - match state { - 0 => { - client.send(&common::general::GetManufacturerId).ok(); - } - 1 => { - client.send(&common::general::GetModelId).ok(); - } - 2 => { - client.send(&common::general::GetSoftwareVersion).ok(); - } - 3 => { - client.send(&common::general::GetWifiMac).ok(); - } - _ => cortex_m::asm::bkpt(), - } - - loop_timer.start(1.secs()).ok(); - nb::block!(loop_timer.wait()).ok(); - - state += 1; - } -} - -// #[interrupt] -// fn TIM7() { -// cortex_m::interrupt::free(|_| { -// let timer = unsafe { TIMER.as_mut().unwrap() }; -// timer.clear_update_interrupt_flag(); -// let ingress = unsafe { INGRESS.as_mut().unwrap() }; -// ingress.digest(); -// }); -// } - -#[interrupt] -fn USART3() { - cortex_m::interrupt::free(|_| { - let ingress = unsafe { INGRESS.as_mut().unwrap() }; - let rx = unsafe { RX.as_mut().unwrap() }; - if let Ok(d) = nb::block!(rx.nb_read()) { - ingress.write(&[d]); - } - }); -} - -// same panicking *behavior* as `panic-probe` but doesn't print a panic message -// this prevents the panic message being printed *twice* when `defmt::panic` is invoked -#[defmt::panic_handler] -fn panic() -> ! { - cortex_m::asm::udf() -} diff --git a/atat/examples/rtic.rs b/atat/examples/rtic.rs deleted file mode 100644 index a2721da7..00000000 --- a/atat/examples/rtic.rs +++ /dev/null @@ -1,177 +0,0 @@ -#![no_main] -#![no_std] -pub mod common; - -use defmt_rtt as _; -use panic_probe as _; // global logger - -#[cfg(feature = "defmt")] -defmt::timestamp!("{=u64}", { - common::timer::DwtTimer::<80_000_000>::now() / 80_000 -}); - -pub mod pac { - pub const NVIC_PRIO_BITS: u8 = 2; - pub use cortex_m_rt::interrupt; - pub use embassy_stm32::pac::Interrupt as interrupt; - pub use embassy_stm32::pac::*; -} - -#[rtic::app(device = crate::pac, peripherals = false, dispatchers = [UART4, UART5])] -mod app { - use super::common::{self, timer::DwtTimer, Urc}; - use bbqueue::BBBuffer; - use dwt_systick_monotonic::*; - use embedded_hal_nb::nb; - - use embassy_stm32::{dma::NoDma, gpio, peripherals::USART3}; - - use atat::{AtatClient, ClientBuilder, Queues}; - - #[monotonic(binds = SysTick, default = true)] - type MyMono = DwtSystick<80_000_000>; - - const RX_BUFFER_BYTES: usize = 512; - // Response queue is capable of holding one full RX buffer - const RES_CAPACITY_BYTES: usize = RX_BUFFER_BYTES; - // URC queue is capable of holding up to three full RX buffer - const URC_CAPACITY_BYTES: usize = RX_BUFFER_BYTES * 3; - - #[shared] - struct SharedResources { - ingress: atat::IngressManager< - atat::AtDigester, - RX_BUFFER_BYTES, - RES_CAPACITY_BYTES, - URC_CAPACITY_BYTES, - >, - } - #[local] - struct LocalResources { - rx: embassy_stm32::usart::UartRx<'static, USART3>, - client: atat::Client< - embassy_stm32::usart::UartTx<'static, USART3>, - DwtTimer<80_000_000>, - 80_000_000, - RES_CAPACITY_BYTES, - URC_CAPACITY_BYTES, - >, - } - - #[init()] - fn init(mut ctx: init::Context) -> (SharedResources, LocalResources, init::Monotonics()) { - // Create static queues for ATAT - static mut RES_QUEUE: BBBuffer = BBBuffer::new(); - static mut URC_QUEUE: BBBuffer = BBBuffer::new(); - - let p = embassy_stm32::init(Default::default()); - - let mut wifi_nrst = gpio::OutputOpenDrain::new( - p.PD13, - gpio::Level::Low, - gpio::Speed::Medium, - gpio::Pull::None, - ); - wifi_nrst.set_high(); - - let serial = embassy_stm32::usart::Uart::new( - p.USART3, - p.PD9, - p.PD8, - // p.PB1, - // p.PA6, - NoDma, - NoDma, - embassy_stm32::usart::Config::default(), - ); - let (tx, rx) = serial.split(); - - // Instantiate ATAT client & IngressManager - let queues = Queues { - res_queue: unsafe { RES_QUEUE.try_split_framed().unwrap() }, - urc_queue: unsafe { URC_QUEUE.try_split_framed().unwrap() }, - }; - - let (client, ingress) = ClientBuilder::new( - tx, - DwtTimer::<80_000_000>::new(), - atat::AtDigester::new(), - atat::Config::new(atat::Mode::Timeout), - ) - .build(queues); - - at_loop::spawn().ok(); - at_send::spawn(0).ok(); - - let mono = DwtSystick::new(&mut ctx.core.DCB, ctx.core.DWT, ctx.core.SYST, 80_000_000); - - ( - SharedResources { ingress }, - LocalResources { client, rx }, - // Initialize the monotonic - init::Monotonics(mono), - ) - } - - #[idle] - fn idle(_: idle::Context) -> ! { - loop { - cortex_m::asm::nop(); - } - } - - #[task(local = [client], priority = 2)] - fn at_send(ctx: at_send::Context, state: u8) { - #[cfg(feature = "defmt")] - defmt::debug!("\r\n\r\n\r\n"); - - match state { - 0 => { - ctx.local - .client - .send(&common::general::GetManufacturerId) - .ok(); - } - 1 => { - ctx.local.client.send(&common::general::GetModelId).ok(); - } - 2 => { - ctx.local - .client - .send(&common::general::GetSoftwareVersion) - .ok(); - } - 3 => { - ctx.local.client.send(&common::general::GetWifiMac).ok(); - } - _ => cortex_m::asm::bkpt(), - } - // Adjust this spin rate to set how often the request/response queue is checked - at_send::spawn_at(monotonics::now() + 1.secs(), state + 1).ok(); - } - - #[task(shared = [ingress], priority = 3)] - fn at_loop(mut ctx: at_loop::Context) { - ctx.shared.ingress.lock(|at| at.digest()); - - // Adjust this spin rate to set how often the request/response queue is checked - at_loop::spawn_at(monotonics::now() + 10.millis()).ok(); - } - - #[task(binds = USART3, priority = 4, shared = [ingress], local = [rx])] - fn serial_irq(mut ctx: serial_irq::Context) { - let rx = ctx.local.rx; - ctx.shared.ingress.lock(|ingress| { - if let Ok(d) = nb::block!(rx.nb_read()) { - ingress.write(&[d]); - } - }); - } -} - -// same panicking *behavior* as `panic-probe` but doesn't print a panic message -// this prevents the panic message being printed *twice* when `defmt::panic` is invoked -#[defmt::panic_handler] -fn panic() -> ! { - cortex_m::asm::udf() -} diff --git a/atat/rust-toolchain b/atat/rust-toolchain new file mode 100644 index 00000000..07ade694 --- /dev/null +++ b/atat/rust-toolchain @@ -0,0 +1 @@ +nightly \ No newline at end of file diff --git a/atat/src/asynch/client.rs b/atat/src/asynch/client.rs new file mode 100644 index 00000000..6298b6b7 --- /dev/null +++ b/atat/src/asynch/client.rs @@ -0,0 +1,134 @@ +use super::AtatClient; +use crate::{frame::Frame, helpers::LossyStr, AtatCmd, AtatUrc, Config, Error, Response}; +use bbqueue::framed::FrameConsumer; +use embassy_time::{with_timeout, Duration, Timer}; +use embedded_io::asynch::Write; + +pub struct Client<'a, W: Write, const RES_CAPACITY: usize, const URC_CAPACITY: usize> { + writer: W, + res_reader: FrameConsumer<'a, RES_CAPACITY>, + urc_reader: FrameConsumer<'a, URC_CAPACITY>, + config: Config, + cooldown_timer: Option, +} + +impl<'a, W: Write, const RES_CAPACITY: usize, const URC_CAPACITY: usize> + Client<'a, W, RES_CAPACITY, URC_CAPACITY> +{ + pub(crate) fn new( + writer: W, + res_reader: FrameConsumer<'a, RES_CAPACITY>, + urc_reader: FrameConsumer<'a, URC_CAPACITY>, + config: Config, + ) -> Self { + Self { + writer, + res_reader, + urc_reader, + config, + cooldown_timer: None, + } + } +} + +impl + Client<'_, W, RES_CAPACITY, URC_CAPACITY> +{ + fn start_cooldown_timer(&mut self) { + self.cooldown_timer = Some(Timer::after(self.config.cmd_cooldown)); + } + + async fn wait_cooldown_timer(&mut self) { + if let Some(cooldown) = self.cooldown_timer.take() { + cooldown.await + } + } +} + +impl AtatClient + for Client<'_, W, RES_CAPACITY, URC_CAPACITY> +{ + async fn send, const LEN: usize>( + &mut self, + cmd: &Cmd, + ) -> Result { + self.wait_cooldown_timer().await; + + let cmd_bytes = cmd.as_bytes(); + let cmd_slice = cmd.get_slice(&cmd_bytes); + if cmd_slice.len() < 50 { + debug!("Sending command: {:?}", LossyStr(cmd_slice)); + } else { + debug!( + "Sending command with long payload ({} bytes)", + cmd_slice.len(), + ); + } + + self.writer + .write_all(cmd_slice) + .await + .map_err(|_| Error::Write)?; + + self.writer.flush().await.map_err(|_| Error::Write)?; + + if !Cmd::EXPECTS_RESPONSE_CODE { + debug!("Command does not expect a response"); + self.start_cooldown_timer(); + return cmd.parse(Ok(&[])); + } + + let response = match with_timeout( + Duration::from_millis(Cmd::MAX_TIMEOUT_MS.into()), + self.res_reader.read_async(), + ) + .await + { + Ok(res) => { + let mut grant = res.unwrap(); + grant.auto_release(true); + + let frame = Frame::decode(grant.as_ref()); + let resp = match Response::from(frame) { + Response::Result(r) => r, + Response::Prompt(_) => Ok(&[][..]), + }; + + cmd.parse(resp) + } + Err(_) => { + warn!("Received timeout after {}ms", Cmd::MAX_TIMEOUT_MS); + Err(Error::Timeout) + } + }; + + self.start_cooldown_timer(); + response + } + + fn try_read_urc_with FnOnce(Urc::Response, &'b [u8]) -> bool>( + &mut self, + handle: F, + ) -> bool { + if let Some(urc_grant) = self.urc_reader.read() { + self.start_cooldown_timer(); + if let Some(urc) = Urc::parse(&urc_grant) { + if handle(urc, &urc_grant) { + urc_grant.release(); + return true; + } + } else { + error!("Parsing URC FAILED: {:?}", LossyStr(&urc_grant)); + urc_grant.release(); + } + } + + false + } + + fn max_urc_len() -> usize { + // bbqueue can only guarantee grant sizes of half its capacity if the queue is empty. + // A _frame_ grant returned by bbqueue has a header. Assume that it is 2 bytes. + (URC_CAPACITY / 2) - 2 + } +} diff --git a/atat/src/asynch/mod.rs b/atat/src/asynch/mod.rs new file mode 100644 index 00000000..983dc2bc --- /dev/null +++ b/atat/src/asynch/mod.rs @@ -0,0 +1,79 @@ +mod client; + +pub use client::Client; + +use crate::{AtatCmd, AtatUrc, Error}; + +pub trait AtatClient { + /// Send an AT command. + /// + /// `cmd` must implement [`AtatCmd`]. + /// + /// This function will also make sure that at least `self.config.cmd_cooldown` + /// has passed since the last response or URC has been received, to allow + /// the slave AT device time to deliver URC's. + async fn send, const LEN: usize>( + &mut self, + cmd: &Cmd, + ) -> Result; + + async fn send_retry, const LEN: usize>( + &mut self, + cmd: &Cmd, + ) -> Result { + for attempt in 1..=Cmd::ATTEMPTS { + if attempt > 1 { + debug!("Attempt {}:", attempt); + } + + match self.send(cmd).await { + Err(Error::Timeout) => {} + r => return r, + } + } + Err(Error::Timeout) + } + + /// Checks if there are any URC's (Unsolicited Response Code) in + /// queue from the ingress manager. + /// + /// Example: + /// ``` + /// use atat::atat_derive::{AtatResp, AtatUrc}; + /// + /// #[derive(Clone, AtatResp)] + /// pub struct MessageWaitingIndication { + /// #[at_arg(position = 0)] + /// pub status: u8, + /// #[at_arg(position = 1)] + /// pub code: u8, + /// } + /// + /// #[derive(Clone, AtatUrc)] + /// pub enum Urc { + /// #[at_urc("+UMWI")] + /// MessageWaitingIndication(MessageWaitingIndication), + /// } + /// + /// // match client.check_urc::() { + /// // Some(Urc::MessageWaitingIndication(MessageWaitingIndication { status, code })) => { + /// // // Do something to act on `+UMWI` URC + /// // } + /// // } + /// ``` + fn try_read_urc(&mut self) -> Option { + let mut first = None; + self.try_read_urc_with::(|urc, _| { + first = Some(urc); + true + }); + first + } + + fn try_read_urc_with FnOnce(Urc::Response, &'b [u8]) -> bool>( + &mut self, + handle: F, + ) -> bool; + + fn max_urc_len() -> usize; +} diff --git a/atat/src/blocking/client.rs b/atat/src/blocking/client.rs new file mode 100644 index 00000000..a9eec7b7 --- /dev/null +++ b/atat/src/blocking/client.rs @@ -0,0 +1,557 @@ +use bbqueue::framed::FrameConsumer; +use embassy_time::Duration; +use embedded_io::blocking::Write; + +use super::timer::Timer; +use super::AtatClient; +use crate::error::{Error, Response}; +use crate::frame::Frame; +use crate::helpers::LossyStr; +use crate::AtatCmd; +use crate::{AtatUrc, Config}; + +/// Client responsible for handling send, receive and timeout from the +/// userfacing side. The client is decoupled from the ingress-manager through +/// some spsc queue consumers, where any received responses can be dequeued. The +/// Client also has an spsc producer, to allow signaling commands like +/// `reset` to the ingress-manager. +pub struct Client<'a, W, const RES_CAPACITY: usize, const URC_CAPACITY: usize> +where + W: Write, +{ + writer: W, + + res_reader: FrameConsumer<'a, RES_CAPACITY>, + urc_reader: FrameConsumer<'a, URC_CAPACITY>, + + cooldown_timer: Option, + config: Config, +} + +impl<'a, W, const RES_CAPACITY: usize, const URC_CAPACITY: usize> + Client<'a, W, RES_CAPACITY, URC_CAPACITY> +where + W: Write, +{ + pub(crate) fn new( + writer: W, + res_reader: FrameConsumer<'a, RES_CAPACITY>, + urc_reader: FrameConsumer<'a, URC_CAPACITY>, + config: Config, + ) -> Self { + Self { + writer, + res_reader, + urc_reader, + cooldown_timer: None, + config, + } + } + + fn start_cooldown_timer(&mut self) { + self.cooldown_timer = Some(Timer::after(self.config.cmd_cooldown)); + } + + fn wait_cooldown_timer(&mut self) { + if let Some(cooldown) = self.cooldown_timer.take() { + cooldown.wait(); + } + } +} + +impl AtatClient + for Client<'_, W, RES_CAPACITY, URC_CAPACITY> +where + W: Write, +{ + fn send, const LEN: usize>(&mut self, cmd: &A) -> Result { + self.wait_cooldown_timer(); + + let cmd_bytes = cmd.as_bytes(); + let cmd_slice = cmd.get_slice(&cmd_bytes); + if cmd_slice.len() < 50 { + debug!("Sending command: {:?}", LossyStr(cmd_slice)); + } else { + debug!( + "Sending command with long payload ({} bytes)", + cmd_slice.len(), + ); + } + + self.writer + .write_all(cmd_slice) + .map_err(|_e| Error::Write)?; + self.writer.flush().map_err(|_e| Error::Write)?; + + if !A::EXPECTS_RESPONSE_CODE { + debug!("Command does not expect a response"); + self.start_cooldown_timer(); + return cmd.parse(Ok(&[])); + } + + let response = Timer::with_timeout(Duration::from_millis(A::MAX_TIMEOUT_MS.into()), || { + self.res_reader.read().map(|mut grant| { + grant.auto_release(true); + + let frame = Frame::decode(grant.as_ref()); + let resp = match Response::from(frame) { + Response::Result(r) => r, + Response::Prompt(_) => Ok(&[] as &[u8]), + }; + + cmd.parse(resp) + }) + }); + + self.start_cooldown_timer(); + response + } + + fn try_read_urc_with FnOnce(Urc::Response, &'b [u8]) -> bool>( + &mut self, + handle: F, + ) -> bool { + if let Some(urc_grant) = self.urc_reader.read() { + self.start_cooldown_timer(); + if let Some(urc) = Urc::parse(&urc_grant) { + if handle(urc, &urc_grant) { + urc_grant.release(); + return true; + } + } else { + error!("Parsing URC FAILED: {:?}", LossyStr(&urc_grant)); + urc_grant.release(); + } + } + + false + } + + fn max_urc_len() -> usize { + // bbqueue can only guarantee grant sizes of half its capacity if the queue is empty. + // A _frame_ grant returned by bbqueue has a header. Assume that it is 2 bytes. + (URC_CAPACITY / 2) - 2 + } +} + +#[cfg(test)] +mod test { + use crate::frame::FrameProducerExt; + + use super::*; + use crate::atat_derive::{AtatCmd, AtatEnum, AtatResp, AtatUrc}; + use crate::{self as atat, InternalError}; + use bbqueue::BBBuffer; + use heapless::String; + use serde_at::HexStr; + + const TEST_RX_BUF_LEN: usize = 256; + const TEST_RES_CAPACITY: usize = 3 * TEST_RX_BUF_LEN; + const TEST_URC_CAPACITY: usize = 3 * TEST_RX_BUF_LEN; + + #[derive(Debug)] + pub struct IoError; + + impl embedded_io::Error for IoError { + fn kind(&self) -> embedded_io::ErrorKind { + embedded_io::ErrorKind::Other + } + } + + struct TxMock { + s: String<64>, + } + + impl TxMock { + fn new(s: String<64>) -> Self { + TxMock { s } + } + } + + impl embedded_io::Io for TxMock { + type Error = IoError; + } + + impl embedded_io::blocking::Write for TxMock { + fn write(&mut self, buf: &[u8]) -> Result { + for c in buf { + self.s.push(*c as char).map_err(|_| IoError)?; + } + Ok(buf.len()) + } + + fn flush(&mut self) -> Result<(), Self::Error> { + Ok(()) + } + } + + #[derive(Debug, PartialEq, Eq)] + pub enum InnerError { + Test, + } + + impl core::str::FromStr for InnerError { + // This error will always get mapped to `atat::Error::Parse` + type Err = (); + + fn from_str(_s: &str) -> Result { + Ok(Self::Test) + } + } + + #[derive(Debug, PartialEq, AtatCmd)] + #[at_cmd("+CFUN", NoResponse, error = "InnerError")] + struct ErrorTester { + x: u8, + } + + #[derive(Clone, AtatCmd)] + #[at_cmd("+CFUN", NoResponse, timeout_ms = 180000)] + pub struct SetModuleFunctionality { + #[at_arg(position = 0)] + pub fun: Functionality, + #[at_arg(position = 1)] + pub rst: Option, + } + + #[derive(Clone, AtatCmd)] + #[at_cmd("+FUN", NoResponse, timeout_ms = 180000)] + pub struct Test2Cmd { + #[at_arg(position = 1)] + pub fun: Functionality, + #[at_arg(position = 0)] + pub rst: Option, + } + + #[derive(Clone, AtatCmd)] + #[at_cmd("+CUN", TestResponseString, timeout_ms = 180000)] + pub struct TestRespStringCmd { + #[at_arg(position = 0)] + pub fun: Functionality, + #[at_arg(position = 1)] + pub rst: Option, + } + #[derive(Clone, AtatCmd)] + #[at_cmd("+CUN", TestResponseStringMixed, timeout_ms = 180000, attempts = 1)] + pub struct TestRespStringMixCmd { + #[at_arg(position = 1)] + pub fun: Functionality, + #[at_arg(position = 0)] + pub rst: Option, + } + + // #[derive(Clone, AtatCmd)] + // #[at_cmd("+CUN", TestResponseStringMixed, timeout_ms = 180000)] + // pub struct TestUnnamedStruct(Functionality, Option); + + #[derive(Clone, PartialEq, AtatEnum)] + #[at_enum(u8)] + pub enum Functionality { + #[at_arg(value = 0)] + Min, + #[at_arg(value = 1)] + Full, + #[at_arg(value = 4)] + APM, + #[at_arg(value = 6)] + DM, + } + + #[derive(Clone, PartialEq, AtatEnum)] + #[at_enum(u8)] + pub enum ResetMode { + #[at_arg(value = 0)] + DontReset, + #[at_arg(value = 1)] + Reset, + } + #[derive(Clone, AtatResp, PartialEq, Debug)] + pub struct NoResponse; + + #[derive(Clone, AtatResp, PartialEq, Debug)] + pub struct TestResponseString { + #[at_arg(position = 0)] + pub socket: u8, + #[at_arg(position = 1)] + pub length: usize, + #[at_arg(position = 2)] + pub data: String<64>, + } + + #[derive(Clone, AtatResp, PartialEq, Debug)] + pub struct TestResponseStringMixed { + #[at_arg(position = 1)] + pub socket: u8, + #[at_arg(position = 2)] + pub length: usize, + #[at_arg(position = 0)] + pub data: String<64>, + } + + #[derive(Debug, Clone, AtatResp, PartialEq)] + pub struct MessageWaitingIndication { + #[at_arg(position = 0)] + pub status: u8, + #[at_arg(position = 1)] + pub code: u8, + } + + #[derive(Debug, Clone, AtatUrc, PartialEq)] + pub enum Urc { + #[at_urc(b"+UMWI")] + MessageWaitingIndication(MessageWaitingIndication), + #[at_urc(b"CONNECT OK")] + ConnectOk, + } + + macro_rules! setup { + ($config:expr) => {{ + static mut RES_Q: BBBuffer = BBBuffer::new(); + let (res_p, res_c) = unsafe { RES_Q.try_split_framed().unwrap() }; + + static mut URC_Q: BBBuffer = BBBuffer::new(); + let (urc_p, urc_c) = unsafe { URC_Q.try_split_framed().unwrap() }; + + let tx_mock = TxMock::new(String::new()); + let client: Client = + Client::new(tx_mock, res_c, urc_c, $config); + (client, res_p, urc_p) + }}; + } + + #[test] + fn error_response() { + let (mut client, mut p, _) = setup!(Config::new()); + + let cmd = ErrorTester { x: 7 }; + + p.try_enqueue(Err(InternalError::Error).into()).unwrap(); + + assert_eq!(client.send(&cmd), Err(Error::Error)); + } + + #[test] + fn generic_error_response() { + let (mut client, mut p, _) = setup!(Config::new()); + + let cmd = SetModuleFunctionality { + fun: Functionality::APM, + rst: Some(ResetMode::DontReset), + }; + + p.try_enqueue(Err(InternalError::Error).into()).unwrap(); + + assert_eq!(client.send(&cmd), Err(Error::Error)); + } + + #[test] + fn string_sent() { + let (mut client, mut p, _) = setup!(Config::new()); + + let cmd = SetModuleFunctionality { + fun: Functionality::APM, + rst: Some(ResetMode::DontReset), + }; + + p.try_enqueue(Frame::Response(&[])).unwrap(); + + assert_eq!(client.send(&cmd), Ok(NoResponse)); + + assert_eq!( + client.writer.s, + String::<32>::from("AT+CFUN=4,0\r\n"), + "Wrong encoding of string" + ); + + p.try_enqueue(Frame::Response(&[])).unwrap(); + + let cmd = Test2Cmd { + fun: Functionality::DM, + rst: Some(ResetMode::Reset), + }; + assert_eq!(client.send(&cmd), Ok(NoResponse)); + + assert_eq!( + client.writer.s, + String::<32>::from("AT+CFUN=4,0\r\nAT+FUN=1,6\r\n"), + "Reverse order string did not match" + ); + } + + #[test] + fn blocking() { + let (mut client, mut p, _) = setup!(Config::new()); + + let cmd = SetModuleFunctionality { + fun: Functionality::APM, + rst: Some(ResetMode::DontReset), + }; + + p.try_enqueue(Frame::Response(&[])).unwrap(); + + assert_eq!(client.send(&cmd), Ok(NoResponse)); + assert_eq!(client.writer.s, String::<32>::from("AT+CFUN=4,0\r\n")); + } + + // Test response containing string + #[test] + fn response_string() { + let (mut client, mut p, _) = setup!(Config::new()); + + // String last + let cmd = TestRespStringCmd { + fun: Functionality::APM, + rst: Some(ResetMode::DontReset), + }; + + let response = b"+CUN: 22,16,\"0123456789012345\""; + p.try_enqueue(Frame::Response(response)).unwrap(); + + assert_eq!( + client.send(&cmd), + Ok(TestResponseString { + socket: 22, + length: 16, + data: String::<64>::from("0123456789012345") + }) + ); + + // Mixed order for string + let cmd = TestRespStringMixCmd { + fun: Functionality::APM, + rst: Some(ResetMode::DontReset), + }; + + let response = b"+CUN: \"0123456789012345\",22,16"; + p.try_enqueue(Frame::Response(response)).unwrap(); + + assert_eq!( + client.send(&cmd), + Ok(TestResponseStringMixed { + socket: 22, + length: 16, + data: String::<64>::from("0123456789012345") + }) + ); + } + + #[test] + fn urc() { + let (mut client, _, mut urc_p) = setup!(Config::new()); + + let response = b"+UMWI: 0, 1"; + + let mut grant = urc_p.grant(response.len()).unwrap(); + grant.copy_from_slice(response.as_ref()); + grant.commit(response.len()); + + assert!(client.try_read_urc::().is_some()); + } + + #[test] + fn urc_keyword() { + let (mut client, _, mut urc_p) = setup!(Config::new()); + + let response = b"CONNECT OK"; + + let mut grant = urc_p.grant(response.len()).unwrap(); + grant.copy_from_slice(response.as_ref()); + grant.commit(response.len()); + + assert_eq!(Urc::ConnectOk, client.try_read_urc::().unwrap()); + } + + #[test] + fn invalid_response() { + let (mut client, mut p, _) = setup!(Config::new()); + + // String last + let cmd = TestRespStringCmd { + fun: Functionality::APM, + rst: Some(ResetMode::DontReset), + }; + + let response = b"+CUN: 22,16,22"; + p.try_enqueue(Frame::Response(response)).unwrap(); + + assert_eq!(client.send(&cmd), Err(Error::Parse)); + } + + #[test] + fn quote_and_no_quote_strings() { + #[derive(Clone, PartialEq, AtatCmd)] + #[at_cmd("+DEVEUI", NoResponse)] + pub struct WithQuoteNoValHexStr { + pub val: HexStr, + } + + let val = HexStr { + val: 0xA0F5, + ..Default::default() + }; + let val = WithQuoteNoValHexStr { val }; + let b = val.as_bytes(); + let s = core::str::from_utf8(&b).unwrap(); + assert_eq!(s, "AT+DEVEUI=\"A0F5\"\r\n"); + + #[derive(Clone, PartialEq, AtatCmd)] + #[at_cmd("+DEVEUI", NoResponse, quote_escape_strings = true)] + pub struct WithQuoteHexStr { + pub val: HexStr, + } + + let val = HexStr { + val: 0xA0F5, + ..Default::default() + }; + let val = WithQuoteHexStr { val }; + let b = val.as_bytes(); + let s = core::str::from_utf8(&b).unwrap(); + assert_eq!(s, "AT+DEVEUI=\"A0F5\"\r\n"); + + #[derive(Clone, PartialEq, AtatCmd)] + #[at_cmd("+DEVEUI", NoResponse, quote_escape_strings = false)] + pub struct WithoutQuoteHexStr { + pub val: HexStr, + } + + let val = HexStr { + val: 0xA0F5_A0F5_A0F5_A0F5_A0F5_A0F5_A0F5_A0F5, + ..Default::default() + }; + let val = WithoutQuoteHexStr { val }; + let b = val.as_bytes(); + let s = core::str::from_utf8(&b).unwrap(); + assert_eq!(s, "AT+DEVEUI=A0F5A0F5A0F5A0F5A0F5A0F5A0F5A0F5\r\n"); + } + + // #[test] + // fn tx_timeout() { + // let timeout = Duration::from_millis(20); + // let (mut client, mut p, _) = setup!(Config::new().tx_timeout(1)); + + // let cmd = SetModuleFunctionality { + // fun: Functionality::APM, + // rst: Some(ResetMode::DontReset), + // }; + + // p.try_enqueue(Frame::Response(&[])).unwrap(); + + // assert_eq!(client.send(&cmd), Err(Error::Timeout)); + // } + + // #[test] + // fn flush_timeout() { + // let timeout = Duration::from_millis(20); + // let (mut client, mut p, _) = setup!(Config::new().flush_timeout(1)); + + // let cmd = SetModuleFunctionality { + // fun: Functionality::APM, + // rst: Some(ResetMode::DontReset), + // }; + + // p.try_enqueue(Frame::Response(&[])).unwrap(); + + // assert_eq!(client.send(&cmd), Err(Error::Timeout)); + // } +} diff --git a/atat/src/blocking/mod.rs b/atat/src/blocking/mod.rs new file mode 100644 index 00000000..249972be --- /dev/null +++ b/atat/src/blocking/mod.rs @@ -0,0 +1,82 @@ +mod client; +mod timer; + +pub use client::Client; + +use crate::{AtatCmd, AtatUrc, Error}; + +pub trait AtatClient { + /// Send an AT command. + /// + /// `cmd` must implement [`AtatCmd`]. + /// + /// This function will block until a response is received, if in Timeout or + /// Blocking mode. In Nonblocking mode, the send can be called until it no + /// longer returns `nb::Error::WouldBlock`, or `self.check_response(cmd)` can + /// be called, with the same result. + /// + /// This function will also make sure that at least `self.config.cmd_cooldown` + /// has passed since the last response or URC has been received, to allow + /// the slave AT device time to deliver URC's. + fn send, const LEN: usize>(&mut self, cmd: &A) -> Result; + + fn send_retry, const LEN: usize>( + &mut self, + cmd: &A, + ) -> Result { + for attempt in 1..=A::ATTEMPTS { + if attempt > 1 { + debug!("Attempt {}:", attempt); + } + + match self.send(cmd) { + Err(Error::Timeout) => {} + r => return r, + } + } + Err(Error::Timeout) + } + + /// Checks if there are any URC's (Unsolicited Response Code) in + /// queue from the ingress manager. + /// + /// Example: + /// ``` + /// use atat::atat_derive::{AtatResp, AtatUrc}; + /// + /// #[derive(Clone, AtatResp)] + /// pub struct MessageWaitingIndication { + /// #[at_arg(position = 0)] + /// pub status: u8, + /// #[at_arg(position = 1)] + /// pub code: u8, + /// } + /// + /// #[derive(Clone, AtatUrc)] + /// pub enum Urc { + /// #[at_urc("+UMWI")] + /// MessageWaitingIndication(MessageWaitingIndication), + /// } + /// + /// // match client.check_urc::() { + /// // Some(Urc::MessageWaitingIndication(MessageWaitingIndication { status, code })) => { + /// // // Do something to act on `+UMWI` URC + /// // } + /// // } + /// ``` + fn try_read_urc(&mut self) -> Option { + let mut first = None; + self.try_read_urc_with::(|urc, _| { + first = Some(urc); + true + }); + first + } + + fn try_read_urc_with FnOnce(Urc::Response, &'b [u8]) -> bool>( + &mut self, + handle: F, + ) -> bool; + + fn max_urc_len() -> usize; +} diff --git a/atat/src/blocking/timer.rs b/atat/src/blocking/timer.rs new file mode 100644 index 00000000..fae155c8 --- /dev/null +++ b/atat/src/blocking/timer.rs @@ -0,0 +1,38 @@ +use crate::error::Error; +use embassy_time::{Duration, Instant}; + +pub struct Timer { + expires_at: Instant, +} + +impl Timer { + pub fn after(duration: Duration) -> Self { + Self { + expires_at: Instant::now() + duration, + } + } + + pub fn with_timeout(timeout: Duration, mut e: F) -> Result + where + F: FnMut() -> Option>, + { + let timer = Timer::after(timeout); + + loop { + if let Some(res) = e() { + return res; + } + if timer.expires_at <= Instant::now() { + return Err(Error::Timeout); + } + } + } + + pub fn wait(self) { + loop { + if self.expires_at <= Instant::now() { + break; + } + } + } +} diff --git a/atat/src/buffers.rs b/atat/src/buffers.rs new file mode 100644 index 00000000..b226beba --- /dev/null +++ b/atat/src/buffers.rs @@ -0,0 +1,143 @@ +use bbqueue::BBBuffer; +use embedded_io::blocking::Write; + +use crate::{Config, Digester, Ingress}; + +/// Buffer size safety +/// +/// BBQueue can only guarantee that issued write grants have half the size of its capacity. +/// In framed mode, each raw grant is prefixed with the size of the bbqueue frame. +/// We expect no larger frames than what can fit in a u16. For each [`crate::frame::Frame`] that is enqueued in the response queue, +/// a binconde dispatch byte is also appended (we use variable int encoding). +/// This means that to write an N byte response, we need a (3 + N) byte grant from the (non-framed) BBQueue. +/// URC's are not wrapped in a [`crate::frame::Frame`] and hence does not need the dispatch byte. +/// +/// The reason why this is behind the async feature flag is that it requires rust nightly. +/// Also, [`crate::AtatIngress.try_advance()`] (the non-async version) can return error if there is no room in the queues, +/// where the async equivalent simply returns () as it assumes that there at some point will be room in the queue. +/// +/// One more additional note: We assume in the conditions that the digest result is never larger than the bytes that were input to the digester. +#[cfg(feature = "async")] +mod buf_safety { + pub struct ConstCheck; + + const BBQUEUE_FRAME_HEADER_SIZE: usize = 2; + const RES_FRAME_DISPATCH_SIZE: usize = 1; + + pub trait True {} + impl True for ConstCheck {} + + pub const fn is_valid_res_capacity( + ) -> bool { + RES_CAPACITY >= 2 * (BBQUEUE_FRAME_HEADER_SIZE + RES_FRAME_DISPATCH_SIZE + INGRESS_BUF_SIZE) + } + + pub const fn is_valid_urc_capacity( + ) -> bool { + URC_CAPACITY == 0 || URC_CAPACITY >= 2 * (BBQUEUE_FRAME_HEADER_SIZE + INGRESS_BUF_SIZE) + } +} + +pub struct Buffers< + const INGRESS_BUF_SIZE: usize, + const RES_CAPACITY: usize, + const URC_CAPACITY: usize, +> { + res_queue: BBBuffer, + urc_queue: BBBuffer, +} + +#[cfg(feature = "async")] +impl + Buffers +where + buf_safety::ConstCheck< + { buf_safety::is_valid_res_capacity::() }, + >: buf_safety::True, + buf_safety::ConstCheck< + { buf_safety::is_valid_urc_capacity::() }, + >: buf_safety::True, +{ + pub const fn new() -> Self { + Self { + res_queue: BBBuffer::new(), + urc_queue: BBBuffer::new(), + } + } +} + +#[cfg(not(feature = "async"))] +impl + Buffers +{ + pub const fn new() -> Self { + Self { + res_queue: BBBuffer::new(), + urc_queue: BBBuffer::new(), + } + } +} + +impl + Buffers +{ + #[cfg(feature = "async")] + pub fn split( + &self, + writer: W, + digester: D, + config: Config, + ) -> ( + Ingress<'_, D, INGRESS_BUF_SIZE, RES_CAPACITY, URC_CAPACITY>, + crate::asynch::Client<'_, W, RES_CAPACITY, URC_CAPACITY>, + ) { + let (res_writer, res_reader) = self.res_queue.try_split_framed().unwrap(); + let (urc_writer, urc_reader) = self.urc_queue.try_split_framed().unwrap(); + + ( + Ingress::new(digester, res_writer, urc_writer), + crate::asynch::Client::new(writer, res_reader, urc_reader, config), + ) + } + + pub fn split_blocking( + &self, + writer: W, + digester: D, + config: Config, + ) -> ( + Ingress<'_, D, INGRESS_BUF_SIZE, RES_CAPACITY, URC_CAPACITY>, + crate::blocking::Client<'_, W, RES_CAPACITY, URC_CAPACITY>, + ) { + let (res_writer, res_reader) = self.res_queue.try_split_framed().unwrap(); + let (urc_writer, urc_reader) = self.urc_queue.try_split_framed().unwrap(); + + ( + Ingress::new(digester, res_writer, urc_writer), + crate::blocking::Client::new(writer, res_reader, urc_reader, config), + ) + } +} + +#[cfg(test)] +mod tests { + #[test] + fn show_why_we_need_two_times_bbqueue_capacity() { + // If this test starts to fail in the future, then it may be because + // bbqueue has relaxed its granting strategy, in which case the + // buffer size safety checks should be revisisted. + + let buffer = bbqueue::BBBuffer::<16>::new(); + let (mut producer, mut consumer) = buffer.try_split().unwrap(); + let grant = producer.grant_exact(9).unwrap(); + grant.commit(9); + let grant = consumer.read().unwrap(); + grant.release(9); + + assert_eq!( + Err(bbqueue::Error::InsufficientSize), + producer.grant_exact(9) + ); + assert!(producer.grant_exact(8).is_ok()); + } +} diff --git a/atat/src/builder.rs b/atat/src/builder.rs deleted file mode 100644 index 7918b294..00000000 --- a/atat/src/builder.rs +++ /dev/null @@ -1,91 +0,0 @@ -use crate::{digest::Digester, Client, Config, IngressManager, Queues}; - -type ClientParser< - Tx, - T, - D, - const TIMER_HZ: u32, - const BUF_LEN: usize, - const RES_CAPACITY: usize, - const URC_CAPACITY: usize, -> = ( - Client, - IngressManager, -); - -/// Builder to set up a [`Client`] and [`IngressManager`] pair. -/// -/// Create a new builder through the [`new`] method. -/// -/// [`Client`]: struct.Client.html -/// [`IngressManager`]: struct.IngressManager.html -/// [`new`]: #method.new -pub struct ClientBuilder< - Tx, - T, - D, - const TIMER_HZ: u32, - const BUF_LEN: usize, - const RES_CAPACITY: usize, - const URC_CAPACITY: usize, -> where - Tx: embedded_hal_nb::serial::Write, - T: fugit_timer::Timer, - D: Digester, -{ - serial_tx: Tx, - timer: T, - config: Config, - digester: D, -} - -impl< - Tx, - T, - D, - const TIMER_HZ: u32, - const BUF_LEN: usize, - const RES_CAPACITY: usize, - const URC_CAPACITY: usize, - > ClientBuilder -where - Tx: embedded_hal_nb::serial::Write, - T: fugit_timer::Timer, - D: Digester, -{ - /// Create a builder for new Atat client instance. - /// - /// The `serial_tx` type must implement the `embedded_hal_nb` - /// [`serial::Write`][serialwrite] trait while the timer must implement - /// the [`fugit_timer::Timer`] trait. - /// - /// [serialwrite]: ../embedded_hal_nb/serial/trait.Write.html - pub const fn new(serial_tx: Tx, timer: T, digester: D, config: Config) -> Self { - Self { - serial_tx, - timer, - config, - digester, - } - } - - /// Set up and return a [`Client`] and [`IngressManager`] pair. - /// - /// [`Client`]: struct.Client.html - /// [`IngressManager`]: struct.IngressManager.html - pub fn build( - self, - queues: Queues, - ) -> ClientParser { - let parser = IngressManager::new(queues.res_queue.0, queues.urc_queue.0, self.digester); - let client = Client::new( - self.serial_tx, - queues.res_queue.1, - queues.urc_queue.1, - self.timer, - self.config, - ); - - (client, parser) - } -} diff --git a/atat/src/client.rs b/atat/src/client.rs deleted file mode 100644 index 3cbe30f1..00000000 --- a/atat/src/client.rs +++ /dev/null @@ -1,821 +0,0 @@ -use bbqueue::framed::FrameConsumer; -use embedded_hal_nb::{nb, serial}; -use fugit::ExtU32; - -use crate::error::{Error, Response}; -use crate::helpers::LossyStr; -use crate::traits::{AtatClient, AtatCmd, AtatUrc}; -use crate::Config; - -#[derive(Debug, PartialEq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -enum ClientState { - Idle, - AwaitingResponse, -} - -/// Whether the AT client should block while waiting responses or return early. -#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum Mode { - /// The function call will wait as long as necessary to complete the operation - Blocking, - /// The function call will not wait at all to complete the operation, and only do what it can. - NonBlocking, - /// The function call will wait only up the max timeout of each command to complete the operation. - Timeout, -} - -/// Client responsible for handling send, receive and timeout from the -/// userfacing side. The client is decoupled from the ingress-manager through -/// some spsc queue consumers, where any received responses can be dequeued. The -/// Client also has an spsc producer, to allow signaling commands like -/// `reset` to the ingress-manager. -pub struct Client< - Tx, - CLK, - const TIMER_HZ: u32, - const RES_CAPACITY: usize, - const URC_CAPACITY: usize, -> where - Tx: serial::Write, - CLK: fugit_timer::Timer, -{ - /// Serial writer - tx: Tx, - - /// The response consumer receives responses from the ingress manager - res_c: FrameConsumer<'static, RES_CAPACITY>, - /// The URC consumer receives URCs from the ingress manager - urc_c: FrameConsumer<'static, URC_CAPACITY>, - - state: ClientState, - timer: CLK, - config: Config, -} - -impl - Client -where - Tx: serial::Write, - CLK: fugit_timer::Timer, -{ - pub fn new( - tx: Tx, - res_c: FrameConsumer<'static, RES_CAPACITY>, - urc_c: FrameConsumer<'static, URC_CAPACITY>, - mut timer: CLK, - config: Config, - ) -> Self { - timer.start(config.cmd_cooldown.millis()).ok(); - - Self { - tx, - res_c, - urc_c, - state: ClientState::Idle, - config, - timer, - } - } -} - -/// Blocks on `nb` function calls but allow to time out -/// Example of usage: -/// `block_timeout!((client.timer, client.config.tx_timeout) => {client.tx.write(c)}.map_err(|_e| Error::Write))?;` -/// `block_timeout!((client.timer, client.config.tx_timeout) => {client.tx.write(c)});` -#[macro_export] -macro_rules! block_timeout { - ($timer:expr, $duration:expr, $e:expr, $map_err:expr) => {{ - if $duration == 0 { - nb::block!($e).map_err($map_err) - } else { - $timer.start($duration.millis()).ok(); - loop { - match $e { - Err(nb::Error::WouldBlock) => (), - Err(nb::Error::Other(e)) => break Err($map_err(e)), - Ok(r) => break Ok(r), - }; - match $timer.wait() { - Err(nb::Error::WouldBlock) => (), - Err(_) => break Err(Error::Write), - Ok(_) => break Err(Error::Timeout), - }; - } - } - }}; - (($timer:expr, $duration:expr) => {$e:expr}) => {{ - block_timeout!($timer, $duration, $e, |e| { e }) - }}; - (($timer:expr, $duration:expr) => {$e:expr}.map_err($map_err:expr)) => {{ - block_timeout!($timer, $duration, $e, $map_err) - }}; -} - -impl AtatClient - for Client -where - Tx: serial::Write, - CLK: fugit_timer::Timer, -{ - fn send, const LEN: usize>( - &mut self, - cmd: &A, - ) -> nb::Result { - if self.state == ClientState::Idle { - // compare the time of the last response or URC and ensure at least - // `self.config.cmd_cooldown` ms have passed before sending a new - // command - nb::block!(self.timer.wait()).ok(); - let cmd_buf = cmd.as_bytes(); - - if cmd_buf.len() < 50 { - debug!("Sending command: \"{:?}\"", LossyStr(&cmd_buf)); - } else { - debug!( - "Sending command with too long payload ({} bytes) to log!", - cmd_buf.len(), - ); - } - - for c in cmd_buf { - block_timeout!((self.timer, self.config.tx_timeout) => {self.tx.write(c)}.map_err(|_e| Error::Write))?; - } - block_timeout!((self.timer, self.config.flush_timeout) => {self.tx.flush()}.map_err(|_e| Error::Write))?; - - self.state = ClientState::AwaitingResponse; - } - - if !A::EXPECTS_RESPONSE_CODE { - self.state = ClientState::Idle; - return cmd.parse(Ok(&[])).map_err(nb::Error::Other); - } - - match self.config.mode { - Mode::Blocking => Ok(nb::block!(self.check_response(cmd))?), - Mode::NonBlocking => self.check_response(cmd), - Mode::Timeout => { - self.timer.start(A::MAX_TIMEOUT_MS.millis()).ok(); - Ok(nb::block!(self.check_response(cmd))?) - } - } - } - - fn peek_urc_with bool>(&mut self, f: F) { - if let Some(urc_grant) = self.urc_c.read() { - self.timer.start(self.config.cmd_cooldown.millis()).ok(); - if let Some(urc) = URC::parse(urc_grant.as_ref()) { - if !f(urc) { - return; - } - } else { - error!("Parsing URC FAILED: {:?}", LossyStr(urc_grant.as_ref())); - } - urc_grant.release(); - } - } - - fn check_response, const LEN: usize>( - &mut self, - cmd: &A, - ) -> nb::Result { - if let Some(mut res_grant) = self.res_c.read() { - res_grant.auto_release(true); - - let res = match Response::from(res_grant.as_ref()) { - Response::Result(r) => r, - Response::Prompt(_) => Ok(&[][..]), - }; - - return cmd - .parse(res) - .map_err(nb::Error::from) - .and_then(|r| { - if self.state == ClientState::AwaitingResponse { - self.timer.start(self.config.cmd_cooldown.millis()).ok(); - self.state = ClientState::Idle; - Ok(r) - } else { - Err(nb::Error::WouldBlock) - } - }) - .map_err(|e| { - self.timer.start(self.config.cmd_cooldown.millis()).ok(); - self.state = ClientState::Idle; - e - }); - } else if self.config.mode == Mode::Timeout && self.timer.wait().is_ok() { - self.state = ClientState::Idle; - return Err(nb::Error::Other(Error::Timeout)); - } - Err(nb::Error::WouldBlock) - } - - fn get_mode(&self) -> Mode { - self.config.mode - } - - fn reset(&mut self) { - while let Some(grant) = self.res_c.read() { - grant.release(); - } - - while let Some(grant) = self.urc_c.read() { - grant.release(); - } - } -} - -#[cfg(test)] -mod test { - use std::sync::mpsc; - use std::thread::{self, JoinHandle}; - use std::time::{Duration, Instant}; - - use super::*; - use crate::{self as atat, InternalError}; - use crate::{ - atat_derive::{AtatCmd, AtatEnum, AtatResp, AtatUrc}, - clock::Clock, - }; - use bbqueue::framed::FrameProducer; - use bbqueue::BBBuffer; - use heapless::String; - use serde_at::HexStr; - - const TEST_RX_BUF_LEN: usize = 256; - const TEST_RES_CAPACITY: usize = 3 * TEST_RX_BUF_LEN; - const TEST_URC_CAPACITY: usize = 3 * TEST_RX_BUF_LEN; - const TIMER_HZ: u32 = 1000; - - struct CdMock { - handle: Option>, - trigger: Option>, - } - impl CdMock { - fn new() -> Self { - CdMock { - handle: None, - trigger: None, - } - } - } - - impl Clock for CdMock { - type Error = core::convert::Infallible; - - /// Return current time `Instant` - fn now(&mut self) -> fugit::TimerInstantU32 { - fugit::TimerInstantU32::from_ticks(0) - } - - /// Start countdown with a `duration` - fn start( - &mut self, - duration: fugit::TimerDurationU32, - ) -> Result<(), Self::Error> { - let (tx, rx) = mpsc::channel(); - self.trigger = Some(tx.clone()); - - thread::spawn(move || { - let trigger = tx.clone(); - thread::sleep(Duration::from_millis(duration.to_millis() as u64)); - trigger.send(true).unwrap(); - }); - - let handle = thread::spawn(move || loop { - match rx.recv() { - Ok(ticked) => break ticked, - _ => break false, - } - }); - self.handle.replace(handle); - Ok(()) - } - - /// Stop timer - fn cancel(&mut self) -> Result<(), Self::Error> { - match self.trigger.take() { - Some(trigger) => Ok(trigger.send(false).unwrap()), - None => Ok(()), - } - } - - /// Wait until countdown `duration` set with the `fn start` has expired - fn wait(&mut self) -> nb::Result<(), Self::Error> { - match &self.handle { - Some(handle) => match handle.is_finished() { - true => self.handle = None, - false => Err(nb::Error::WouldBlock)?, - }, - None => (), - } - Ok(()) - } - } - - #[derive(Debug)] - pub enum SerialError {} - - impl serial::Error for SerialError { - fn kind(&self) -> serial::ErrorKind { - serial::ErrorKind::Other - } - } - - struct TxMock { - s: String<64>, - timeout: Option, - timer_start: Option, - } - - impl TxMock { - fn new(s: String<64>, timeout: Option) -> Self { - TxMock { - s, - timeout, - timer_start: None, - } - } - } - - impl serial::ErrorType for TxMock { - type Error = serial::ErrorKind; - } - - impl serial::Write for TxMock { - fn write(&mut self, c: u8) -> nb::Result<(), Self::Error> { - if let Some(timeout) = self.timeout { - if self.timer_start.get_or_insert(Instant::now()).elapsed() < timeout { - return Err(nb::Error::WouldBlock); - } - self.timer_start.take(); - } - self.s - .push(c as char) - .map_err(|_| nb::Error::Other(serial::ErrorKind::Other)) - } - - fn flush(&mut self) -> nb::Result<(), Self::Error> { - if let Some(timeout) = self.timeout { - if self.timer_start.get_or_insert(Instant::now()).elapsed() < timeout { - return Err(nb::Error::WouldBlock); - } - self.timer_start.take(); - } - Ok(()) - } - } - - #[derive(Debug, PartialEq, Eq)] - pub enum InnerError { - Test, - } - - impl core::str::FromStr for InnerError { - // This error will always get mapped to `atat::Error::Parse` - type Err = (); - - fn from_str(_s: &str) -> Result { - Ok(Self::Test) - } - } - - #[derive(Debug, PartialEq, AtatCmd)] - #[at_cmd("+CFUN", NoResponse, error = "InnerError")] - struct ErrorTester { - x: u8, - } - - #[derive(Clone, AtatCmd)] - #[at_cmd("+CFUN", NoResponse, timeout_ms = 180000)] - pub struct SetModuleFunctionality { - #[at_arg(position = 0)] - pub fun: Functionality, - #[at_arg(position = 1)] - pub rst: Option, - } - - #[derive(Clone, AtatCmd)] - #[at_cmd("+FUN", NoResponse, timeout_ms = 180000)] - pub struct Test2Cmd { - #[at_arg(position = 1)] - pub fun: Functionality, - #[at_arg(position = 0)] - pub rst: Option, - } - - #[derive(Clone, AtatCmd)] - #[at_cmd("+CUN", TestResponseString, timeout_ms = 180000)] - pub struct TestRespStringCmd { - #[at_arg(position = 0)] - pub fun: Functionality, - #[at_arg(position = 1)] - pub rst: Option, - } - #[derive(Clone, AtatCmd)] - #[at_cmd("+CUN", TestResponseStringMixed, timeout_ms = 180000, attempts = 1)] - pub struct TestRespStringMixCmd { - #[at_arg(position = 1)] - pub fun: Functionality, - #[at_arg(position = 0)] - pub rst: Option, - } - - // #[derive(Clone, AtatCmd)] - // #[at_cmd("+CUN", TestResponseStringMixed, timeout_ms = 180000)] - // pub struct TestUnnamedStruct(Functionality, Option); - - #[derive(Clone, PartialEq, AtatEnum)] - #[at_enum(u8)] - pub enum Functionality { - #[at_arg(value = 0)] - Min, - #[at_arg(value = 1)] - Full, - #[at_arg(value = 4)] - APM, - #[at_arg(value = 6)] - DM, - } - - #[derive(Clone, PartialEq, AtatEnum)] - #[at_enum(u8)] - pub enum ResetMode { - #[at_arg(value = 0)] - DontReset, - #[at_arg(value = 1)] - Reset, - } - #[derive(Clone, AtatResp, PartialEq, Debug)] - pub struct NoResponse; - - #[derive(Clone, AtatResp, PartialEq, Debug)] - pub struct TestResponseString { - #[at_arg(position = 0)] - pub socket: u8, - #[at_arg(position = 1)] - pub length: usize, - #[at_arg(position = 2)] - pub data: String<64>, - } - - #[derive(Clone, AtatResp, PartialEq, Debug)] - pub struct TestResponseStringMixed { - #[at_arg(position = 1)] - pub socket: u8, - #[at_arg(position = 2)] - pub length: usize, - #[at_arg(position = 0)] - pub data: String<64>, - } - - #[derive(Debug, Clone, AtatResp, PartialEq)] - pub struct MessageWaitingIndication { - #[at_arg(position = 0)] - pub status: u8, - #[at_arg(position = 1)] - pub code: u8, - } - - #[derive(Debug, Clone, AtatUrc, PartialEq)] - pub enum Urc { - #[at_urc(b"+UMWI")] - MessageWaitingIndication(MessageWaitingIndication), - #[at_urc(b"CONNECT OK")] - ConnectOk, - } - - macro_rules! setup { - ($config:expr => $timeout:expr) => {{ - static mut RES_Q: BBBuffer = BBBuffer::new(); - let (res_p, res_c) = unsafe { RES_Q.try_split_framed().unwrap() }; - - static mut URC_Q: BBBuffer = BBBuffer::new(); - let (urc_p, urc_c) = unsafe { URC_Q.try_split_framed().unwrap() }; - - let tx_mock = TxMock::new(String::new(), $timeout); - let client: Client< - TxMock, - CdMock, - TIMER_HZ, - TEST_RES_CAPACITY, - TEST_URC_CAPACITY, - > = Client::new(tx_mock, res_c, urc_c, CdMock::new(), $config); - (client, res_p, urc_p) - }}; - ($config:expr, $timeout:expr) => {{ - setup!($config => Some($timeout)) - }}; - ($config:expr) => {{ - setup!($config => None) - }}; - } - - pub fn enqueue_res( - producer: &mut FrameProducer<'static, TEST_RES_CAPACITY>, - res: Result<&[u8], InternalError>, - ) { - let header: crate::error::Encoded = res.into(); - - let mut grant = producer.grant(header.len()).unwrap(); - match header { - crate::error::Encoded::Simple(h) => grant[..1].copy_from_slice(&[h]), - crate::error::Encoded::Nested(h, b) => { - grant[..1].copy_from_slice(&[h]); - grant[1..2].copy_from_slice(&[b]); - } - crate::error::Encoded::Array(h, b) => { - grant[..1].copy_from_slice(&[h]); - grant[1..header.len()].copy_from_slice(&b); - } - crate::error::Encoded::Slice(h, b) => { - grant[..1].copy_from_slice(&[h]); - grant[1..header.len()].copy_from_slice(b); - } - }; - grant.commit(header.len()); - } - - #[test] - fn error_response() { - let (mut client, mut p, _) = setup!(Config::new(Mode::Blocking)); - - let cmd = ErrorTester { x: 7 }; - - enqueue_res(&mut p, Err(InternalError::Error)); - - assert_eq!(client.state, ClientState::Idle); - assert_eq!(nb::block!(client.send(&cmd)), Err(Error::Error)); - assert_eq!(client.state, ClientState::Idle); - } - - #[test] - fn generic_error_response() { - let (mut client, mut p, _) = setup!(Config::new(Mode::Blocking)); - - let cmd = SetModuleFunctionality { - fun: Functionality::APM, - rst: Some(ResetMode::DontReset), - }; - - enqueue_res(&mut p, Err(InternalError::Error)); - - assert_eq!(client.state, ClientState::Idle); - assert_eq!(nb::block!(client.send(&cmd)), Err(Error::Error)); - assert_eq!(client.state, ClientState::Idle); - } - - #[test] - fn string_sent() { - let (mut client, mut p, _) = setup!(Config::new(Mode::Blocking)); - - let cmd = SetModuleFunctionality { - fun: Functionality::APM, - rst: Some(ResetMode::DontReset), - }; - - enqueue_res(&mut p, Ok(&[])); - - assert_eq!(client.state, ClientState::Idle); - assert_eq!(client.send(&cmd), Ok(NoResponse)); - assert_eq!(client.state, ClientState::Idle); - - assert_eq!( - client.tx.s, - String::<32>::from("AT+CFUN=4,0\r\n"), - "Wrong encoding of string" - ); - - enqueue_res(&mut p, Ok(&[])); - - let cmd = Test2Cmd { - fun: Functionality::DM, - rst: Some(ResetMode::Reset), - }; - assert_eq!(client.send(&cmd), Ok(NoResponse)); - - assert_eq!( - client.tx.s, - String::<32>::from("AT+CFUN=4,0\r\nAT+FUN=1,6\r\n"), - "Reverse order string did not match" - ); - } - - #[test] - fn blocking() { - let (mut client, mut p, _) = setup!(Config::new(Mode::Blocking)); - - let cmd = SetModuleFunctionality { - fun: Functionality::APM, - rst: Some(ResetMode::DontReset), - }; - - enqueue_res(&mut p, Ok(&[])); - - assert_eq!(client.state, ClientState::Idle); - assert_eq!(client.send(&cmd), Ok(NoResponse)); - assert_eq!(client.state, ClientState::Idle); - assert_eq!(client.tx.s, String::<32>::from("AT+CFUN=4,0\r\n")); - } - - #[test] - fn non_blocking() { - let (mut client, mut p, _) = setup!(Config::new(Mode::NonBlocking)); - - let cmd = SetModuleFunctionality { - fun: Functionality::APM, - rst: Some(ResetMode::DontReset), - }; - - assert_eq!(client.state, ClientState::Idle); - assert_eq!(client.send(&cmd), Err(nb::Error::WouldBlock)); - assert_eq!(client.state, ClientState::AwaitingResponse); - - assert_eq!(client.check_response(&cmd), Err(nb::Error::WouldBlock)); - - enqueue_res(&mut p, Ok(&[])); - - assert_eq!(client.state, ClientState::AwaitingResponse); - - assert_eq!(client.check_response(&cmd), Ok(NoResponse)); - assert_eq!(client.state, ClientState::Idle); - } - - // Test response containing string - #[test] - fn response_string() { - let (mut client, mut p, _) = setup!(Config::new(Mode::Blocking)); - - // String last - let cmd = TestRespStringCmd { - fun: Functionality::APM, - rst: Some(ResetMode::DontReset), - }; - - let response = b"+CUN: 22,16,\"0123456789012345\""; - enqueue_res(&mut p, Ok(response)); - - assert_eq!(client.state, ClientState::Idle); - - assert_eq!( - client.send(&cmd), - Ok(TestResponseString { - socket: 22, - length: 16, - data: String::<64>::from("0123456789012345") - }) - ); - assert_eq!(client.state, ClientState::Idle); - - // Mixed order for string - let cmd = TestRespStringMixCmd { - fun: Functionality::APM, - rst: Some(ResetMode::DontReset), - }; - - let response = b"+CUN: \"0123456789012345\",22,16"; - enqueue_res(&mut p, Ok(response)); - - assert_eq!( - client.send(&cmd), - Ok(TestResponseStringMixed { - socket: 22, - length: 16, - data: String::<64>::from("0123456789012345") - }) - ); - assert_eq!(client.state, ClientState::Idle); - } - - #[test] - fn urc() { - let (mut client, _, mut urc_p) = setup!(Config::new(Mode::NonBlocking)); - - let response = b"+UMWI: 0, 1"; - - let mut grant = urc_p.grant(response.len()).unwrap(); - grant.copy_from_slice(response.as_ref()); - grant.commit(response.len()); - - assert_eq!(client.state, ClientState::Idle); - assert!(client.check_urc::().is_some()); - assert_eq!(client.state, ClientState::Idle); - } - - #[test] - fn urc_keyword() { - let (mut client, _, mut urc_p) = setup!(Config::new(Mode::NonBlocking)); - - let response = b"CONNECT OK"; - - let mut grant = urc_p.grant(response.len()).unwrap(); - grant.copy_from_slice(response.as_ref()); - grant.commit(response.len()); - - assert_eq!(client.state, ClientState::Idle); - assert_eq!(Urc::ConnectOk, client.check_urc::().unwrap()); - assert_eq!(client.state, ClientState::Idle); - } - - #[test] - fn invalid_response() { - let (mut client, mut p, _) = setup!(Config::new(Mode::Blocking)); - - // String last - let cmd = TestRespStringCmd { - fun: Functionality::APM, - rst: Some(ResetMode::DontReset), - }; - - let response = b"+CUN: 22,16,22"; - enqueue_res(&mut p, Ok(response)); - - assert_eq!(client.state, ClientState::Idle); - assert_eq!(client.send(&cmd), Err(nb::Error::Other(Error::Parse))); - assert_eq!(client.state, ClientState::Idle); - } - - #[test] - fn tx_timeout() { - let timeout = Duration::from_millis(20); - let (mut client, mut p, _) = setup!(Config::new(Mode::Blocking).tx_timeout(1), timeout); - - let cmd = SetModuleFunctionality { - fun: Functionality::APM, - rst: Some(ResetMode::DontReset), - }; - - enqueue_res(&mut p, Ok(&[])); - - assert_eq!(client.state, ClientState::Idle); - assert_eq!(client.send(&cmd), Err(nb::Error::Other(Error::Timeout))); - assert_eq!(client.state, ClientState::Idle); - } - - #[test] - fn flush_timeout() { - let timeout = Duration::from_millis(20); - let (mut client, mut p, _) = setup!(Config::new(Mode::Blocking).flush_timeout(1), timeout); - - let cmd = SetModuleFunctionality { - fun: Functionality::APM, - rst: Some(ResetMode::DontReset), - }; - - enqueue_res(&mut p, Ok(&[])); - - assert_eq!(client.state, ClientState::Idle); - assert_eq!(client.send(&cmd), Err(nb::Error::Other(Error::Timeout))); - assert_eq!(client.state, ClientState::Idle); - } - - #[test] - fn quote_and_no_quote_strings() { - #[derive(Clone, PartialEq, AtatCmd)] - #[at_cmd("+DEVEUI", NoResponse)] - pub struct WithQuoteNoValHexStr { - pub val: HexStr, - } - - let val = HexStr { - val: 0xA0F5, - ..Default::default() - }; - let val = WithQuoteNoValHexStr { val }; - let b = val.as_bytes(); - let s = core::str::from_utf8(&b).unwrap(); - assert_eq!(s, "AT+DEVEUI=\"A0F5\"\r\n"); - - #[derive(Clone, PartialEq, AtatCmd)] - #[at_cmd("+DEVEUI", NoResponse, quote_escape_strings = true)] - pub struct WithQuoteHexStr { - pub val: HexStr, - } - - let val = HexStr { - val: 0xA0F5, - ..Default::default() - }; - let val = WithQuoteHexStr { val }; - let b = val.as_bytes(); - let s = core::str::from_utf8(&b).unwrap(); - assert_eq!(s, "AT+DEVEUI=\"A0F5\"\r\n"); - - #[derive(Clone, PartialEq, AtatCmd)] - #[at_cmd("+DEVEUI", NoResponse, quote_escape_strings = false)] - pub struct WithoutQuoteHexStr { - pub val: HexStr, - } - - let val = HexStr { - val: 0xA0F5_A0F5_A0F5_A0F5_A0F5_A0F5_A0F5_A0F5, - ..Default::default() - }; - let val = WithoutQuoteHexStr { val }; - let b = val.as_bytes(); - let s = core::str::from_utf8(&b).unwrap(); - assert_eq!(s, "AT+DEVEUI=A0F5A0F5A0F5A0F5A0F5A0F5A0F5A0F5\r\n"); - } -} diff --git a/atat/src/clock.rs b/atat/src/clock.rs deleted file mode 100644 index 846cbd11..00000000 --- a/atat/src/clock.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub use fugit_timer::Duration; -pub use fugit_timer::Instant; -pub use fugit_timer::Timer as Clock; diff --git a/atat/src/config.rs b/atat/src/config.rs new file mode 100644 index 00000000..dd915cbb --- /dev/null +++ b/atat/src/config.rs @@ -0,0 +1,48 @@ +use embassy_time::Duration; + +/// Configuration of both the ingress manager, and the AT client. Some of these +/// parameters can be changed on the fly, through issuing a [`Command`] from the +/// client. +/// +/// [`Command`]: enum.Command.html +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct Config { + pub(crate) cmd_cooldown: Duration, + pub(crate) tx_timeout: Duration, + pub(crate) flush_timeout: Duration, +} + +impl Default for Config { + fn default() -> Self { + Self { + cmd_cooldown: Duration::from_millis(20), + tx_timeout: Duration::from_ticks(0), + flush_timeout: Duration::from_ticks(0), + } + } +} + +impl Config { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + #[must_use] + pub const fn tx_timeout(mut self, duration: Duration) -> Self { + self.tx_timeout = duration; + self + } + + #[must_use] + pub const fn flush_timeout(mut self, duration: Duration) -> Self { + self.flush_timeout = duration; + self + } + + #[must_use] + pub const fn cmd_cooldown(mut self, duration: Duration) -> Self { + self.cmd_cooldown = duration; + self + } +} diff --git a/atat/src/digest.rs b/atat/src/digest.rs index 1b1fa951..c1d8eae3 100644 --- a/atat/src/digest.rs +++ b/atat/src/digest.rs @@ -468,7 +468,7 @@ mod test { enum UrcTestParser {} impl Parser for UrcTestParser { - fn parse<'a>(buf: &'a [u8]) -> Result<(&'a [u8], usize), ParseError> { + fn parse(buf: &[u8]) -> Result<(&[u8], usize), ParseError> { let (_, r) = nom::branch::alt((urc_helper("+UUSORD"), urc_helper("+CIEV")))(buf)?; Ok(r) diff --git a/atat/src/error/mod.rs b/atat/src/error/mod.rs index a6245060..82781f3c 100644 --- a/atat/src/error/mod.rs +++ b/atat/src/error/mod.rs @@ -19,8 +19,6 @@ pub enum InternalError<'a> { InvalidResponse, /// Command was aborted Aborted, - /// Buffer overflow - Overflow, /// Failed to parse received response Parse, /// Error response containing any error message @@ -35,26 +33,6 @@ pub enum InternalError<'a> { Custom(&'a [u8]), } -impl<'a> From<&'a [u8]> for InternalError<'a> { - fn from(b: &'a [u8]) -> Self { - match &b[0] { - 0x00 => InternalError::Read, - 0x01 => InternalError::Write, - 0x02 => InternalError::Timeout, - 0x03 => InternalError::InvalidResponse, - 0x04 => InternalError::Aborted, - 0x05 => InternalError::Overflow, - // 0x06 => InternalError::Parse, - 0x07 => InternalError::Error, - 0x08 => InternalError::CmeError(u16::from_le_bytes(b[1..3].try_into().unwrap()).into()), - 0x09 => InternalError::CmsError(u16::from_le_bytes(b[1..3].try_into().unwrap()).into()), - 0x10 if !b.is_empty() => InternalError::ConnectionError(b[1].into()), - 0x11 if !b.is_empty() => InternalError::Custom(&b[1..]), - _ => InternalError::Parse, - } - } -} - #[cfg(feature = "defmt")] impl<'a> defmt::Format for InternalError<'a> { fn format(&self, f: defmt::Formatter) { @@ -64,7 +42,6 @@ impl<'a> defmt::Format for InternalError<'a> { InternalError::Timeout => defmt::write!(f, "InternalError::Timeout"), InternalError::InvalidResponse => defmt::write!(f, "InternalError::InvalidResponse"), InternalError::Aborted => defmt::write!(f, "InternalError::Aborted"), - InternalError::Overflow => defmt::write!(f, "InternalError::Overflow"), InternalError::Parse => defmt::write!(f, "InternalError::Parse"), InternalError::Error => defmt::write!(f, "InternalError::Error"), InternalError::CmeError(e) => defmt::write!(f, "InternalError::CmeError({:?})", e), @@ -79,73 +56,11 @@ impl<'a> defmt::Format for InternalError<'a> { } } -pub enum Encoded<'a> { - Simple(u8), - Nested(u8, u8), - Array(u8, [u8; 2]), - Slice(u8, &'a [u8]), -} - -impl<'a> From>> for Encoded<'a> { - fn from(v: Result<&'a [u8], InternalError<'a>>) -> Self { - match v { - Ok(r) => Self::Slice(0xFF, r), - Err(e) => e.into(), - } - } -} - -impl<'a> From> for Encoded<'a> { - fn from(v: InternalError<'a>) -> Self { - match v { - InternalError::Read => Encoded::Simple(0x00), - InternalError::Write => Encoded::Simple(0x01), - InternalError::Timeout => Encoded::Simple(0x02), - InternalError::InvalidResponse => Encoded::Simple(0x03), - InternalError::Aborted => Encoded::Simple(0x04), - InternalError::Overflow => Encoded::Simple(0x05), - InternalError::Parse => Encoded::Simple(0x06), - InternalError::Error => Encoded::Simple(0x07), - InternalError::CmeError(e) => Encoded::Array(0x08, (e as u16).to_le_bytes()), - InternalError::CmsError(e) => Encoded::Array(0x09, (e as u16).to_le_bytes()), - InternalError::ConnectionError(e) => Encoded::Nested(0x10, e as u8), - InternalError::Custom(e) => Encoded::Slice(0x11, e), - } - } -} - -impl<'a> From for Encoded<'a> { - fn from(v: u8) -> Self { - Self::Nested(0xFE, v) - } -} - -impl<'a> Encoded<'a> { - pub const fn len(&self) -> usize { - match self { - Encoded::Simple(_) => 1, - Encoded::Nested(_, _) => 2, - Encoded::Array(_, _) => 3, - Encoded::Slice(_, b) => 1 + b.len(), - } - } -} - pub enum Response<'a> { Result(Result<&'a [u8], InternalError<'a>>), Prompt(u8), } -impl<'a> From<&'a [u8]> for Response<'a> { - fn from(b: &'a [u8]) -> Self { - match b[0] { - 0xFF => Response::Result(Ok(&b[1..])), - 0xFE => Response::Prompt(b[1]), - _ => Response::Result(Err(InternalError::from(b))), - } - } -} - /// Errors returned by the crate #[derive(Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] @@ -160,8 +75,6 @@ pub enum Error { InvalidResponse, /// Command was aborted Aborted, - /// Buffer overflow - Overflow, /// Failed to parse received response Parse, /// Generic error response without any error message @@ -186,7 +99,6 @@ impl<'a> From> for Error { InternalError::Timeout => Self::Timeout, InternalError::InvalidResponse => Self::InvalidResponse, InternalError::Aborted => Self::Aborted, - InternalError::Overflow => Self::Overflow, InternalError::Parse => Self::Parse, InternalError::Error => Self::Error, InternalError::CmeError(e) => Self::CmeError(e), diff --git a/atat/src/frame.rs b/atat/src/frame.rs new file mode 100644 index 00000000..ca946932 --- /dev/null +++ b/atat/src/frame.rs @@ -0,0 +1,136 @@ +use crate::{InternalError, Response}; +use bbqueue::framed::FrameProducer; +use bincode::{BorrowDecode, Encode}; + +#[derive(Debug, Clone, Copy, Encode, BorrowDecode, PartialEq)] +pub enum Frame<'a> { + Response(&'a [u8]), + Prompt(u8), + ReadError, + WriteError, + TimeoutError, + InvalidResponseError, + AbortedError, + ParseError, + OtherError, + CmeError(u16), + CmsError(u16), + ConnectionError(u8), + CustomError(&'a [u8]), +} + +const BINCODE_CONFIG: bincode::config::Configuration = + bincode::config::standard().with_variable_int_encoding(); + +impl Frame<'_> { + pub fn max_len(&self) -> usize { + // bincode enum discrimonator is 1 byte when variable_int_encoding is specified + 1 + match self { + Frame::Response(b) => variable_int_encoding_length(b.len()) + b.len(), + Frame::Prompt(p) => variable_int_encoding_length(*p as usize), + Frame::CmeError(e) => variable_int_encoding_length(*e as usize), + Frame::CmsError(e) => variable_int_encoding_length(*e as usize), + Frame::CustomError(b) => variable_int_encoding_length(b.len()) + b.len(), + _ => 0, + } + } + + pub fn encode(&self, buffer: &mut [u8]) -> usize { + let encoded = bincode::encode_into_slice(self, buffer, BINCODE_CONFIG).unwrap(); + assert!(encoded <= self.max_len()); + encoded + } +} + +fn variable_int_encoding_length(len: usize) -> usize { + // See https://docs.rs/bincode/2.0.0-rc.2/bincode/config/struct.Configuration.html#method.with_variable_int_encoding + if len < 251 { + 1 + } else { + assert!(len < usize::pow(2, 16)); + 1 + 2 + } +} + +impl<'a> Frame<'a> { + pub fn decode(buffer: &'a [u8]) -> Self { + let (frame, decoded) = bincode::borrow_decode_from_slice(buffer, BINCODE_CONFIG).unwrap(); + assert_eq!(buffer.len(), decoded); + frame + } +} + +impl<'a> From>> for Frame<'a> { + fn from(value: Result<&'a [u8], InternalError<'a>>) -> Self { + match value { + Ok(slice) => Frame::Response(slice), + Err(error) => error.into(), + } + } +} + +impl<'a> From> for Frame<'a> { + fn from(v: InternalError<'a>) -> Self { + match v { + InternalError::Read => Frame::ReadError, + InternalError::Write => Frame::WriteError, + InternalError::Timeout => Frame::TimeoutError, + InternalError::InvalidResponse => Frame::InvalidResponseError, + InternalError::Aborted => Frame::AbortedError, + InternalError::Parse => Frame::ParseError, + InternalError::Error => Frame::OtherError, + InternalError::CmeError(e) => Frame::CmeError(e as u16), + InternalError::CmsError(e) => Frame::CmsError(e as u16), + InternalError::ConnectionError(e) => Frame::ConnectionError(e as u8), + InternalError::Custom(e) => Frame::CustomError(e), + } + } +} + +impl<'a> From> for Response<'a> { + fn from(value: Frame<'a>) -> Self { + match value { + Frame::Response(slice) => Self::Result(Ok(slice)), + Frame::Prompt(value) => Self::Prompt(value), + Frame::ReadError => Self::Result(Err(InternalError::Read)), + Frame::WriteError => Self::Result(Err(InternalError::Write)), + Frame::TimeoutError => Self::Result(Err(InternalError::Timeout)), + Frame::InvalidResponseError => Self::Result(Err(InternalError::InvalidResponse)), + Frame::AbortedError => Self::Result(Err(InternalError::Aborted)), + Frame::ParseError => Self::Result(Err(InternalError::Parse)), + Frame::OtherError => Self::Result(Err(InternalError::Error)), + Frame::CmeError(e) => Self::Result(Err(InternalError::CmeError(e.try_into().unwrap()))), + Frame::CmsError(e) => Self::Result(Err(InternalError::CmsError(e.try_into().unwrap()))), + Frame::ConnectionError(e) => { + Self::Result(Err(InternalError::ConnectionError(e.try_into().unwrap()))) + } + Frame::CustomError(e) => Self::Result(Err(InternalError::Custom(e))), + } + } +} + +pub(crate) trait FrameProducerExt<'a> { + fn try_enqueue(&mut self, frame: Frame<'a>) -> Result<(), ()>; + + #[cfg(feature = "async")] + async fn enqueue(&mut self, frame: Frame<'a>); +} + +impl FrameProducerExt<'_> for FrameProducer<'_, N> { + fn try_enqueue(&mut self, frame: Frame<'_>) -> Result<(), ()> { + if let Ok(mut grant) = self.grant(frame.max_len()) { + let len = frame.encode(grant.as_mut()); + grant.commit(len); + Ok(()) + } else { + Err(()) + } + } + + #[cfg(feature = "async")] + async fn enqueue(&mut self, frame: Frame<'_>) { + let mut grant = self.grant_async(frame.max_len()).await.unwrap(); + let len = frame.encode(grant.as_mut()); + grant.commit(len); + } +} diff --git a/atat/src/ingress.rs b/atat/src/ingress.rs new file mode 100644 index 00000000..4837d348 --- /dev/null +++ b/atat/src/ingress.rs @@ -0,0 +1,213 @@ +use crate::{ + frame::{Frame, FrameProducerExt}, + helpers::LossyStr, + DigestResult, Digester, +}; +use bbqueue::framed::FrameProducer; + +#[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Error { + ResponseQueueFull, + UrcQueueFull, +} + +pub trait AtatIngress { + /// Get the write buffer of the ingress + /// + /// Bytes written to the buffer must be committed by calling advance. + fn write_buf(&mut self) -> &mut [u8]; + + /// Commit written bytes to the ingress and make them visible to the digester. + fn try_advance(&mut self, commit: usize) -> Result<(), Error>; + + /// Commit written bytes to the ingress and make them visible to the digester. + #[cfg(feature = "async")] + async fn advance(&mut self, commit: usize); + + /// Write a buffer to the ingress + #[cfg(feature = "async")] + async fn write(&mut self, buf: &[u8]) { + let mut buf = buf; + while !buf.is_empty() { + let ingress_buf = self.write_buf(); + let len = usize::min(buf.len(), ingress_buf.len()); + ingress_buf[..len].copy_from_slice(&buf[..len]); + self.advance(len).await; + buf = &buf[len..]; + } + } + + /// Read all bytes from the provided serial and ingest the read bytes into + /// the ingress from where they will be processed + #[cfg(feature = "async")] + async fn read_from(&mut self, serial: &mut impl embedded_io::asynch::Read) -> ! { + use embedded_io::Error; + loop { + let buf = self.write_buf(); + match serial.read(buf).await { + Ok(received) => { + if received > 0 { + self.advance(received).await; + } + } + Err(e) => { + error!("Got serial read error {:?}", e.kind()); + } + } + } + } +} + +pub struct Ingress< + 'a, + D: Digester, + const INGRESS_BUF_SIZE: usize, + const RES_CAPACITY: usize, + const URC_CAPACITY: usize, +> { + digester: D, + buf: [u8; INGRESS_BUF_SIZE], + pos: usize, + res_writer: FrameProducer<'a, RES_CAPACITY>, + urc_writer: FrameProducer<'a, URC_CAPACITY>, +} + +impl< + 'a, + D: Digester, + const INGRESS_BUF_SIZE: usize, + const RES_CAPACITY: usize, + const URC_CAPACITY: usize, + > Ingress<'a, D, INGRESS_BUF_SIZE, RES_CAPACITY, URC_CAPACITY> +{ + pub(crate) fn new( + digester: D, + res_writer: FrameProducer<'a, RES_CAPACITY>, + urc_writer: FrameProducer<'a, URC_CAPACITY>, + ) -> Self { + Self { + digester, + buf: [0; INGRESS_BUF_SIZE], + pos: 0, + res_writer, + urc_writer, + } + } +} + +impl< + D: Digester, + const INGRESS_BUF_SIZE: usize, + const RES_CAPACITY: usize, + const URC_CAPACITY: usize, + > AtatIngress for Ingress<'_, D, INGRESS_BUF_SIZE, RES_CAPACITY, URC_CAPACITY> +{ + fn write_buf(&mut self) -> &mut [u8] { + &mut self.buf[self.pos..] + } + + fn try_advance(&mut self, commit: usize) -> Result<(), Error> { + self.pos += commit; + assert!(self.pos <= self.buf.len()); + + while self.pos > 0 { + let swallowed = match self.digester.digest(&self.buf[..self.pos]) { + (DigestResult::None, used) => used, + (DigestResult::Prompt(prompt), swallowed) => { + self.res_writer + .try_enqueue(Frame::Prompt(prompt)) + .map_err(|_| Error::ResponseQueueFull)?; + debug!("Received prompt"); + swallowed + } + (DigestResult::Urc(urc_line), swallowed) => { + let mut grant = self + .urc_writer + .grant(urc_line.len()) + .map_err(|_| Error::UrcQueueFull)?; + debug!("Received URC: {:?}", LossyStr(urc_line)); + grant.copy_from_slice(urc_line); + grant.commit(urc_line.len()); + swallowed + } + (DigestResult::Response(resp), swallowed) => { + match &resp { + Ok(r) => { + if r.is_empty() { + debug!("Received OK") + } else { + debug!("Received response: {:?}", LossyStr(r)); + } + } + Err(e) => { + warn!("Received error response {:?}", e); + } + } + + self.res_writer + .try_enqueue(resp.into()) + .map_err(|_| Error::ResponseQueueFull)?; + swallowed + } + }; + + if swallowed == 0 { + break; + } + + self.buf.copy_within(swallowed..self.pos, 0); + self.pos -= swallowed; + } + + Ok(()) + } + + #[cfg(feature = "async")] + async fn advance(&mut self, commit: usize) { + self.pos += commit; + assert!(self.pos <= self.buf.len()); + + while self.pos > 0 { + let swallowed = match self.digester.digest(&self.buf[..self.pos]) { + (DigestResult::None, used) => used, + (DigestResult::Prompt(prompt), swallowed) => { + self.res_writer.enqueue(Frame::Prompt(prompt)).await; + debug!("Received prompt"); + swallowed + } + (DigestResult::Urc(urc_line), swallowed) => { + let mut grant = self.urc_writer.grant_async(urc_line.len()).await.unwrap(); + debug!("Received URC: {:?}", LossyStr(urc_line)); + grant.copy_from_slice(urc_line); + grant.commit(urc_line.len()); + swallowed + } + (DigestResult::Response(resp), swallowed) => { + match &resp { + Ok(r) => { + if r.is_empty() { + debug!("Received OK") + } else { + debug!("Received response: {:?}", LossyStr(r)); + } + } + Err(e) => { + warn!("Received error response {:?}", e); + } + } + + self.res_writer.enqueue(resp.into()).await; + swallowed + } + }; + + if swallowed == 0 { + break; + } + + self.buf.copy_within(swallowed..self.pos, 0); + self.pos -= swallowed; + } + } +} diff --git a/atat/src/ingress_manager.rs b/atat/src/ingress_manager.rs deleted file mode 100644 index 83c60f2b..00000000 --- a/atat/src/ingress_manager.rs +++ /dev/null @@ -1,217 +0,0 @@ -use bbqueue::framed::FrameProducer; -use heapless::Vec; - -use crate::digest::{DigestResult, Digester}; -use crate::error::InternalError; -use crate::helpers::LossyStr; - -pub struct IngressManager< - D, - const BUF_LEN: usize, - const RES_CAPACITY: usize, - const URC_CAPACITY: usize, -> where - D: Digester, -{ - /// Buffer holding incoming bytes. - buf: Vec, - - /// The response producer sends responses to the client - res_p: FrameProducer<'static, RES_CAPACITY>, - /// The URC producer sends URCs to the client - urc_p: FrameProducer<'static, URC_CAPACITY>, - - /// Digester. - digester: D, -} - -impl - IngressManager -where - D: Digester, -{ - pub const fn new( - res_p: FrameProducer<'static, RES_CAPACITY>, - urc_p: FrameProducer<'static, URC_CAPACITY>, - digester: D, - ) -> Self { - Self { - buf: Vec::new(), - res_p, - urc_p, - digester, - } - } - - fn enqueue_encoded_header<'a, const N: usize>( - producer: &mut FrameProducer<'static, N>, - header: impl Into>, - ) -> Result<(), ()> { - let header = header.into(); - if let Ok(mut grant) = producer.grant(header.len()) { - match header { - crate::error::Encoded::Simple(h) => grant[..1].copy_from_slice(&[h]), - crate::error::Encoded::Nested(h, b) => { - grant[..1].copy_from_slice(&[h]); - grant[1..2].copy_from_slice(&[b]); - } - crate::error::Encoded::Array(h, b) => { - grant[..1].copy_from_slice(&[h]); - grant[1..header.len()].copy_from_slice(&b); - } - crate::error::Encoded::Slice(h, b) => { - grant[..1].copy_from_slice(&[h]); - grant[1..header.len()].copy_from_slice(b); - } - }; - grant.commit(header.len()); - Ok(()) - } else { - Err(()) - } - } - - /// Write data into the internal buffer raw bytes being the core type allows - /// the ingress manager to be abstracted over the communication medium. - /// - /// This function should be called by the UART Rx, either in a receive - /// interrupt, or a DMA interrupt, to move data from the peripheral into the - /// ingress manager receive buffer. - pub fn write(&mut self, data: &[u8]) { - if data.is_empty() { - return; - } - - if self.buf.extend_from_slice(data).is_err() { - error!("OVERFLOW DATA! Buffer: {:?}", LossyStr(&self.buf)); - if Self::enqueue_encoded_header(&mut self.res_p, Err(InternalError::Overflow)).is_err() - { - error!("Response queue full!"); - } - } - } - - /// Return the current length of the internal buffer - /// - /// This can be useful for custom flowcontrol implementations - pub fn len(&self) -> usize { - self.buf.len() - } - - /// Returns whether the internal buffer is empty - /// - /// This can be useful for custom flowcontrol implementations - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - /// Return the capacity of the internal buffer - /// - /// This can be useful for custom flowcontrol implementations - pub const fn capacity(&self) -> usize { - self.buf.capacity() - } - - pub fn digest(&mut self) { - if let Ok(swallowed) = match self.digester.digest(&self.buf) { - (DigestResult::None, swallowed) => Ok(swallowed), - (DigestResult::Prompt(prompt), swallowed) => { - if Self::enqueue_encoded_header(&mut self.res_p, prompt).is_ok() { - Ok(swallowed) - } else { - error!("Response queue full!"); - Err(()) - } - } - (DigestResult::Urc(urc_line), swallowed) => { - if let Ok(mut grant) = self.urc_p.grant(urc_line.len()) { - grant.copy_from_slice(urc_line); - grant.commit(urc_line.len()); - Ok(swallowed) - } else { - error!("URC queue full!"); - Err(()) - } - } - (DigestResult::Response(resp), swallowed) => { - #[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); - } - }; - - if Self::enqueue_encoded_header(&mut self.res_p, resp).is_ok() { - Ok(swallowed) - } else { - error!("Response queue full!"); - Err(()) - } - } - } { - self.buf.rotate_left(swallowed); - self.buf.truncate(self.buf.len() - swallowed); - // if !self.buf.is_empty() { - // trace!("Buffer remainder: \"{:?}\"", LossyStr(&self.buf)); - // } - } - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::{digest::ParseError, error::Response, AtDigester, Parser}; - use bbqueue::BBBuffer; - - const TEST_RX_BUF_LEN: usize = 256; - const TEST_URC_CAPACITY: usize = 10; - const TEST_RES_CAPACITY: usize = 10; - - enum UrcTestParser {} - - impl Parser for UrcTestParser { - fn parse<'a>(_buf: &'a [u8]) -> Result<(&'a [u8], usize), ParseError> { - Err(ParseError::NoMatch) - } - } - - #[test] - fn overflow() { - static mut RES_Q: BBBuffer = BBBuffer::new(); - let (res_p, mut res_c) = unsafe { RES_Q.try_split_framed().unwrap() }; - - static mut URC_Q: BBBuffer = BBBuffer::new(); - let (urc_p, _urc_c) = unsafe { URC_Q.try_split_framed().unwrap() }; - - let mut ingress = - IngressManager::<_, TEST_RX_BUF_LEN, TEST_RES_CAPACITY, TEST_URC_CAPACITY>::new( - res_p, - urc_p, - AtDigester::::new(), - ); - - ingress.write(b"+USORD: 3,266,\""); - for _ in 0..266 { - ingress.write(b"s"); - } - ingress.write(b"\"\r\n"); - ingress.digest(); - let mut grant = res_c.read().unwrap(); - grant.auto_release(true); - - let res = match Response::from(grant.as_ref()) { - Response::Result(r) => r, - Response::Prompt(_) => Ok(&[][..]), - }; - - assert_eq!(res, Err(InternalError::Overflow)); - } -} diff --git a/atat/src/lib.rs b/atat/src/lib.rs index 06e2c6a1..6b9f31bd 100644 --- a/atat/src/lib.rs +++ b/atat/src/lib.rs @@ -217,23 +217,29 @@ #![allow(clippy::type_complexity)] #![allow(clippy::fallible_impl_from)] #![cfg_attr(all(not(test), not(feature = "std")), no_std)] +#![cfg_attr(feature = "async", allow(incomplete_features))] +#![cfg_attr(feature = "async", feature(generic_const_exprs))] +#![cfg_attr(feature = "async", feature(async_fn_in_trait))] // This mod MUST go first, so that the others see its macros. pub(crate) mod fmt; -mod builder; -mod client; -pub mod clock; +mod buffers; +mod config; pub mod digest; mod error; +mod frame; pub mod helpers; -mod ingress_manager; -mod queues; +mod ingress; mod traits; - pub use bbqueue; pub use nom; +pub mod blocking; + +#[cfg(feature = "async")] +pub mod asynch; + #[cfg(feature = "bytes")] pub use serde_bytes; @@ -254,65 +260,12 @@ pub use serde_at; #[cfg(feature = "derive")] pub use heapless; -pub use builder::ClientBuilder; -pub use client::{Client, Mode}; +pub use buffers::Buffers; +pub use config::Config; pub use digest::{AtDigester, AtDigester as DefaultDigester, DigestResult, Digester, Parser}; pub use error::{Error, InternalError, Response}; -pub use ingress_manager::IngressManager; -pub use queues::Queues; -pub use traits::{AtatClient, AtatCmd, AtatResp, AtatUrc}; - -/// Configuration of both the ingress manager, and the AT client. Some of these -/// parameters can be changed on the fly, through issuing a [`Command`] from the -/// client. -/// -/// [`Command`]: enum.Command.html -#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] -pub struct Config { - mode: Mode, - cmd_cooldown: u32, - tx_timeout: u32, - flush_timeout: u32, -} - -impl Default for Config { - fn default() -> Self { - Self { - mode: Mode::Blocking, - cmd_cooldown: 20, - tx_timeout: 0, - flush_timeout: 0, - } - } -} - -impl Config { - #[must_use] - pub fn new(mode: Mode) -> Self { - Self { - mode, - ..Self::default() - } - } - - #[must_use] - pub const fn tx_timeout(mut self, ms: u32) -> Self { - self.tx_timeout = ms; - self - } - - #[must_use] - pub const fn flush_timeout(mut self, ms: u32) -> Self { - self.flush_timeout = ms; - self - } - - #[must_use] - pub const fn cmd_cooldown(mut self, ms: u32) -> Self { - self.cmd_cooldown = ms; - self - } -} +pub use ingress::{AtatIngress, Ingress}; +pub use traits::{AtatCmd, AtatResp, AtatUrc}; #[cfg(test)] #[cfg(feature = "defmt")] diff --git a/atat/src/queues.rs b/atat/src/queues.rs deleted file mode 100644 index f7e1a623..00000000 --- a/atat/src/queues.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! Type definitions for the queues used in this crate. - -use bbqueue::framed::{FrameConsumer, FrameProducer}; - -pub struct Queues { - pub res_queue: ( - FrameProducer<'static, RES_CAPACITY>, - FrameConsumer<'static, RES_CAPACITY>, - ), - pub urc_queue: ( - FrameProducer<'static, URC_CAPACITY>, - FrameConsumer<'static, URC_CAPACITY>, - ), -} diff --git a/atat/src/traits.rs b/atat/src/traits.rs index 36c6f10d..c71b1ed5 100644 --- a/atat/src/traits.rs +++ b/atat/src/traits.rs @@ -1,6 +1,4 @@ use crate::error::{Error, InternalError}; -use crate::Mode; -use embedded_hal_nb::nb; use heapless::{String, Vec}; /// This trait needs to be implemented for every response type. @@ -81,106 +79,12 @@ pub trait AtatCmd { /// Return the command as a heapless `Vec` of bytes. fn as_bytes(&self) -> Vec; - /// Parse the response into a `Self::Response` or `Error` instance. - fn parse(&self, resp: Result<&[u8], InternalError>) -> Result; -} - -pub trait AtatClient { - /// Send an AT command. - /// - /// `cmd` must implement [`AtatCmd`]. - /// - /// This function will block until a response is received, if in Timeout or - /// Blocking mode. In Nonblocking mode, the send can be called until it no - /// longer returns `nb::Error::WouldBlock`, or `self.check_response(cmd)` can - /// be called, with the same result. - /// - /// This function will also make sure that atleast `self.config.cmd_cooldown` - /// has passed since the last response or URC has been received, to allow - /// the slave AT device time to deliver URC's. - fn send, const LEN: usize>( - &mut self, - cmd: &A, - ) -> nb::Result; - - fn send_retry, const LEN: usize>( - &mut self, - cmd: &A, - ) -> nb::Result { - for attempt in 1..=A::ATTEMPTS { - if attempt > 1 { - debug!("Attempt {}:", attempt); - } - - match self.send(cmd) { - Err(nb::Error::Other(Error::Timeout)) => {} - r => return r, - } - } - Err(nb::Error::Other(Error::Timeout)) + fn get_slice<'a>(&'a self, bytes: &'a Vec) -> &'a [u8] { + bytes } - /// Checks if there are any URC's (Unsolicited Response Code) in - /// queue from the ingress manager. - /// - /// Example: - /// ``` - /// use atat::atat_derive::{AtatResp, AtatUrc}; - /// - /// #[derive(Clone, AtatResp)] - /// pub struct MessageWaitingIndication { - /// #[at_arg(position = 0)] - /// pub status: u8, - /// #[at_arg(position = 1)] - /// pub code: u8, - /// } - /// - /// #[derive(Clone, AtatUrc)] - /// pub enum Urc { - /// #[at_urc("+UMWI")] - /// MessageWaitingIndication(MessageWaitingIndication), - /// } - /// - /// // match client.check_urc::() { - /// // Some(Urc::MessageWaitingIndication(MessageWaitingIndication { status, code })) => { - /// // // Do something to act on `+UMWI` URC - /// // } - /// // } - /// ``` - fn check_urc(&mut self) -> Option { - let mut return_urc = None; - self.peek_urc_with::(|urc| { - return_urc = Some(urc); - true - }); - return_urc - } - - fn peek_urc_with bool>(&mut self, f: F); - - /// Check if there are any responses enqueued from the ingress manager. - /// - /// The function will return `nb::Error::WouldBlock` until a response or an - /// error is available, or a timeout occurs and `config.mode` is Timeout. - /// - /// This function is usually only called through [`send`]. - /// - /// [`send`]: #method.send - fn check_response, const LEN: usize>( - &mut self, - cmd: &A, - ) -> nb::Result; - - /// Get the configured mode of the client. - /// - /// Options are: - /// - `NonBlocking` - /// - `Blocking` - /// - `Timeout` - fn get_mode(&self) -> Mode; - - /// Reset the client, queues and ingress buffer, discarding any contents - fn reset(&mut self); + /// Parse the response into a `Self::Response` or `Error` instance. + fn parse(&self, resp: Result<&[u8], InternalError>) -> Result; } impl AtatResp for Vec where T: AtatResp {} diff --git a/examples/Cargo.toml b/examples/Cargo.toml new file mode 100644 index 00000000..b3e7f42c --- /dev/null +++ b/examples/Cargo.toml @@ -0,0 +1,72 @@ +[package] +name = "atat-examples" +version = "0.18.0" +authors = ["Mathias Koch "] +description = "Examples for ATAT" +keywords = ["arm", "cortex-m", "AT", "no-std", "embedded-hal-driver"] +categories = ["embedded", "no-std"] +readme = "../README.md" +license = "MIT OR Apache-2.0" +repository = "https://github.com/BlackbirdHQ/atat" +edition = "2021" +documentation = "https://docs.rs/atat" + +[[bin]] +name = "embassy" +required-features = ["embedded"] + +[[bin]] +name = "std-tokio" +required-features = ["std"] + +[dependencies] +atat = { path = "../atat", features = ["async"] } +embedded-io = "0.4" +critical-section = "1.1.1" + +cortex-m = { version = "0.7.6", optional = true } +cortex-m-rt = { version = "0.7.3", optional = true } +defmt-rtt = { version = "0.4", optional = true } +panic-probe = { version = "0.3.0", features = ["print-defmt"], optional = true } +embassy-executor = { git = "https://github.com/embassy-rs/embassy", rev = "299689d", features = [ + "defmt", + "nightly", + "integrated-timers", +], optional = true } +embassy-time = { version = "0.1.0" } +embassy-rp = { git = "https://github.com/embassy-rs/embassy", rev = "299689d", features = [ + "unstable-pac", + "nightly", + "time-driver", + "critical-section-impl", +], optional = true } + +env_logger = { version = "0.10", optional = true } +tokio = { version = "1.26", default-features = false, features = [ + "time", + "rt-multi-thread", + "macros", +], optional = true } +tokio-serial = { version = "5.4.4", optional = true } + +[features] +embedded = [ + "dep:panic-probe", + "dep:cortex-m", + "dep:cortex-m-rt", + "dep:defmt-rtt", + "dep:embassy-rp", + "dep:embassy-executor", + "embassy-rp?/defmt", + "atat/defmt", + "atat/thumbv6" +] +std = [ + "dep:env_logger", + "dep:tokio", + "dep:tokio-serial", + "atat/log", + "embassy-time/std", + "embassy-time/generic-queue", + "critical-section/std", +] diff --git a/examples/src/bin/embassy.rs b/examples/src/bin/embassy.rs new file mode 100644 index 00000000..785a52d0 --- /dev/null +++ b/examples/src/bin/embassy.rs @@ -0,0 +1,87 @@ +#![no_std] +#![no_main] +#![feature(type_alias_impl_trait)] +#![allow(incomplete_features)] + +use atat::{asynch::AtatClient, AtatIngress, Buffers, DefaultDigester, Ingress}; +use atat_examples::common; +use embassy_executor::Spawner; +use embassy_executor::_export::StaticCell; +use embassy_rp::{ + interrupt, + peripherals::UART0, + uart::{self, BufferedUart, BufferedUartRx}, +}; +use {defmt_rtt as _, panic_probe as _}; + +macro_rules! singleton { + ($val:expr) => {{ + type T = impl Sized; + static STATIC_CELL: StaticCell = StaticCell::new(); + let (x,) = STATIC_CELL.init(($val,)); + x + }}; +} + +#[embassy_executor::main] +async fn main(spawner: Spawner) { + let p = embassy_rp::init(Default::default()); + + let (tx_pin, rx_pin, uart) = (p.PIN_0, p.PIN_1, p.UART0); + + let irq = interrupt::take!(UART0_IRQ); + let tx_buf = &mut singleton!([0u8; 16])[..]; + let rx_buf = &mut singleton!([0u8; 16])[..]; + let uart = BufferedUart::new( + uart, + irq, + tx_pin, + rx_pin, + tx_buf, + rx_buf, + uart::Config::default(), + ); + let (reader, writer) = uart.split(); + + static BUFFERS: Buffers<256, 1024, 1024> = Buffers::<256, 1024, 1024>::new(); + + let (ingress, mut client) = BUFFERS.split( + writer, + DefaultDigester::::default(), + atat::Config::default(), + ); + + spawner.spawn(ingress_task(ingress, reader)).unwrap(); + + let mut state: u8 = 0; + loop { + // These will all timeout after 1 sec, as there is no response + match state { + 0 => { + client.send(&common::general::GetManufacturerId).await.ok(); + } + 1 => { + client.send(&common::general::GetModelId).await.ok(); + } + 2 => { + client.send(&common::general::GetSoftwareVersion).await.ok(); + } + 3 => { + client.send(&common::general::GetWifiMac).await.ok(); + } + _ => cortex_m::asm::bkpt(), + } + + embassy_time::Timer::after(embassy_time::Duration::from_secs(1)).await; + + state += 1; + } +} + +#[embassy_executor::task] +async fn ingress_task( + mut ingress: Ingress<'static, DefaultDigester, 256, 1024, 1024>, + mut reader: BufferedUartRx<'static, UART0>, +) -> ! { + ingress.read_from(&mut reader).await +} diff --git a/examples/src/bin/std-tokio.rs b/examples/src/bin/std-tokio.rs new file mode 100644 index 00000000..1f7e05c5 --- /dev/null +++ b/examples/src/bin/std-tokio.rs @@ -0,0 +1,57 @@ +#![feature(async_fn_in_trait)] +#![allow(incomplete_features)] +use atat_examples::common; + +use std::process::exit; + +use atat::{asynch::AtatClient, AtatIngress, Buffers, Config, DefaultDigester, Ingress}; +use embedded_io::adapters::FromTokio; +use tokio_serial::SerialStream; + +#[tokio::main] +async fn main() -> ! { + env_logger::init(); + + static BUFFERS: Buffers<256, 1024, 1024> = Buffers::<256, 1024, 1024>::new(); + + let (reader, writer) = SerialStream::pair().expect("Failed to create serial pair"); + + let (ingress, mut client) = BUFFERS.split( + FromTokio::new(writer), + DefaultDigester::::default(), + Config::default(), + ); + + tokio::spawn(ingress_task(ingress, FromTokio::new(reader))); + + let mut state: u8 = 0; + loop { + // These will all timeout after 1 sec, as there is no response + match state { + 0 => { + client.send(&common::general::GetManufacturerId).await.ok(); + } + 1 => { + client.send(&common::general::GetModelId).await.ok(); + } + 2 => { + client.send(&common::general::GetSoftwareVersion).await.ok(); + } + 3 => { + client.send(&common::general::GetWifiMac).await.ok(); + } + _ => exit(0), + } + + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + state += 1; + } +} + +async fn ingress_task<'a>( + mut ingress: Ingress<'a, DefaultDigester, 256, 1024, 1024>, + mut reader: FromTokio, +) -> ! { + ingress.read_from(&mut reader).await +} diff --git a/atat/examples/common/general/mod.rs b/examples/src/common/general/mod.rs similarity index 100% rename from atat/examples/common/general/mod.rs rename to examples/src/common/general/mod.rs diff --git a/atat/examples/common/general/responses.rs b/examples/src/common/general/responses.rs similarity index 89% rename from atat/examples/common/general/responses.rs rename to examples/src/common/general/responses.rs index 44e4d8a4..3394d744 100644 --- a/atat/examples/common/general/responses.rs +++ b/examples/src/common/general/responses.rs @@ -1,6 +1,6 @@ //! Responses for General Commands use atat::atat_derive::AtatResp; -use heapless::String; +use atat::heapless::String; /// 4.1 Manufacturer identification /// Text string identifying the manufacturer. @@ -26,5 +26,5 @@ pub struct SoftwareVersion { /// 7.11 Wi-Fi Access point station list +UWAPSTALIST #[derive(Clone, AtatResp)] pub struct WifiMac { - pub mac_addr: heapless_bytes::Bytes<12>, + pub mac_addr: atat::heapless_bytes::Bytes<12>, } diff --git a/atat/examples/common/general/urc.rs b/examples/src/common/general/urc.rs similarity index 100% rename from atat/examples/common/general/urc.rs rename to examples/src/common/general/urc.rs diff --git a/atat/examples/common/mod.rs b/examples/src/common/mod.rs similarity index 96% rename from atat/examples/common/mod.rs rename to examples/src/common/mod.rs index 5fc9caf2..7b37cfc0 100644 --- a/atat/examples/common/mod.rs +++ b/examples/src/common/mod.rs @@ -1,5 +1,4 @@ pub mod general; -pub mod timer; use atat::atat_derive::AtatUrc; diff --git a/examples/src/lib.rs b/examples/src/lib.rs new file mode 100644 index 00000000..8f553291 --- /dev/null +++ b/examples/src/lib.rs @@ -0,0 +1,2 @@ +#![no_std] +pub mod common;