diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8797b4d..21c04f7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,56 +1,92 @@ +name: CI + on: push: branches: - master pull_request: -name: CI - concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - name: Checkout source code + uses: actions/checkout@v4 + + - uses: dsherret/rust-toolchain-file@v1 + + - name: Build (library) + run: cargo build --all --target thumbv7em-none-eabihf --features "lara-r6" + + # - name: Build (examples) + # run: | + # for EXAMPLE in $(ls examples); + # do + # (cd examples/$EXAMPLE && cargo build) + # done + test: - name: Build & Test + name: Test runs-on: ubuntu-latest steps: - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 + - uses: dsherret/rust-toolchain-file@v1 - - name: Build - uses: actions-rs/cargo@v1 - with: - command: build - args: --all --features "defmt-impl,lara-r6" --target thumbv7em-none-eabihf - name: Test - uses: actions-rs/cargo@v1 - with: - command: test - args: --lib --features "log,lara-r6" - env: - DEFMT_LOG: off + run: cargo test --features "lara-r6" + + - name: Install Miri + run: | + rustup toolchain install nightly --component miri + rustup override set nightly + cargo miri setup + + - name: Test (Miri) + run: cargo miri test --lib --features "lara-r6" rustfmt: - name: rustfmt + name: Rustfmt runs-on: ubuntu-latest steps: - name: Checkout source code - uses: actions/checkout@v3 + + uses: actions/checkout@v4 - uses: dsherret/rust-toolchain-file@v1 - - name: Rustfmt - run: cargo fmt -- --check + + - name: Run rustfmt (library) + run: cargo fmt --all -- --check --verbose + + - name: Run rustfmt (examples) + run: | + for EXAMPLE in $(ls examples); + do + (cd examples/$EXAMPLE && cargo fmt --all -- --check --verbose) + done clippy: - name: clippy + name: Clippy runs-on: ubuntu-latest + env: + CLIPPY_PARAMS: -W clippy::all -W clippy::pedantic -W clippy::nursery -W clippy::cargo steps: - name: Checkout source code - uses: actions/checkout@v3 + + uses: actions/checkout@v4 - uses: dsherret/rust-toolchain-file@v1 - - name: Run clippy - uses: actions-rs/clippy-check@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - args: --features "lara-r6" -- ${{ env.CLIPPY_PARAMS }} + + - name: Run clippy (library) + run: cargo clippy --features "lara-r6" -- ${{ env.CLIPPY_PARAMS }} + + # - name: Run clippy (examples) + # run: | + # for EXAMPLE in $(ls examples); + # do + # (cd examples/$EXAMPLE && cargo clippy -- ${{ env.CLIPPY_PARAMS }}) + # done diff --git a/.vscode/settings.json b/.vscode/settings.json index d19761f..7f9a29e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,6 +10,7 @@ ], "rust-analyzer.cargo.features": [ "lara-r6", + "embassy-embedded-hal" ], "rust-analyzer.cargo.target": "thumbv7em-none-eabihf", "rust-analyzer.diagnostics.disabled": [ diff --git a/Cargo.toml b/Cargo.toml index 2420618..ad3df9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,157 @@ -[workspace] -resolver = "2" -members = [ - "ublox-cellular", - # "examples/common_lib", - # "examples/linux", - # "examples/linux_mqtt", - # "examples/linux_jobs", +[package] +name = "ublox-cellular-rs" +version = "0.4.0" +authors = ["Mathias Koch "] +description = "Driver crate for u-blox cellular devices, implementation follows 'UBX-13002752 - R65'" +readme = "../README.md" +keywords = ["arm", "cortex-m", "ublox", "cellular", "embedded-hal-driver"] +categories = ["embedded", "no-std"] +license = "MIT OR Apache-2.0" +repository = "https://github.com/BlackbirdHQ/ublox-cellular-rs" +edition = "2021" + +[lib] +name = "ublox_cellular" +doctest = false + +[dependencies] +atat = { version = "0.23", features = ["derive", "bytes"] } +heapless = { version = "^0.8", features = ["serde"] } +serde = { version = "^1", default-features = false, features = ["derive"] } +#ublox-sockets = { version = "0.5", optional = true } +ublox-sockets = { git = "https://github.com/BlackbirdHQ/ublox-sockets", branch = "feature/async-borrowed-sockets", optional = true } +embassy-time = "0.3" +embassy-sync = "0.6" + +log = { version = "^0.4", default-features = false, optional = true } +defmt = { version = "^0.3", optional = true } + +futures-util = { version = "0.3.29", default-features = false } + +embassy-futures = { version = "0.1" } + +embedded-hal = "1.0.0" +embedded-nal-async = { version = "0.8" } + +embassy-at-cmux = { git = "https://github.com/MathiasKoch/embassy", rev = "c83b7a052" } +# embassy-at-cmux = { path = "../embassy/embassy-at-cmux" } + +embassy-net-ppp = { version = "0.1", optional = true } +embassy-net = { version = "0.4", features = [ + "proto-ipv4", + "medium-ip", +], optional = true } +embassy-embedded-hal = { version = "0.1", optional = true } + +embedded-io-async = "0.6" + +[features] +default = ["socket-udp", "socket-tcp", "ppp"] + + +### Cellular feature list from ubxlib: +use-upsd-context-activation = [] +# mno-profile = [] +# cscon = [] +# root-of-trust = [] +# async-sock-close = [] +# data-counters = [] +# security-tls-iana-numbering = [] +# security-tls-server-name-indication = [] +# security-tls-psk-as-hex = [] +# mqtt = [] +# mqtt-sara-r4-old-syntax = [] +# mqtt-set-local-port = [] +# mqtt-session-retain = [] +# mqtt-binary-publish = [] +# mqtt-will = [] +# mqtt-keep-alive = [] +# mqtt-security = [] +ucged5 = [] +context-mapping-required = [] +# security-tls-cipher-list = [] +# auto-bauding = [] +# at-profiles = [] +# security-ztp = [] +# file-system-tag = [] +# dtr-power-saving = [] +# 3gpp-power-saving = [] +# 3gpp-power-saving-paging-window-set = [] +# deep-sleep-urc = [] +# edrx = [] +# mqttsn = [] +# mqttsn-security = [] +# cts-control = [] +# sock-set-local-port = [] +# fota = [] +# uart-power-saving = [] +# snr-reported = [] +authentication-mode-automatic = [] +# lwm2m = [] +ucged = [] +# http = [] +ppp = ["dep:embassy-net-ppp", "dep:embassy-net"] + + +automatic-apn = [] +internal-network-stack = ["dep:ublox-sockets"] + +socket-tcp = ["ublox-sockets?/socket-tcp", "embassy-net?/tcp"] +socket-udp = ["ublox-sockets?/socket-udp", "embassy-net?/udp"] + +defmt = [ + "dep:defmt", + "atat/defmt", + "heapless/defmt-03", + "embassy-time/defmt", + "embassy-sync/defmt", + "embassy-at-cmux/defmt", + "embassy-futures/defmt", + "ublox-sockets?/defmt", + "embassy-net-ppp?/defmt", + "embassy-net?/defmt", +] +log = ["dep:log", "ublox-sockets?/log", "atat/log"] + +# The supported list of cellular modules. +# +# Note: if you add a new module type here, you also need to add it in +# `modules.rs` +any-module = [] +lara-r6 = ["ucged"] +lena-r8 = [] +sara-r410m = ["ucged", "ucged5"] +sara-r412m = ["ucged", "ucged5"] +sara-r422 = ["context-mapping-required", "ucged"] +sara-r5 = ["context-mapping-required", "ucged", "authentication-mode-automatic"] +sara-u201 = [ + "use-upsd-context-activation", + "context-mapping-required", + "ucged", + "authentication-mode-automatic", ] +toby-r2 = [] + +[workspace] +members = [] +default-members = ["."] +exclude = ["examples"] [patch.crates-io] -atat = { git = "https://github.com/BlackbirdHQ/atat", rev = "c5caaf7" } -# ublox-sockets = { path = "../ublox-sockets" } \ No newline at end of file +#ublox-sockets = { git = "https://github.com/BlackbirdHQ/ublox-sockets", branch = "feature/async-borrowed-sockets" } +#ublox-sockets = { path = "../ublox-sockets" } + +embassy-time = { git = "https://github.com/MathiasKoch/embassy", rev = "49e44344e" } +embassy-sync = { git = "https://github.com/MathiasKoch/embassy", rev = "49e44344e" } +embassy-futures = { git = "https://github.com/MathiasKoch/embassy", rev = "49e44344e" } +embassy-net-ppp = { git = "https://github.com/MathiasKoch/embassy", rev = "49e44344e" } +embassy-net = { git = "https://github.com/MathiasKoch/embassy", rev = "49e44344e" } + +#embassy-time = { path = "../embassy/embassy-time" } +#embassy-sync = { path = "../embassy/embassy-sync" } +#embassy-futures = { path = "../embassy/embassy-futures" } +atat = { git = "https://github.com/BlackbirdHQ/atat", rev = "a466836" } +# atat = { path = "../atat/atat" } + +# [patch."https://github.com/MathiasKoch/embassy"] +# embassy-at-cmux = { path = "../embassy/embassy-at-cmux" } diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index 0ff523d..0000000 --- a/examples/README.md +++ /dev/null @@ -1,6 +0,0 @@ -## Examples - -This folder contains a number of examples, most of which a made to run on linux using a serial port. - -**NOTES** -- `linux_jobs` requires device certificates to run! diff --git a/examples/common_lib/Cargo.toml b/examples/common_lib/Cargo.toml deleted file mode 100644 index 9e2c78e..0000000 --- a/examples/common_lib/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "common_lib" -version = "0.0.1" -authors = ["Mathias Koch "] -description = "Common implementations for ublox-cellular examples" -readme = "../../README.md" -keywords = ["arm", "cortex-m", "ublox", "cellular"] -categories = ["embedded", "no-std"] -license = "MIT OR Apache-2.0" -repository = "https://github.com/BlackbirdHQ/ublox-cellular-rs" -edition = "2018" - -[lib] -name = "common" -doctest = false - -[dependencies] -serialport = "4" -embedded-hal = "=1.0.0-alpha.9" -embedded-hal-nb = "=1.0.0-alpha.1" -nb = "1" -log = { version = "0.4", default-features = false } -ublox-cellular-rs = { path = "../../ublox-cellular" } - -[target.'cfg(target_os = "linux")'.dependencies] -linux-embedded-hal = "0.3" diff --git a/examples/common_lib/src/gpio.rs b/examples/common_lib/src/gpio.rs deleted file mode 100644 index 2c2b1b7..0000000 --- a/examples/common_lib/src/gpio.rs +++ /dev/null @@ -1,83 +0,0 @@ -use embedded_hal::digital::{InputPin, OutputPin}; - -#[cfg(target_os = "linux")] -mod linux { - use super::*; - use linux_embedded_hal::{sysfs_gpio, Pin}; - - // implement newest embedded_hal traits - // linux_embedded_hal uses old ones - pub struct ExtPin(Pin); - - impl OutputPin for ExtPin { - type Error = sysfs_gpio::Error; - - fn set_low(&mut self) -> Result<(), Self::Error> { - if self.0.get_active_low()? { - self.0.set_value(1) - } else { - self.0.set_value(0) - } - } - - fn set_high(&mut self) -> Result<(), Self::Error> { - if self.0.get_active_low()? { - self.0.set_value(0) - } else { - self.0.set_value(1) - } - } - } - - impl InputPin for ExtPin { - type Error = sysfs_gpio::Error; - - fn is_high(&self) -> Result { - if !self.0.get_active_low()? { - self.0.get_value().map(|val| val != 0) - } else { - self.0.get_value().map(|val| val == 0) - } - } - - fn is_low(&self) -> Result { - self.is_high().map(|val| !val) - } - } -} - -#[cfg(not(target_os = "linux"))] -mod other { - use super::*; - - pub struct ExtPin; - - impl OutputPin for ExtPin { - type Error = std::convert::Infallible; - - fn set_low(&mut self) -> Result<(), Self::Error> { - Ok(()) - } - - fn set_high(&mut self) -> Result<(), Self::Error> { - Ok(()) - } - } - - impl InputPin for ExtPin { - type Error = std::convert::Infallible; - - fn is_high(&self) -> Result { - Ok(true) - } - - fn is_low(&self) -> Result { - self.is_high().map(|val| !val) - } - } -} - -#[cfg(target_os = "linux")] -pub use linux::ExtPin; -#[cfg(not(target_os = "linux"))] -pub use other::ExtPin; diff --git a/examples/common_lib/src/lib.rs b/examples/common_lib/src/lib.rs deleted file mode 100644 index 3ca8d92..0000000 --- a/examples/common_lib/src/lib.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod gpio; -pub mod serial; -pub mod timer; diff --git a/examples/common_lib/src/serial.rs b/examples/common_lib/src/serial.rs deleted file mode 100644 index 6ab5d73..0000000 --- a/examples/common_lib/src/serial.rs +++ /dev/null @@ -1,42 +0,0 @@ -use serialport; -use std::io::{ErrorKind as IoErrorKind, Read, Write}; -use embedded_hal_nb::nb; - -pub struct Serial(pub Box); - -/// Helper to convert std::io::Error to the nb::Error -fn translate_io_errors(err: std::io::Error) -> nb::Error { - match err.kind() { - IoErrorKind::WouldBlock | IoErrorKind::TimedOut | IoErrorKind::Interrupted => { - nb::Error::WouldBlock - } - _err => nb::Error::Other(embedded_hal::serial::ErrorKind::Other), - } -} - -impl embedded_hal_nb::serial::Read for Serial { - type Error = embedded_hal::serial::ErrorKind; - - fn read(&mut self) -> nb::Result { - let mut buffer = [0; 1]; - let bytes_read = self.0.read(&mut buffer).map_err(translate_io_errors)?; - if bytes_read == 1 { - Ok(buffer[0]) - } else { - Err(nb::Error::WouldBlock) - } - } -} - -impl embedded_hal_nb::serial::Write for Serial { - type Error = embedded_hal::serial::ErrorKind; - - fn write(&mut self, word: u8) -> nb::Result<(), Self::Error> { - self.0.write(&[word]).map_err(translate_io_errors)?; - Ok(()) - } - - fn flush(&mut self) -> nb::Result<(), Self::Error> { - self.0.flush().map_err(translate_io_errors) - } -} diff --git a/examples/common_lib/src/timer.rs b/examples/common_lib/src/timer.rs deleted file mode 100644 index 3b6b3d0..0000000 --- a/examples/common_lib/src/timer.rs +++ /dev/null @@ -1,87 +0,0 @@ -use ublox_cellular::fugit; -use ublox_cellular::prelude::*; - -pub struct SysTimer { - description: String, - monotonic: std::time::Instant, - start: Option, - duration: fugit::TimerDurationU32, -} - -impl SysTimer { - pub fn new(description: &str) -> Self { - Self { - description: description.into(), - monotonic: std::time::Instant::now(), - start: None, - duration: fugit::TimerDurationU32::millis(0), - } - } -} - -impl Clock for SysTimer { - type Error = std::convert::Infallible; - - fn now(&mut self) -> fugit::TimerInstantU32 { - let millis = self.monotonic.elapsed().as_millis(); - fugit::TimerInstantU32::from_ticks(millis as u32) - } - - fn start(&mut self, duration: fugit::TimerDurationU32) -> Result<(), Self::Error> { - self.start = Some(std::time::Instant::now()); - self.duration = duration.convert(); - log::debug!( - "[{}] start {:?} duration {:?}", - self.description, - self.start, - self.duration - ); - Ok(()) - } - - fn cancel(&mut self) -> Result<(), Self::Error> { - if self.start.is_some() { - self.start = None; - } - Ok(()) - } - - fn wait(&mut self) -> nb::Result<(), Self::Error> { - if let Some(start) = self.start { - if std::time::Instant::now() - start - > std::time::Duration::from_millis(self.duration.ticks() as u64) - { - log::debug!( - "[{}] now {:?} start {:?} duration {:?} {:?}", - self.description, - std::time::Instant::now(), - self.start, - self.duration, - std::time::Duration::from_millis(self.duration.ticks() as u64) - ); - Ok(()) - } else { - Err(nb::Error::WouldBlock) - } - } else { - Ok(()) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use fugit::ExtU32; - - #[test] - fn sys_timer() { - let now = std::time::Instant::now(); - - let mut t: SysTimer<1000> = SysTimer::new(""); - t.start(1.secs()).unwrap(); - nb::block!(t.wait()).unwrap(); - - assert!(now.elapsed().as_millis() >= 1000); - } -} diff --git a/examples/embassy-rp2040-example/.cargo/config.toml b/examples/embassy-rp2040-example/.cargo/config.toml new file mode 100644 index 0000000..0bb5ce6 --- /dev/null +++ b/examples/embassy-rp2040-example/.cargo/config.toml @@ -0,0 +1,8 @@ +[target.'cfg(all(target_arch = "arm", target_os = "none"))'] +runner = 'probe-rs run --chip RP2040' + +[build] +target = "thumbv6m-none-eabi" + +[env] +DEFMT_LOG = "trace,embassy_hal_internal=error" diff --git a/examples/embassy-rp2040-example/.gitignore b/examples/embassy-rp2040-example/.gitignore new file mode 100644 index 0000000..092e4d3 --- /dev/null +++ b/examples/embassy-rp2040-example/.gitignore @@ -0,0 +1,9 @@ +**/*.rs.bk +.#* +.gdb_history +*.fifo +target/ +*.o +**/*secrets* + +.DS_Store diff --git a/examples/embassy-rp2040-example/.vscode/settings.json b/examples/embassy-rp2040-example/.vscode/settings.json new file mode 100644 index 0000000..b51df5c --- /dev/null +++ b/examples/embassy-rp2040-example/.vscode/settings.json @@ -0,0 +1,18 @@ +{ + // override the default setting (`cargo check --all-targets`) which produces the following error + // "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.procMacro.enable": true, + "rust-analyzer.cargo.target": "thumbv6m-none-eabi", + // "rust-analyzer.cargo.target": "thumbv7em-none-eabihf", + "rust-analyzer.cargo.features": [ + "ppp", + ], + "rust-analyzer.checkOnSave.command": "clippy", + "rust-analyzer.cargoRunner": null, + "rust-analyzer.experimental.procAttrMacros": false, + "rust-analyzer.diagnostics.disabled": [ + "macro-error" + ], +} \ No newline at end of file diff --git a/examples/embassy-rp2040-example/Cargo.toml b/examples/embassy-rp2040-example/Cargo.toml new file mode 100644 index 0000000..831d5b6 --- /dev/null +++ b/examples/embassy-rp2040-example/Cargo.toml @@ -0,0 +1,104 @@ +[package] +name = "embassy-rp2040-example" +version = "0.1.0" +edition = "2021" + +[dependencies] +embassy-rp = { version = "0.1.0", features = [ + "defmt", + "time-driver", + "unstable-pac", + "rom-v2-intrinsics", + "critical-section-impl", +] } +embassy-executor = { version = "0.5.0", features = [ + "arch-cortex-m", + "executor-thread", + "defmt", + "integrated-timers", + "nightly", +] } +embassy-time = { version = "0.3", features = [ + "defmt", + "defmt-timestamp-uptime", +] } +embassy-futures = { version = "0.1", features = ["defmt"] } +embassy-sync = { version = "0.6", features = ["defmt"] } + +embassy-net = { version = "0.4", features = [ + "defmt", + "packet-trace", + "proto-ipv4", + "medium-ip", + "tcp", + "udp", + "dns", +] } +embassy-net-ppp = { version = "0.1", features = ["defmt"] } +embassy-at-cmux = { path = "../../../embassy/embassy-at-cmux", features = [ + "defmt", +] } + +embedded-tls = { git = "https://github.com/drogue-iot/embedded-tls", rev = "f788e02", default-features = false, features = [ + "defmt", +] } + +embedded-io-async = { version = "0.6" } +heapless = "0.8" +portable-atomic = { version = "1.5.1", features = ["critical-section"] } + +cortex-m = { version = "0.7.7", features = ["inline-asm"] } +cortex-m-rt = "0.7.3" + +defmt = "0.3.5" +defmt-rtt = "0.4.0" +panic-probe = { version = "0.3.1", features = ["print-defmt"] } + +static_cell = { version = "2.0", features = [] } + +atat = { version = "0.23", features = ["derive", "bytes", "defmt"] } +ublox-cellular-rs = { version = "0.4.0", path = "../..", features = [ + "lara-r6", + "defmt", +] } +reqwless = { git = "https://github.com/drogue-iot/reqwless", features = [ + "defmt", +] } +smoltcp = { version = "*", default-features = false, features = [ + "dns-max-server-count-4" +] } +rand_chacha = { version = "0.3", default-features = false } + + +[features] +ppp = ["ublox-cellular-rs/ppp"] +internal-network-stack = ["ublox-cellular-rs/internal-network-stack"] + +[patch.crates-io] +ublox-sockets = { git = "https://github.com/BlackbirdHQ/ublox-sockets", branch = "feature/async-borrowed-sockets" } +# atat = { git = "https://github.com/BlackbirdHQ/atat", branch = "master" } + +no-std-net = { git = "https://github.com/rushmorem/no-std-net", branch = "issue-15" } +# embassy-rp = { git = "https://github.com/embassy-rs/embassy", branch = "main" } +# embassy-time = { git = "https://github.com/embassy-rs/embassy", branch = "main" } +# embassy-sync = { git = "https://github.com/embassy-rs/embassy", branch = "main" } +# embassy-net = { git = "https://github.com/embassy-rs/embassy", branch = "main" } +# embassy-net-ppp = { git = "https://github.com/embassy-rs/embassy", branch = "main" } +# embassy-futures = { git = "https://github.com/embassy-rs/embassy", branch = "main" } +# embassy-executor = { git = "https://github.com/embassy-rs/embassy", branch = "main" } + +embassy-rp = { path = "../../../embassy/embassy-rp" } +embassy-time = { path = "../../../embassy/embassy-time" } +embassy-sync = { path = "../../../embassy/embassy-sync" } +embassy-net = { path = "../../../embassy/embassy-net" } +embassy-net-ppp = { path = "../../../embassy/embassy-net-ppp" } +embassy-futures = { path = "../../../embassy/embassy-futures" } +embassy-executor = { path = "../../../embassy/embassy-executor" } +atat = { path = "../../../atat/atat" } + + +[profile.dev] +opt-level = "s" + +[profile.release] +opt-level = "s" diff --git a/examples/embassy-rp2040-example/build.rs b/examples/embassy-rp2040-example/build.rs new file mode 100644 index 0000000..3f915f9 --- /dev/null +++ b/examples/embassy-rp2040-example/build.rs @@ -0,0 +1,36 @@ +//! This build script copies the `memory.x` file from the crate root into +//! a directory where the linker can always find it at build time. +//! For many projects this is optional, as the linker always searches the +//! project root directory -- wherever `Cargo.toml` is. However, if you +//! are using a workspace or have a more complicated build setup, this +//! build script becomes required. Additionally, by requesting that +//! Cargo re-run the build script whenever `memory.x` is changed, +//! updating `memory.x` ensures a rebuild of the application with the +//! new memory settings. + +use std::env; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; + +fn main() { + // Put `memory.x` in our output directory and ensure it's + // on the linker search path. + let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); + File::create(out.join("memory.x")) + .unwrap() + .write_all(include_bytes!("memory.x")) + .unwrap(); + println!("cargo:rustc-link-search={}", out.display()); + + // By default, Cargo will re-run a build script whenever + // any file in the project changes. By specifying `memory.x` + // here, we ensure the build script is only re-run when + // `memory.x` is changed. + println!("cargo:rerun-if-changed=memory.x"); + + println!("cargo:rustc-link-arg-bins=--nmagic"); + println!("cargo:rustc-link-arg-bins=-Tlink.x"); + println!("cargo:rustc-link-arg-bins=-Tlink-rp.x"); + println!("cargo:rustc-link-arg-bins=-Tdefmt.x"); +} diff --git a/examples/embassy-rp2040-example/memory.x b/examples/embassy-rp2040-example/memory.x new file mode 100644 index 0000000..eb8c173 --- /dev/null +++ b/examples/embassy-rp2040-example/memory.x @@ -0,0 +1,5 @@ +MEMORY { + BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100 + FLASH : ORIGIN = 0x10000100, LENGTH = 1024K - 0x100 + RAM : ORIGIN = 0x20000000, LENGTH = 256K +} \ No newline at end of file diff --git a/examples/embassy-rp2040-example/rust-toolchain.toml b/examples/embassy-rp2040-example/rust-toolchain.toml new file mode 100644 index 0000000..d1bbca4 --- /dev/null +++ b/examples/embassy-rp2040-example/rust-toolchain.toml @@ -0,0 +1,8 @@ +# Before upgrading check that everything is available on all tier1 targets here: +# https://rust-lang.github.io/rustup-components-history +[toolchain] +channel = "nightly-2024-02-01" +components = [ "rust-src", "rustfmt", "llvm-tools" ] +targets = [ + "thumbv6m-none-eabi", +] \ No newline at end of file diff --git a/examples/embassy-rp2040-example/src/bin/embassy-internal-stack.rs b/examples/embassy-rp2040-example/src/bin/embassy-internal-stack.rs new file mode 100644 index 0000000..5485175 --- /dev/null +++ b/examples/embassy-rp2040-example/src/bin/embassy-internal-stack.rs @@ -0,0 +1,188 @@ +#![no_std] +#![no_main] +#![allow(stable_features)] +#![feature(type_alias_impl_trait)] + +use defmt::*; +use embassy_executor::Spawner; +use embassy_rp::gpio; +use embassy_rp::gpio::Input; + +use embassy_rp::gpio::OutputOpenDrain; +use embassy_rp::uart::BufferedUart; +use embassy_rp::uart::BufferedUartRx; +use embassy_rp::uart::BufferedUartTx; +use embassy_rp::{bind_interrupts, peripherals::UART0, uart::BufferedInterruptHandler}; +use embassy_time::{Duration, Timer}; +use static_cell::StaticCell; +use ublox_cellular::asynch::InternalRunner; +use ublox_cellular::asynch::Resources; +use {defmt_rtt as _, panic_probe as _}; + +use ublox_cellular::config::{Apn, CellularConfig}; + +use ublox_cellular::asynch::state::OperationState; + +bind_interrupts!(struct Irqs { + UART0_IRQ => BufferedInterruptHandler; +}); + +const CMD_BUF_SIZE: usize = 128; +const INGRESS_BUF_SIZE: usize = 1024; +const URC_CAPACITY: usize = 2; + +struct MyCelullarConfig { + reset_pin: Option>, + power_pin: Option>, + vint_pin: Option>, +} + +impl<'a> CellularConfig<'a> for MyCelullarConfig { + type ResetPin = OutputOpenDrain<'static>; + type PowerPin = OutputOpenDrain<'static>; + type VintPin = Input<'static>; + + const FLOW_CONTROL: bool = true; + const HEX_MODE: bool = true; + const APN: Apn<'a> = Apn::Given { + name: "em", + username: None, + password: None, + }; + fn reset_pin(&mut self) -> Option<&mut Self::ResetPin> { + info!("reset_pin"); + self.reset_pin.as_mut() + } + fn power_pin(&mut self) -> Option<&mut Self::PowerPin> { + info!("power_pin"); + self.power_pin.as_mut() + } + fn vint_pin(&mut self) -> Option<&mut Self::VintPin> { + info!("vint_pin = {}", self.vint_pin.as_mut()?.is_high()); + self.vint_pin.as_mut() + } +} + +#[embassy_executor::main] +async fn main(spawner: Spawner) { + let p = { + let config = + embassy_rp::config::Config::new(embassy_rp::clocks::ClockConfig::crystal(12_000_000)); + embassy_rp::init(config) + }; + + static TX_BUF: StaticCell<[u8; 16]> = StaticCell::new(); + static RX_BUF: StaticCell<[u8; 16]> = StaticCell::new(); + + let cell_uart = BufferedUart::new_with_rtscts( + p.UART0, + Irqs, + p.PIN_0, + p.PIN_1, + p.PIN_3, + p.PIN_2, + TX_BUF.init([0; 16]), + RX_BUF.init([0; 16]), + embassy_rp::uart::Config::default(), + ); + + let (uart_rx, uart_tx) = cell_uart.split(); + let cell_nrst = gpio::OutputOpenDrain::new(p.PIN_4, gpio::Level::High); + let cell_pwr = gpio::OutputOpenDrain::new(p.PIN_5, gpio::Level::High); + let cell_vint = gpio::Input::new(p.PIN_6, gpio::Pull::None); + + static RESOURCES: StaticCell< + Resources, CMD_BUF_SIZE, INGRESS_BUF_SIZE, URC_CAPACITY>, + > = StaticCell::new(); + + let (_net_device, mut control, runner) = ublox_cellular::asynch::new_internal( + uart_rx, + uart_tx, + RESOURCES.init(Resources::new()), + MyCelullarConfig { + reset_pin: Some(cell_nrst), + power_pin: Some(cell_pwr), + vint_pin: Some(cell_vint), + }, + ); + + // defmt::info!("{:?}", runner.init().await); + // control.set_desired_state(PowerState::Connected).await; + // control + // .send(&crate::command::network_service::SetOperatorSelection { + // mode: crate::command::network_service::types::OperatorSelectionMode::Automatic, + // format: Some(0), + // }) + // .await; + + defmt::unwrap!(spawner.spawn(cell_task(runner))); + + Timer::after(Duration::from_millis(1000)).await; + loop { + control + .set_desired_state(OperationState::DataEstablished) + .await; + info!("set_desired_state(PowerState::Alive)"); + while control.operation_state() != OperationState::DataEstablished { + Timer::after(Duration::from_millis(1000)).await; + } + Timer::after(Duration::from_millis(10000)).await; + + loop { + Timer::after(Duration::from_millis(1000)).await; + let operator = control.get_operator().await; + info!("{}", operator); + let signal_quality = control.get_signal_quality().await; + info!("{}", signal_quality); + if signal_quality.is_err() { + let desired_state = control.desired_state(); + control.set_desired_state(desired_state).await + } + if let Ok(sq) = signal_quality { + if let Ok(op) = operator { + if op.oper.is_none() { + continue; + } + } + if sq.rxlev > 0 && sq.rsrp != 255 { + break; + } + } + } + let dns = control + .send(&ublox_cellular::command::dns::ResolveNameIp { + resolution_type: + ublox_cellular::command::dns::types::ResolutionType::DomainNameToIp, + ip_domain_string: "www.google.com", + }) + .await; + info!("dns: {:?}", dns); + Timer::after(Duration::from_millis(10000)).await; + control.set_desired_state(OperationState::PowerDown).await; + info!("set_desired_state(PowerState::PowerDown)"); + while control.operation_state() != OperationState::PowerDown { + Timer::after(Duration::from_millis(1000)).await; + } + + Timer::after(Duration::from_millis(5000)).await; + } +} + +// #[embassy_executor::task] +// async fn net_task(stack: &'static Stack>) -> ! { +// stack.run().await +// } + +#[embassy_executor::task] +async fn cell_task( + mut runner: InternalRunner< + 'static, + BufferedUartRx<'static, UART0>, + BufferedUartTx<'static, UART0>, + MyCelullarConfig, + INGRESS_BUF_SIZE, + URC_CAPACITY, + >, +) -> ! { + runner.run().await +} diff --git a/examples/embassy-rp2040-example/src/bin/embassy-smoltcp-ppp.rs b/examples/embassy-rp2040-example/src/bin/embassy-smoltcp-ppp.rs new file mode 100644 index 0000000..616db7e --- /dev/null +++ b/examples/embassy-rp2040-example/src/bin/embassy-smoltcp-ppp.rs @@ -0,0 +1,256 @@ +#![cfg(feature = "ppp")] +#![no_std] +#![no_main] +#![allow(stable_features)] +#![feature(type_alias_impl_trait)] +#![feature(impl_trait_in_assoc_type)] + +use defmt::*; +use embassy_executor::Spawner; +use embassy_net::tcp::TcpSocket; +use embassy_net::Stack; +use embassy_net::StackResources; +use embassy_rp::gpio; +use embassy_rp::gpio::Input; + +use embassy_rp::gpio::OutputOpenDrain; +use embassy_rp::uart::BufferedUart; +use embassy_rp::uart::BufferedUartRx; +use embassy_rp::uart::BufferedUartTx; +use embassy_rp::{bind_interrupts, peripherals::UART0, uart::BufferedInterruptHandler}; +use embassy_time::Duration; +use embedded_tls::Aes128GcmSha256; +use embedded_tls::TlsConfig; +use embedded_tls::TlsConnection; +use embedded_tls::TlsContext; +use embedded_tls::UnsecureProvider; +use rand_chacha::rand_core::SeedableRng; +use rand_chacha::ChaCha8Rng; +use reqwless::headers::ContentType; +use reqwless::request::Request; +use reqwless::request::RequestBuilder as _; +use reqwless::response::Response; +use static_cell::StaticCell; +use ublox_cellular::asynch::Resources; +use {defmt_rtt as _, panic_probe as _}; + +use ublox_cellular::config::{Apn, CellularConfig}; + +bind_interrupts!(struct Irqs { + UART0_IRQ => BufferedInterruptHandler; +}); + +const CMD_BUF_SIZE: usize = 128; +const INGRESS_BUF_SIZE: usize = 512; +const URC_CAPACITY: usize = 16; + +struct MyCelullarConfig { + reset_pin: Option>, + power_pin: Option>, + vint_pin: Option>, +} + +impl<'a> CellularConfig<'a> for MyCelullarConfig { + type ResetPin = OutputOpenDrain<'static>; + type PowerPin = OutputOpenDrain<'static>; + type VintPin = Input<'static>; + + const FLOW_CONTROL: bool = true; + + const APN: Apn<'a> = Apn::Given { + name: "onomondo", + username: None, + password: None, + }; + + const PPP_CONFIG: embassy_net_ppp::Config<'a> = embassy_net_ppp::Config { + username: b"", + password: b"", + }; + + fn reset_pin(&mut self) -> Option<&mut Self::ResetPin> { + info!("reset_pin"); + self.reset_pin.as_mut() + } + + fn power_pin(&mut self) -> Option<&mut Self::PowerPin> { + info!("power_pin"); + self.power_pin.as_mut() + } + + fn vint_pin(&mut self) -> Option<&mut Self::VintPin> { + info!("vint_pin = {}", self.vint_pin.as_mut()?.is_high()); + self.vint_pin.as_mut() + } +} + +#[embassy_executor::main] +async fn main(spawner: Spawner) { + let p = { + let config = + embassy_rp::config::Config::new(embassy_rp::clocks::ClockConfig::crystal(12_000_000)); + embassy_rp::init(config) + }; + + static TX_BUF: StaticCell<[u8; 256]> = StaticCell::new(); + static RX_BUF: StaticCell<[u8; 256]> = StaticCell::new(); + + let cell_uart = BufferedUart::new_with_rtscts( + p.UART0, + Irqs, + p.PIN_0, + p.PIN_1, + p.PIN_3, + p.PIN_2, + TX_BUF.init([0; 256]), + RX_BUF.init([0; 256]), + embassy_rp::uart::Config::default(), + ); + + let cell_nrst = gpio::OutputOpenDrain::new(p.PIN_4, gpio::Level::High); + let cell_pwr = gpio::OutputOpenDrain::new(p.PIN_5, gpio::Level::High); + let cell_vint = gpio::Input::new(p.PIN_6, gpio::Pull::None); + + static RESOURCES: StaticCell> = + StaticCell::new(); + let mut runner = ublox_cellular::asynch::Runner::new( + cell_uart.split(), + RESOURCES.init(Resources::new()), + MyCelullarConfig { + reset_pin: Some(cell_nrst), + power_pin: Some(cell_pwr), + vint_pin: Some(cell_vint), + }, + ); + + static PPP_STATE: StaticCell> = StaticCell::new(); + let net_device = runner.ppp_stack(PPP_STATE.init(embassy_net_ppp::State::new())); + + // Generate random seed + let seed = 0x0123_4567_89ab_cdef; // chosen by fair dice roll. guarenteed to be random. + + // Init network stack + static STACK: StaticCell>> = StaticCell::new(); + static STACK_RESOURCES: StaticCell> = StaticCell::new(); + + let stack = &*STACK.init(Stack::new( + net_device, + embassy_net::Config::default(), + STACK_RESOURCES.init(StackResources::new()), + seed, + )); + + spawner.spawn(net_task(stack)).unwrap(); + spawner.spawn(cell_task(runner, stack)).unwrap(); + + stack.wait_config_up().await; + + // embassy_time::Timer::after(Duration::from_secs(2)).await; + + info!("We have network!"); + + let mut rx_buffer = [0; 4096]; + let mut tx_buffer = [0; 4096]; + let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); + socket.set_timeout(Some(Duration::from_secs(20))); + + let hostname = "ecdsa-test.germancoding.com"; + // let hostname = "eohkv57m7xxdr4m.m.pipedream.net"; + info!("looking up {:?}...", hostname); + + let mut remote = stack + .dns_query(hostname, smoltcp::wire::DnsQueryType::A) + .await + .unwrap(); + let remote_endpoint = (remote.pop().unwrap(), 443); + info!("connecting to {:?}...", remote_endpoint); + let r = socket.connect(remote_endpoint).await; + if let Err(e) = r { + warn!("connect error: {:?}", e); + return; + } + info!("TCP connected!"); + + let mut read_record_buffer = [0; 16640]; + let mut write_record_buffer = [0; 16640]; + let config = TlsConfig::new().with_server_name(hostname); + let mut tls = TlsConnection::new(socket, &mut read_record_buffer, &mut write_record_buffer); + + tls.open(TlsContext::new( + &config, + UnsecureProvider::new::(ChaCha8Rng::seed_from_u64(seed)), + )) + .await + .expect("error establishing TLS connection"); + + info!("TLS Established!"); + + let request = Request::get("/") + .host(hostname) + .content_type(ContentType::TextPlain) + .build(); + request.write(&mut tls).await.unwrap(); + + let mut rx_buf = [0; 4096]; + let response = Response::read(&mut tls, reqwless::request::Method::GET, &mut rx_buf) + .await + .unwrap(); + + { + info!("Got resp! {=[u8]:a}", &rx_buf[..512]); + } + + // let mut buf = [0; 16384]; + // let len = response + // .body() + // .reader() + // .read_to_end(&mut buf) + // .await + // .unwrap(); + // info!("{=[u8]:a}", &buf[..len]); + + loop { + embassy_time::Timer::after(Duration::from_secs(1)).await + } +} + +#[embassy_executor::task] +async fn net_task(stack: &'static Stack>) -> ! { + stack.run().await +} + +#[embassy_executor::task] +async fn cell_task( + runner: ublox_cellular::asynch::Runner< + 'static, + BufferedUartRx<'static, UART0>, + BufferedUartTx<'static, UART0>, + MyCelullarConfig, + INGRESS_BUF_SIZE, + URC_CAPACITY, + >, + stack: &'static embassy_net::Stack>, +) -> ! { + runner + .run(|ipv4| { + let Some(addr) = ipv4.address else { + defmt::warn!("PPP did not provide an IP address."); + return; + }; + let mut dns_servers = heapless::Vec::new(); + for s in ipv4.dns_servers.iter().flatten() { + let _ = dns_servers.push(embassy_net::Ipv4Address::from_bytes(&s.0)); + } + let config = embassy_net::ConfigV4::Static(embassy_net::StaticConfigV4 { + address: embassy_net::Ipv4Cidr::new( + embassy_net::Ipv4Address::from_bytes(&addr.0), + 0, + ), + gateway: None, + dns_servers, + }); + + stack.set_config_v4(config); + }) + .await +} diff --git a/examples/embassy-stm32-example/.cargo/config.toml b/examples/embassy-stm32-example/.cargo/config.toml new file mode 100644 index 0000000..25c56a2 --- /dev/null +++ b/examples/embassy-stm32-example/.cargo/config.toml @@ -0,0 +1,8 @@ +[target.thumbv7em-none-eabihf] +runner = 'probe-rs run --chip STM32H747XIHx' + +[build] +target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU) + +[env] +DEFMT_LOG = "trace" diff --git a/examples/embassy-stm32-example/.gitignore b/examples/embassy-stm32-example/.gitignore new file mode 100644 index 0000000..092e4d3 --- /dev/null +++ b/examples/embassy-stm32-example/.gitignore @@ -0,0 +1,9 @@ +**/*.rs.bk +.#* +.gdb_history +*.fifo +target/ +*.o +**/*secrets* + +.DS_Store diff --git a/examples/embassy-stm32-example/Cargo.toml b/examples/embassy-stm32-example/Cargo.toml new file mode 100644 index 0000000..44fc7e0 --- /dev/null +++ b/examples/embassy-stm32-example/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "embassy-stm32-example" +version = "0.1.0" +edition = "2021" + +# Example for the STM32H747I-DISCO board with SARA-R5 modem attached to UART8 +[dependencies] +embassy-stm32 = { version = "0.1.0", features = ["defmt", "stm32h747xi-cm7", "time-driver-any", "exti", "memory-x", "unstable-pac"] } +embassy-executor = { version = "0.5.0", features = ["arch-cortex-m", "executor-thread", "defmt", "integrated-timers"] } +embassy-time = { version = "0.3", features = ["defmt", "defmt-timestamp-uptime"] } +embassy-sync = { version = "0.5", features = ["defmt"] } + + +cortex-m = { version = "0.7.7", features = ["inline-asm", "critical-section-single-core"] } +cortex-m-rt = "0.7.3" + +defmt = "0.3.5" +defmt-rtt = "0.4.0" +panic-probe = { version = "0.3.1", features = ["print-defmt"] } + +static_cell = { version = "2.0", features = []} + +atat = { version = "0.23.0", features = ["derive", "bytes", "defmt"] } +ublox-cellular-rs = {version = "0.4.0", path = "../..", features = ["sara-r5", "defmt"]} + +[patch.crates-io] +ublox-sockets = { git = "https://github.com/BlackbirdHQ/ublox-sockets", branch = "feature/async-borrowed-sockets" } +atat = { git = "https://github.com/BlackbirdHQ/atat", branch = "master" } +no-std-net = { git = "https://github.com/rushmorem/no-std-net", branch = "issue-15" } +embassy-stm32 = { git = "https://github.com/embassy-rs/embassy", branch = "main" } +embassy-time = { git = "https://github.com/embassy-rs/embassy", branch = "main" } +embassy-sync = { git = "https://github.com/embassy-rs/embassy", branch = "main" } +embassy-futures = { git = "https://github.com/embassy-rs/embassy", branch = "main" } +embassy-executor = { git = "https://github.com/embassy-rs/embassy", branch = "main" } + +#embassy-stm32 = { path = "../../../embassy/embassy-stm32" } +#embassy-time = { path = "../../../embassy/embassy-time" } +#embassy-sync = { path = "../../../embassy/embassy-sync" } +#embassy-futures = { path = "../../../embassy/embassy-futures" } +#embassy-executor = { path = "../../../embassy/embassy-executor" } + +#ublox-sockets = { path = "../../../ublox-sockets" } +#atat = { path = "../../../atat/atat" } + +[profile.dev] +opt-level = "s" + +[profile.release] +opt-level = "s" \ No newline at end of file diff --git a/examples/embassy-stm32-example/build.rs b/examples/embassy-stm32-example/build.rs new file mode 100644 index 0000000..8cd32d7 --- /dev/null +++ b/examples/embassy-stm32-example/build.rs @@ -0,0 +1,5 @@ +fn main() { + println!("cargo:rustc-link-arg-bins=--nmagic"); + println!("cargo:rustc-link-arg-bins=-Tlink.x"); + println!("cargo:rustc-link-arg-bins=-Tdefmt.x"); +} diff --git a/examples/embassy-stm32-example/rust-toolchain.toml b/examples/embassy-stm32-example/rust-toolchain.toml new file mode 100644 index 0000000..25d771e --- /dev/null +++ b/examples/embassy-stm32-example/rust-toolchain.toml @@ -0,0 +1,14 @@ +# Before upgrading check that everything is available on all tier1 targets here: +# https://rust-lang.github.io/rustup-components-history +[toolchain] +channel = "1.75" +components = [ "rust-src", "rustfmt", "llvm-tools" ] +targets = [ + "thumbv7em-none-eabi", + "thumbv7m-none-eabi", + "thumbv6m-none-eabi", + "thumbv7em-none-eabihf", + "thumbv8m.main-none-eabihf", + "riscv32imac-unknown-none-elf", + "wasm32-unknown-unknown", +] \ No newline at end of file diff --git a/examples/embassy-stm32-example/src/main.rs b/examples/embassy-stm32-example/src/main.rs new file mode 100644 index 0000000..88b4ef0 --- /dev/null +++ b/examples/embassy-stm32-example/src/main.rs @@ -0,0 +1,289 @@ +#![no_std] +#![no_main] +#![allow(stable_features)] +// #![feature(type_alias_impl_trait)] + +use atat::asynch::Client; +use atat::heapless::String; +use atat::ResponseSlot; +use atat::UrcChannel; +use core::cell::RefCell; +use cortex_m_rt::entry; +use defmt::*; +use embassy_executor::{Executor, Spawner}; +use embassy_stm32::gpio::{AnyPin, Input, Level, Output, OutputOpenDrain, Pin, Pull, Speed}; +use embassy_stm32::peripherals::UART8; +use embassy_stm32::rcc::VoltageScale; +use embassy_stm32::time::{khz, mhz}; +use embassy_stm32::usart::{BufferedUart, BufferedUartRx, BufferedUartTx}; +use embassy_stm32::{bind_interrupts, interrupt, peripherals, usart, Config}; +use embassy_time::{Duration, Timer}; +use static_cell::StaticCell; +use {defmt_rtt as _, panic_probe as _}; + +// use embedded_hal::digital::{ErrorType, InputPin, OutputPin}; + +use ublox_cellular; +use ublox_cellular::config::{Apn, CellularConfig, ReverseOutputPin}; + +use atat::asynch::AtatClient; +use atat::{AtDigester, AtatIngress, DefaultDigester, Ingress, Parser}; +use ublox_cellular::asynch::runner::Runner; +use ublox_cellular::asynch::state::{LinkState, OperationState}; +use ublox_cellular::asynch::State; +use ublox_cellular::command; +use ublox_cellular::command::{Urc, AT}; + +bind_interrupts!(struct Irqs { + UART8 => embassy_stm32::usart::BufferedInterruptHandler; +}); + +const INGRESS_BUF_SIZE: usize = 1024; +const URC_CAPACITY: usize = 2; +const URC_SUBSCRIBERS: usize = 2; + +struct MyCelullarConfig { + reset_pin: Option>, + // reset_pin: Option, + power_pin: Option>>, + // power_pin: Option, + vint_pin: Option>, + // vint_pin: Option +} + +impl<'a> CellularConfig<'a> for MyCelullarConfig { + type ResetPin = Output<'static>; + // type ResetPin = NoPin; + type PowerPin = ReverseOutputPin>; + // type PowerPin = NoPin; + type VintPin = Input<'static>; + // type VintPin = NoPin; + + const FLOW_CONTROL: bool = false; + const HEX_MODE: bool = true; + const APN: Apn<'a> = Apn::Given { + name: "hologram", + username: None, + password: None, + }; + fn reset_pin(&mut self) -> Option<&mut Self::ResetPin> { + info!("reset_pin"); + return self.reset_pin.as_mut(); + } + fn power_pin(&mut self) -> Option<&mut Self::PowerPin> { + info!("power_pin"); + return self.power_pin.as_mut(); + } + fn vint_pin(&mut self) -> Option<&mut Self::VintPin> { + info!("vint_pin = {}", self.vint_pin.as_mut()?.is_high()); + return self.vint_pin.as_mut(); + } +} + +#[embassy_executor::task] +async fn main_task(spawner: Spawner) { + let mut config = Config::default(); + { + use embassy_stm32::rcc::*; + config.rcc.supply_config = SupplyConfig::DirectSMPS; + config.rcc.hsi = Some(HSIPrescaler::DIV1); + config.rcc.csi = true; + config.rcc.pll1 = Some(Pll { + source: PllSource::HSI, + prediv: PllPreDiv::DIV4, + mul: PllMul::MUL50, + divp: Some(PllDiv::DIV2), //400mhz + divq: Some(PllDiv::DIV8), // 100mhz + divr: None, + }); + config.rcc.sys = Sysclk::PLL1_P; // 400 Mhz + config.rcc.ahb_pre = AHBPrescaler::DIV2; // 200 Mhz + config.rcc.apb1_pre = APBPrescaler::DIV2; // 100 Mhz + config.rcc.apb2_pre = APBPrescaler::DIV2; // 100 Mhz + config.rcc.apb3_pre = APBPrescaler::DIV2; // 100 Mhz + config.rcc.apb4_pre = APBPrescaler::DIV2; // 100 Mhz + config.rcc.voltage_scale = VoltageScale::Scale1; + } + let p = embassy_stm32::init(config); + + let led1_pin = p.PI12.degrade(); + let led2_pin = p.PI13.degrade(); + let led3_pin = p.PI14.degrade(); + + spawner.spawn(blinky(led1_pin)).unwrap(); + spawner.spawn(blinky(led2_pin)).unwrap(); + spawner.spawn(blinky(led3_pin)).unwrap(); + + static tx_buf: StaticCell<[u8; 16]> = StaticCell::new(); + static rx_buf: StaticCell<[u8; 16]> = StaticCell::new(); + static INGRESS_BUF: StaticCell<[u8; INGRESS_BUF_SIZE]> = StaticCell::new(); + + let (tx_pin, rx_pin, uart) = (p.PJ8, p.PJ9, p.UART8); + let mut uart_config = embassy_stm32::usart::Config::default(); + { + uart_config.baudrate = 115200; + // uart_config.baudrate = 9600; + uart_config.parity = embassy_stm32::usart::Parity::ParityNone; + uart_config.stop_bits = embassy_stm32::usart::StopBits::STOP1; + uart_config.data_bits = embassy_stm32::usart::DataBits::DataBits8; + } + + let uart = BufferedUart::new( + uart, + Irqs, + rx_pin, + tx_pin, + tx_buf.init([0u8; 16]), + rx_buf.init([0u8; 16]), + uart_config, + ); + let (writer, reader) = uart.unwrap().split(); + // let power = Output::new(p.PJ4, Level::High, Speed::VeryHigh).degrade(); + // let reset = Output::new(p.PF8, Level::High, Speed::VeryHigh).degrade(); + let celullar_config = MyCelullarConfig { + reset_pin: Some(Output::new(p.PF8, Level::High, Speed::Low)), + power_pin: Some(ReverseOutputPin(Output::new(p.PJ4, Level::Low, Speed::Low))), + // reset_pin: Some(OutputOpenDrain::new(p.PF8, Level::High, Speed::Low, Pull::None).degrade()), + // power_pin: Some(OutputOpenDrain::new(p.PJ4, Level::High, Speed::Low, Pull::None).degrade()), + // power_pin: None, + vint_pin: Some(Input::new(p.PJ3, Pull::Down)), + }; + + static RES_SLOT: ResponseSlot = ResponseSlot::new(); + static URC_CHANNEL: UrcChannel = UrcChannel::new(); + let ingress = Ingress::new( + DefaultDigester::::default(), + INGRESS_BUF.init([0; INGRESS_BUF_SIZE]), + &RES_SLOT, + &URC_CHANNEL, + ); + static buf: StaticCell<[u8; INGRESS_BUF_SIZE]> = StaticCell::new(); + let mut client = Client::new( + writer, + &RES_SLOT, + buf.init([0; INGRESS_BUF_SIZE]), + atat::Config::default(), + ); + + spawner.spawn(ingress_task(ingress, reader)).unwrap(); + + static state: StaticCell, INGRESS_BUF_SIZE>>> = + StaticCell::new(); + let (device, mut control, mut runner) = ublox_cellular::asynch::new( + state.init(State::new(client)), + &URC_CHANNEL, + celullar_config, + ) + .await; + // defmt::info!("{:?}", runner.init().await); + // control.set_desired_state(PowerState::Connected).await; + // control + // .send(&crate::command::network_service::SetOperatorSelection { + // mode: crate::command::network_service::types::OperatorSelectionMode::Automatic, + // format: Some(0), + // }) + // .await; + + defmt::unwrap!(spawner.spawn(cellular_task(runner))); + Timer::after(Duration::from_millis(1000)).await; + loop { + control + .set_desired_state(OperationState::DataEstablished) + .await; + info!("set_desired_state(PowerState::Alive)"); + while control.operation_state() != OperationState::DataEstablished { + Timer::after(Duration::from_millis(1000)).await; + } + Timer::after(Duration::from_millis(10000)).await; + + loop { + Timer::after(Duration::from_millis(1000)).await; + let operator = control.get_operator().await; + info!("{}", operator); + let signal_quality = control.get_signal_quality().await; + info!("{}", signal_quality); + if signal_quality.is_err() { + let desired_state = control.desired_state(); + control.set_desired_state(desired_state).await + } + if let Ok(sq) = signal_quality { + if let Ok(op) = operator { + if op.oper == None { + continue; + } + } + if sq.rxlev > 0 && sq.rsrp != 255 { + break; + } + } + } + let dns = control + .send(&ublox_cellular::command::dns::ResolveNameIp { + resolution_type: + ublox_cellular::command::dns::types::ResolutionType::DomainNameToIp, + ip_domain_string: "www.google.com", + }) + .await; + debug!("dns: {:?}", dns); + Timer::after(Duration::from_millis(10000)).await; + control.set_desired_state(OperationState::PowerDown).await; + info!("set_desired_state(PowerState::PowerDown)"); + while control.operation_state() != OperationState::PowerDown { + Timer::after(Duration::from_millis(1000)).await; + } + + Timer::after(Duration::from_millis(5000)).await; + } +} + +#[embassy_executor::task] +async fn ingress_task( + mut ingress: Ingress< + 'static, + DefaultDigester, + ublox_cellular::command::Urc, + { INGRESS_BUF_SIZE }, + { URC_CAPACITY }, + { URC_SUBSCRIBERS }, + >, + mut reader: BufferedUartRx<'static, UART8>, +) -> ! { + ingress.read_from(&mut reader).await; + defmt::panic!("ingress_task ended"); +} + +#[embassy_executor::task] +async fn cellular_task( + runner: Runner< + 'static, + atat::asynch::Client<'_, BufferedUartTx<'static, UART8>, { INGRESS_BUF_SIZE }>, + MyCelullarConfig, + { URC_CAPACITY }, + >, +) -> ! { + runner.run().await +} + +#[embassy_executor::task(pool_size = 3)] +async fn blinky(mut led: AnyPin) { + let mut output = Output::new(led, Level::High, Speed::Low); + loop { + output.set_high(); + Timer::after(Duration::from_millis(1000)).await; + output.set_low(); + Timer::after(Duration::from_millis(1000)).await; + } +} + +static EXECUTOR: StaticCell = StaticCell::new(); + +#[entry] +fn main() -> ! { + info!("Hello World!"); + + let executor = EXECUTOR.init(Executor::new()); + + executor.run(|spawner| { + unwrap!(spawner.spawn(main_task(spawner))); + }) +} diff --git a/examples/linux_jobs/Cargo.toml b/examples/linux_jobs/Cargo.toml deleted file mode 100644 index d0634a1..0000000 --- a/examples/linux_jobs/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "linux_jobs_example" -version = "0.0.1" -authors = ["Mathias Koch "] -description = "Example for running ublox-cellular with mqttrust & rustot in a linux environment" -readme = "../../README.md" -keywords = ["arm", "cortex-m", "ublox", "cellular", "example"] -categories = ["embedded", "no-std"] -license = "MIT OR Apache-2.0" -repository = "https://github.com/BlackbirdHQ/ublox-cellular-rs" -edition = "2018" - -[dependencies] -embedded-hal = "0.2.3" -atat = { version = "^0.4.1", features = ["derive"] } -log = { version = "0.4", default-features = false } -env_logger = "0.7.1" -linux-embedded-hal = "0.3.0" -serialport = "3.3.0" -nb = "0.1.2" -heapless = { version = "^0.5", features = ["serde"] } -rust-crypto = "^0.2" -mqttrust = { git = "https://github.com/BlackbirdHQ/mqttrust", rev = "2cdffdb" } -rustot = { git = "https://github.com/BlackbirdHQ/rustot", rev = "46d573a" } - -common_lib = { path = "../common_lib" } -ublox-cellular-rs = { path = "../../ublox-cellular", features = ["logging"] } diff --git a/examples/linux_jobs/src/file_handler.rs b/examples/linux_jobs/src/file_handler.rs deleted file mode 100644 index aec1f94..0000000 --- a/examples/linux_jobs/src/file_handler.rs +++ /dev/null @@ -1,71 +0,0 @@ -use crypto::digest::Digest; -use crypto::sha1::Sha1; -use rustot::{ - jobs::FileDescription, - ota::{ - ota::ImageState, - pal::{OtaPal, OtaPalError, PalImageState}, - }, -}; -use std::io::{Cursor, Write}; - -pub struct FileHandler { - filebuf: Option>>, -} - -impl FileHandler { - pub fn new() -> Self { - FileHandler { filebuf: None } - } -} - -impl OtaPal for FileHandler { - type Error = (); - fn abort(&mut self, _file: &FileDescription) -> Result<(), OtaPalError> { - Ok(()) - } - fn create_file_for_rx( - &mut self, - file: &FileDescription, - ) -> Result<(), OtaPalError> { - self.filebuf = Some(Cursor::new(Vec::with_capacity(file.filesize))); - Ok(()) - } - fn get_platform_image_state(&mut self) -> Result> { - unimplemented!() - } - fn set_platform_image_state( - &mut self, - _image_state: ImageState, - ) -> Result<(), OtaPalError> { - unimplemented!() - } - fn reset_device(&mut self) -> Result<(), OtaPalError> { - Ok(()) - } - fn close_file(&mut self, _file: &FileDescription) -> Result<(), OtaPalError> { - if let Some(ref mut buf) = &mut self.filebuf { - let mut hasher = Sha1::new(); - hasher.input(buf.get_ref()); - log::info!("Sha1 is {:}!", hasher.result_str()); - Ok(()) - } else { - Err(OtaPalError::BadFileHandle) - } - } - fn write_block( - &mut self, - _file: &FileDescription, - block_offset: usize, - block_payload: &[u8], - ) -> Result> { - if let Some(ref mut buf) = &mut self.filebuf { - buf.set_position(block_offset as u64); - buf.write(block_payload) - .map_err(|_e| OtaPalError::FileWriteFailed)?; - Ok(block_payload.len()) - } else { - Err(OtaPalError::BadFileHandle) - } - } -} diff --git a/examples/linux_jobs/src/main.rs b/examples/linux_jobs/src/main.rs deleted file mode 100644 index b604065..0000000 --- a/examples/linux_jobs/src/main.rs +++ /dev/null @@ -1,266 +0,0 @@ -extern crate alloc; - -mod file_handler; - -use serialport; -use std::io; -use std::thread; - -use file_handler::FileHandler; - -use ublox_cellular::gprs::APNInfo; -use ublox_cellular::prelude::*; -use ublox_cellular::sockets::Ipv4Addr; -use ublox_cellular::{error::Error as GSMError, Config, GsmClient}; - -use atat::blocking::AtatClient; -use embedded_hal::digital::v2::OutputPin; -use linux_embedded_hal::Pin; -use mqttrust::{MqttClient, MqttEvent, MqttOptions, Notification, Request}; - -use rustot::{ - jobs::{is_job_message, IotJobsData, JobAgent, JobDetails, JobStatus}, - ota::ota::{is_ota_message, OtaAgent, OtaConfig}, -}; - -use heapless::{consts, spsc::Queue, ArrayLength}; - -use common::{serial::Serial, timer::SysTimer}; -use std::time::Duration; - -fn attach_gprs(gsm: &GsmClient) -> Result<(), GSMError> -where - C: AtatClient, - RST: OutputPin, - DTR: OutputPin, -{ - gsm.init(true)?; - - // Load certificates - gsm.import_root_ca( - 0, - "Verisign", - include_bytes!("../secrets_mini_2/Verisign.pem"), - )?; - gsm.import_certificate( - 0, - "cert", - include_bytes!("../secrets_mini_2/certificate.pem.crt"), - )?; - gsm.import_private_key( - 0, - "key", - include_bytes!("../secrets_mini_2/private.pem.key"), - None, - )?; - - gsm.begin("").unwrap(); - gsm.attach_gprs(APNInfo::new("em")).unwrap(); - Ok(()) -} - -static mut Q: Queue = Queue(heapless::i::Queue::new()); - -static mut URC_READY: bool = false; - -struct NvicUrcMatcher {} - -impl NvicUrcMatcher { - pub fn new() -> Self { - NvicUrcMatcher {} - } -} - -impl> atat::UrcMatcher for NvicUrcMatcher { - fn process(&mut self, buf: &mut heapless::Vec) -> atat::UrcMatcherResult { - if let Some(line) = atat::get_line(buf, &[b'\r'], b'\r', b'\n', false, false) { - unsafe { URC_READY = true }; - atat::UrcMatcherResult::Complete(line) - } else { - atat::UrcMatcherResult::Incomplete - } - } -} - -type AtatRxBufLen = consts::U2048; - -fn main() { - env_logger::builder() - .filter_level(log::LevelFilter::Debug) - .init(); - - // Serial port settings - let settings = serialport::SerialPortSettings { - baud_rate: 115_200, - data_bits: serialport::DataBits::Eight, - parity: serialport::Parity::None, - stop_bits: serialport::StopBits::One, - flow_control: serialport::FlowControl::None, - timeout: Duration::from_millis(5000), - }; - - // Open serial port - let serial_tx = serialport::open_with_settings("/dev/ttyUSB0", &settings) - .expect("Could not open serial port"); - let mut serial_rx = serial_tx.try_clone().expect("Failed to clone serial port"); - - static mut RES_QUEUE: atat::ResQueue = Queue(heapless::i::Queue::u8()); - static mut URC_QUEUE: atat::UrcQueue = Queue(heapless::i::Queue::u8()); - static mut COM_QUEUE: atat::ComQueue = Queue(heapless::i::Queue::u8()); - let (res_p, res_c) = unsafe { RES_QUEUE.split() }; - let (urc_p, urc_c) = unsafe { URC_QUEUE.split() }; - let (com_p, com_c) = unsafe { COM_QUEUE.split() }; - - let at_config = atat::Config::new(atat::Mode::Timeout); - let mut ingress = atat::IngressManager::with_custom_urc_matcher( - res_p, - urc_p, - com_c, - at_config, - Some(NvicUrcMatcher::new()), - ); - let cell_client = atat::Client::new( - Serial(serial_tx), - res_c, - urc_c, - com_p, - SysTimer::new(), - at_config, - ); - - let gsm = GsmClient::<_, Pin, Pin>::new(cell_client, Config::new()); - - let (p, c) = unsafe { Q.split() }; - - let thing_name = heapless::String::::from("test_mini_2"); - - // Connect to AWS IoT - let mut mqtt_eventloop = MqttEvent::new( - c, - SysTimer::new(), - MqttOptions::new(thing_name.as_str(), Ipv4Addr::new(52, 208, 158, 107), 8883) - .set_max_packet_size(2048), - ); - - let mqtt_client = MqttClient::new(p, thing_name); - - let file_handler = FileHandler::new(); - let mut job_agent = JobAgent::new(); - let mut ota_agent = OtaAgent::new( - file_handler, - SysTimer::new(), - OtaConfig::default().set_block_size(512), - ); - - // Launch reading thread - thread::Builder::new() - .name("serial_read".to_string()) - .spawn(move || loop { - let mut buffer = [0; 32]; - match serial_rx.read(&mut buffer[..]) { - Ok(0) => {} - Ok(bytes_read) => { - ingress.write(&buffer[0..bytes_read]); - ingress.digest(); - ingress.digest(); - // gsm.spin(); - } - Err(e) => match e.kind() { - io::ErrorKind::WouldBlock - | io::ErrorKind::TimedOut - | io::ErrorKind::Interrupted => { - // Ignore - } - _ => { - log::error!("Serial reading thread error while reading: {}", e); - } - }, - } - }) - .unwrap(); - - if attach_gprs(&gsm).is_ok() { - loop { - match mqtt_eventloop.connect(&gsm) { - Ok(_) => { - break; - } - Err(nb::Error::Other(_e)) => panic!("Failed to connect to MQTT"), - Err(nb::Error::WouldBlock) => {} - } - if unsafe { URC_READY } { - gsm.spin().unwrap(); - unsafe { URC_READY = false }; - } - thread::sleep(Duration::from_millis(100)); - } - - job_agent.subscribe_to_jobs(&mqtt_client).unwrap(); - - job_agent - .describe_job_execution(&mqtt_client, "$next", None, None) - .unwrap(); - - loop { - if unsafe { URC_READY } { - log::info!("Spinning from URC_RDY"); - gsm.spin().unwrap(); - unsafe { URC_READY = false }; - } - - ota_agent.request_timer_irq(&mqtt_client); - - match mqtt_eventloop.yield_event(&gsm) { - Ok(Notification::Publish(publish)) => { - if is_job_message(&publish.topic_name) { - match job_agent.handle_message(&mqtt_client, &publish) { - Ok(None) => {} - Ok(Some(job)) => { - log::debug!("Accepted a new JOB! {:?}", job); - match job.details { - JobDetails::OtaJob(otajob) => { - ota_agent.process_ota_job(&mqtt_client, otajob).unwrap() - } - _ => {} - } - } - Err(e) => { - log::error!( - "[{}, {:?}]: {:?}", - publish.topic_name, - publish.qospid, - e - ); - } - } - } else if is_ota_message(&publish.topic_name) { - match ota_agent.handle_message(&mqtt_client, &publish) { - Ok(progress) => { - log::info!("OTA Progress: {}%", progress); - if progress == 100 { - job_agent - .update_job_execution(&mqtt_client, JobStatus::Succeeded) - .unwrap(); - } - } - Err(e) => { - log::error!( - "[{}, {:?}]: {:?}", - publish.topic_name, - publish.qospid, - e - ); - } - } - } else { - log::info!("Got some other incoming message {:?}", publish); - } - } - _ => { - // log::debug!("{:?}", n); - } - } - thread::sleep(Duration::from_millis(100)); - } - } -} diff --git a/examples/linux_mqtt/Cargo.toml b/examples/linux_mqtt/Cargo.toml deleted file mode 100644 index 934d4b6..0000000 --- a/examples/linux_mqtt/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "linux_mqtt_example" -version = "0.0.1" -authors = ["Mathias Koch "] -description = "Example for running ublox-cellular with mqttrust in a linux environment" -readme = "../../README.md" -keywords = ["arm", "cortex-m", "ublox", "cellular", "example"] -categories = ["embedded", "no-std"] -license = "MIT OR Apache-2.0" -repository = "https://github.com/BlackbirdHQ/ublox-cellular-rs" -edition = "2018" - -[dependencies] -embedded-hal = "0.2.3" -log = { version = "0.4", default-features = false } -atat = { version = "^0.4.1", features = ["derive"] } -env_logger = "0.7.1" -linux-embedded-hal = "0.3.0" -serialport = "3.3.0" -nb = "0.1.2" -heapless = { version = "^0.5", features = ["serde"] } -mqttrust = { git = "https://github.com/BlackbirdHQ/mqttrust", rev = "2cdffdb", features = ["alloc"] } - -common_lib = { path = "../common_lib" } -ublox-cellular-rs = { path = "../../ublox-cellular", features = ["logging"] } diff --git a/examples/linux_mqtt/src/main.rs b/examples/linux_mqtt/src/main.rs deleted file mode 100644 index c37b6d1..0000000 --- a/examples/linux_mqtt/src/main.rs +++ /dev/null @@ -1,215 +0,0 @@ -extern crate alloc; - -use serialport; -use std::io; -use std::thread; - -use ublox_cellular::gprs::APNInfo; -use ublox_cellular::prelude::*; -use ublox_cellular::{error::Error as GSMError, sockets::{SocketSet, Socket}, Config, GsmClient}; - -use atat::{self, AtatClient, ClientBuilder, ComQueue, Queues, ResQueue, UrcQueue}; -use embedded_hal::digital::v2::OutputPin; -use linux_embedded_hal::Pin; -use mqttrust::{ - MqttEvent, MqttOptions, Notification, PublishRequest, QoS, Request, SubscribeRequest, - SubscribeTopic, -}; - -use heapless::{consts, spsc::Queue, ArrayLength, String, Vec}; - -use common::{serial::Serial, timer::SysTimer}; -use std::time::Duration; - -fn attach_gprs(gsm: &GsmClient) -> Result<(), GSMError> -where - C: AtatClient, - RST: OutputPin, - DTR: OutputPin, - N: ArrayLength>>, - L: ArrayLength, -{ - gsm.init(true)?; - gsm.begin().unwrap(); - gsm.attach_gprs().unwrap(); - Ok(()) -} - -static mut Q: Queue>, consts::U10, u8> = Queue(heapless::i::Queue::u8()); - -static mut SOCKET_SET: Option> = None; - -static mut URC_READY: bool = false; - -struct NvicUrcMatcher {} - -impl NvicUrcMatcher { - pub fn new() -> Self { - NvicUrcMatcher {} - } -} - -impl> atat::UrcMatcher for NvicUrcMatcher { - fn process(&mut self, buf: &mut heapless::Vec) -> atat::UrcMatcherResult { - if let Some(line) = atat::get_line(buf, &[b'\r'], b'\r', b'\n', false, false) { - unsafe { URC_READY = true }; - atat::UrcMatcherResult::Complete(line) - } else { - atat::UrcMatcherResult::Incomplete - } - } -} - -type AtatRxBufLen = consts::U2048; - -fn main() { - env_logger::builder() - .filter_level(log::LevelFilter::Debug) - .init(); - - // Serial port settings - let settings = serialport::SerialPortSettings { - baud_rate: 230_400, - data_bits: serialport::DataBits::Eight, - parity: serialport::Parity::None, - stop_bits: serialport::StopBits::One, - flow_control: serialport::FlowControl::None, - timeout: Duration::from_millis(5000), - }; - - // Open serial port - let serial_tx = serialport::open_with_settings("/dev/ttyUSB0", &settings) - .expect("Could not open serial port"); - let mut serial_rx = serial_tx.try_clone().expect("Failed to clone serial port"); - - static mut RES_QUEUE: ResQueue = Queue(heapless::i::Queue::u8()); - static mut URC_QUEUE: UrcQueue = Queue(heapless::i::Queue::u8()); - static mut COM_QUEUE: ComQueue = Queue(heapless::i::Queue::u8()); - - let queues = Queues { - res_queue: unsafe { RES_QUEUE.split() }, - urc_queue: unsafe { URC_QUEUE.split() }, - com_queue: unsafe { COM_QUEUE.split() }, - }; - - let (cell_client, mut ingress) = ClientBuilder::new( - Serial(serial_tx), - SysTimer::new(), - atat::Config::new(atat::Mode::Timeout), - ) - .with_custom_urc_matcher(NvicUrcMatcher::new()) - .build(queues); - - unsafe { - SOCKET_SET = Some(SocketSet::new()); - } - - let gsm = GsmClient::<_, Pin, Pin, _, _>::new( - cell_client, - unsafe { SOCKET_SET.as_mut().unwrap() }, - Config::new(APNInfo::new("em")), - ); - - let (mut p, c) = unsafe { Q.split() }; - - // Connect to broker.hivemq.com:1883 - let mut mqtt_eventloop = MqttEvent::new( - c, - SysTimer::new(), - MqttOptions::new("test_mini_1", "broker.hivemq.com".into(), 1883), - ); - - log::info!("{:?}", mqtt_eventloop.options.broker()); - - // Launch reading thread - thread::Builder::new() - .name("serial_read".to_string()) - .spawn(move || loop { - let mut buffer = [0; 32]; - match serial_rx.read(&mut buffer[..]) { - Ok(0) => {} - Ok(bytes_read) => { - ingress.write(&buffer[0..bytes_read]); - ingress.digest(); - ingress.digest(); - // gsm.spin(); - } - Err(e) => match e.kind() { - io::ErrorKind::WouldBlock - | io::ErrorKind::TimedOut - | io::ErrorKind::Interrupted => { - // Ignore - } - _ => { - log::error!("Serial reading thread error while reading: {}", e); - } - }, - } - }) - .unwrap(); - - if attach_gprs(&gsm).is_ok() { - nb::block!(mqtt_eventloop.connect(&gsm)).expect("Failed to connect to MQTT"); - - // Publish @ http://www.hivemq.com/demos/websocket-client/ - p.enqueue( - SubscribeRequest { - topics: Vec::from_slice(&[ - SubscribeTopic { - topic_path: String::from("mqttrust/tester/subscriber"), - qos: QoS::AtLeastOnce, - }, - SubscribeTopic { - topic_path: String::from("mqttrust/tester/subscriber2"), - qos: QoS::AtLeastOnce, - }, - ]) - .unwrap(), - } - .into(), - ) - .expect("Failed to subscribe!"); - - thread::Builder::new() - .name("eventloop".to_string()) - .spawn(move || { - let mut cnt = 0; - loop { - // Subscribe @ http://www.hivemq.com/demos/websocket-client/ - p.enqueue( - PublishRequest::new( - String::from("fbmini/input/test_mini_1"), - format!("{{\"key\": \"Hello World from Factbird Mini - {}!\"}}", cnt) - .as_bytes() - .to_owned(), - ) - .into(), - ) - .expect("Failed to publish!"); - cnt += 1; - thread::sleep(Duration::from_millis(5000)); - } - }) - .unwrap(); - - loop { - if unsafe { URC_READY } { - gsm.spin().unwrap(); - } - match nb::block!(mqtt_eventloop.yield_event(&gsm)) { - Ok(Notification::Publish(_publish)) => { - log::debug!( - "[{}, {:?}]: {:?}", - _publish.topic_name, - _publish.qospid, - String::from_utf8(_publish.payload).unwrap() - ); - } - _ => { - // log::debug!("{:?}", n); - } - } - thread::sleep(Duration::from_millis(500)); - } - } -} diff --git a/examples/sockets/Cargo.toml b/examples/sockets/Cargo.toml deleted file mode 100644 index ac10e6e..0000000 --- a/examples/sockets/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "sockets" -version = "0.1.0" -authors = ["Mathias Koch ", "Andres Vahter "] -description = "Example for running ublox-cellular in a linux environment" -readme = "../../README.md" -keywords = ["arm", "cortex-m", "ublox", "cellular", "example"] -categories = ["embedded", "no-std"] -license = "MIT OR Apache-2.0" -repository = "https://github.com/BlackbirdHQ/ublox-cellular-rs" -edition = "2018" - -[dependencies] -log = { version = "0.4", default-features = false } -env_logger = "0.9" -serialport = "4" -structopt = "0.3" -nb = "1" - -common_lib = { path = "../common_lib" } -ublox-cellular-rs = { path = "../../ublox-cellular", default-features = false, features = ["log", "sara-n3", "socket-udp", "socket-tcp"] } diff --git a/examples/sockets/src/main.rs b/examples/sockets/src/main.rs deleted file mode 100644 index 1138d51..0000000 --- a/examples/sockets/src/main.rs +++ /dev/null @@ -1,217 +0,0 @@ -use std::thread; -use std::time::Duration; - -use atat::bbqueue::BBBuffer; -use atat::heapless::spsc::Queue; -use common::{gpio::ExtPin, serial::Serial, timer::SysTimer}; -use serialport; -use structopt::StructOpt; -use ublox_cellular::atat; -use ublox_cellular::prelude::*; -use ublox_cellular::sockets::{SocketHandle, SocketSet}; -use ublox_cellular::{APNInfo, Config, GsmClient}; - -// TODO: better naming and explanations -const RX_BUF_LEN: usize = 256; -const RES_CAPACITY: usize = 256; -const URC_CAPACITY: usize = 256; -const TIMER_HZ: u32 = 1000; -const MAX_SOCKET_COUNT: usize = 6; -const SOCKET_RING_BUFFER_LEN: usize = 1024; - -static mut SOCKET_SET: Option> = None; - -#[derive(StructOpt, Debug)] -struct Opt { - /// Serial port device - #[structopt(short, long, default_value = "/dev/ttyUSB0")] - port: String, - - /// Serial port baudrate - #[structopt(short, long, default_value = "115200")] - baud: u32, -} - -#[derive(Debug)] -enum NetworkError { - SocketOpen, - SocketConnect, - SocketClosed, -} - -fn connect + ?Sized>( - socket: &mut Option, - network: &mut N, - socket_addr: SocketAddr, -) -> Result<(), NetworkError> { - let sock = match socket.as_mut() { - None => { - let sock = network.socket().map_err(|_e| NetworkError::SocketOpen)?; - socket.get_or_insert(sock) - } - Some(sock) => sock, - }; - - nb::block!(network.connect(sock, socket_addr)).map_err(|_| { - socket.take(); - NetworkError::SocketConnect - }) -} - -fn is_connected + ?Sized>( - socket: &Option, - network: &mut N, -) -> Result { - match socket { - Some(ref socket) => network - .is_connected(socket) - .map_err(|_e| NetworkError::SocketClosed), - None => Err(NetworkError::SocketClosed), - } -} - -fn main() { - let opt = Opt::from_args(); - - // different log levels can be set using RUST_LOG env variable - // this sets common_lib to info and all others to debug: - // RUST_LOG=common=info,debug ./target/debug/sockets --port /dev/tty.usbserial-01028661 - // use comma separate list to add specific log levels to other modules: - // RUST_LOG=common=info,atat=info,debug ./target/debug/sockets --port /dev/tty.usbserial-01028661 - env_logger::builder().format_timestamp_millis().init(); - - let serial_tx = serialport::new(opt.port, opt.baud) - .timeout(Duration::from_millis(10)) - .open() - .expect("Could not open serial port"); - - let mut serial_rx = serial_tx.try_clone().expect("Failed to clone serial port"); - - static mut RES_QUEUE: BBBuffer = BBBuffer::new(); - static mut URC_QUEUE: BBBuffer = BBBuffer::new(); - static mut COM_QUEUE: atat::ComQueue = Queue::new(); - - let queues = atat::Queues { - res_queue: unsafe { RES_QUEUE.try_split_framed().unwrap() }, - urc_queue: unsafe { URC_QUEUE.try_split_framed().unwrap() }, - com_queue: unsafe { COM_QUEUE.split() }, - }; - - let (cell_client, mut ingress) = - atat::ClientBuilder::<_, _, _, _, TIMER_HZ, RX_BUF_LEN, RES_CAPACITY, URC_CAPACITY>::new( - Serial(serial_tx), - SysTimer::new("RX"), - atat::Config::new(atat::Mode::Timeout), - ) - .build(queues); - - unsafe { - SOCKET_SET = Some(SocketSet::new()); - } - - let mut cell_client = GsmClient::< - _, - _, - ExtPin, - ExtPin, - ExtPin, - ExtPin, - TIMER_HZ, - MAX_SOCKET_COUNT, - SOCKET_RING_BUFFER_LEN, - >::new( - cell_client, - SysTimer::new("CELL"), - Config::new("").with_apn_info(APNInfo::new("internet.tele2.ee")), - ); - - cell_client.set_socket_storage(unsafe { SOCKET_SET.as_mut().unwrap() }); - - // spawn serial reading thread - thread::Builder::new() - .spawn(move || loop { - ingress.digest(); - - let mut buffer = [0; 32]; - match serial_rx.read(&mut buffer[..]) { - Ok(0) => {} - Ok(bytes_read) => { - //ingress.digest(); - // log::info!( - // "rx: {:?}", - // std::str::from_utf8(&buffer[..bytes_read]).unwrap() - // ); - ingress.write(&buffer[0..bytes_read]); - ingress.digest(); - } - Err(e) => match e.kind() { - std::io::ErrorKind::Interrupted => {} - _ => { - //log::error!("Serial reading thread error while reading: {}", e); - } - }, - } - //ingress.digest(); - }) - .unwrap(); - - let mut socket: Option = None; - let mut count = 0; - - // notice that `.data_service` must be called continuously to tick modem state machine - loop { - cell_client - .data_service(&APNInfo::new("em")) - .and_then(|mut service| { - match is_connected(&socket, &mut service) { - Ok(false) => { - // socket is present, but not connected - // usually this implies that the socket is closed for writes - // close and recycle the socket - let sock = socket.take().unwrap(); - TcpClientStack::close(&mut service, sock).expect("cannot close socket"); - } - Err(_) => { - // socket not available, try to create and connect - if let Err(e) = connect( - &mut socket, - &mut service, - SocketAddr::new(IpAddr::V4(Ipv4Addr::new(195, 34, 89, 241)), 7), - ) { - log::error!("cannot connect {:?}", e); - } - } - Ok(true) => { - // socket is available, and connected. - } - } - - // socket can be used if connected - socket.as_mut().and_then(|sock| { - if let Err(e) = nb::block!(TcpClientStack::send( - &mut service, - sock, - format!("Whatup {}", count).as_bytes() - )) { - log::error!("cannot send {:?}", e); - } - - let mut buf = [0; 32]; - match nb::block!(TcpClientStack::receive(&mut service, sock, &mut buf)) { - Ok(count) => { - log::info!("received {} bytes: {:?}", count, &buf[..count]); - } - Err(e) => { - log::error!("cannot receive {:?}", e); - } - } - Some(()) - }); - - Ok(()) - }) - .ok(); - - count += 1; - } -} diff --git a/examples/tokio-std-example/.cargo/config.toml b/examples/tokio-std-example/.cargo/config.toml new file mode 100644 index 0000000..38c661e --- /dev/null +++ b/examples/tokio-std-example/.cargo/config.toml @@ -0,0 +1,5 @@ +[build] +target = "x86_64-unknown-linux-gnu" + +[env] +RUST_LOG = "trace,embassy_hal_internal=error" diff --git a/examples/tokio-std-example/.gitignore b/examples/tokio-std-example/.gitignore new file mode 100644 index 0000000..092e4d3 --- /dev/null +++ b/examples/tokio-std-example/.gitignore @@ -0,0 +1,9 @@ +**/*.rs.bk +.#* +.gdb_history +*.fifo +target/ +*.o +**/*secrets* + +.DS_Store diff --git a/examples/tokio-std-example/.vscode/settings.json b/examples/tokio-std-example/.vscode/settings.json new file mode 100644 index 0000000..64a95fd --- /dev/null +++ b/examples/tokio-std-example/.vscode/settings.json @@ -0,0 +1,17 @@ +{ + // override the default setting (`cargo check --all-targets`) which produces the following error + // "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.procMacro.enable": true, + "rust-analyzer.cargo.target": "x86_64-unknown-linux-gnu", + "rust-analyzer.cargo.features": [ + "ppp", + ], + "rust-analyzer.checkOnSave.command": "clippy", + "rust-analyzer.cargoRunner": null, + "rust-analyzer.experimental.procAttrMacros": false, + "rust-analyzer.diagnostics.disabled": [ + "macro-error" + ], +} \ No newline at end of file diff --git a/examples/tokio-std-example/Cargo.toml b/examples/tokio-std-example/Cargo.toml new file mode 100644 index 0000000..190e2e5 --- /dev/null +++ b/examples/tokio-std-example/Cargo.toml @@ -0,0 +1,85 @@ +[package] +name = "tokio-std-example" +version = "0.1.0" +edition = "2021" + +[dependencies] +tokio = { version = "1", features = ["full"] } +tokio-serial = "5.4.1" + +embassy-time = { version = "0.3", features = ["std", "generic-queue"] } +embassy-futures = { version = "0.1", features = ["log"] } +embassy-sync = { version = "0.6", features = ["log"] } + +embassy-net = { version = "0.4", features = [ + "log", + "packet-trace", + "proto-ipv4", + "medium-ip", + "tcp", + "udp", + "dns", +] } +embassy-net-ppp = { version = "0.1", features = ["log"] } +embassy-at-cmux = { path = "../../../embassy/embassy-at-cmux", features = [ + "log", +] } + +embedded-tls = { git = "https://github.com/drogue-iot/embedded-tls", rev = "f788e02", default-features = false, features = [ + "log", +] } + +embedded-io-adapters = { version = "0.6", features = ["tokio-1"] } +embedded-io-async = { version = "0.6" } +heapless = "0.8" + +env_logger = "0.11" +log = "0.4.21" + +static_cell = { version = "2.0" } + +atat = { version = "0.23", features = ["derive", "bytes", "log"] } +ublox-cellular-rs = { version = "0.4.0", path = "../..", features = [ + "lara-r6", + "log", +] } +reqwless = { git = "https://github.com/drogue-iot/reqwless", features = [ + "log", +] } +smoltcp = { version = "*", default-features = false, features = [ + "dns-max-server-count-4", +] } +rand_chacha = { version = "0.3", default-features = false } + +embedded-mqtt = { path = "../../../embedded-mqtt", features = ["log", "embedded-tls"] } + + +[features] +ppp = ["ublox-cellular-rs/ppp"] +internal-network-stack = ["ublox-cellular-rs/internal-network-stack"] + +[patch.crates-io] +ublox-sockets = { git = "https://github.com/BlackbirdHQ/ublox-sockets", branch = "feature/async-borrowed-sockets" } + +no-std-net = { git = "https://github.com/rushmorem/no-std-net", branch = "issue-15" } +# embassy-time = { git = "https://github.com/embassy-rs/embassy", branch = "main" } +# embassy-sync = { git = "https://github.com/embassy-rs/embassy", branch = "main" } +# embassy-net = { git = "https://github.com/embassy-rs/embassy", branch = "main" } +# embassy-net-ppp = { git = "https://github.com/embassy-rs/embassy", branch = "main" } +# embassy-futures = { git = "https://github.com/embassy-rs/embassy", branch = "main" } + +embassy-time = { path = "../../../embassy/embassy-time" } +embassy-sync = { path = "../../../embassy/embassy-sync" } +embassy-net = { path = "../../../embassy/embassy-net" } +embassy-net-ppp = { path = "../../../embassy/embassy-net-ppp" } +embassy-futures = { path = "../../../embassy/embassy-futures" } +atat = { path = "../../../atat/atat" } + +[patch."https://github.com/drogue-iot/embedded-tls"] +embedded-tls = { path = "../../../embedded-tls" } + +[profile.dev] +opt-level = "s" + +[profile.release] +opt-level = "s" diff --git a/examples/tokio-std-example/rust-toolchain.toml b/examples/tokio-std-example/rust-toolchain.toml new file mode 100644 index 0000000..f39e558 --- /dev/null +++ b/examples/tokio-std-example/rust-toolchain.toml @@ -0,0 +1,8 @@ +# Before upgrading check that everything is available on all tier1 targets here: +# https://rust-lang.github.io/rustup-components-history +[toolchain] +channel = "nightly-2024-02-01" +components = [ "rust-src", "rustfmt", "llvm-tools" ] +targets = [ + "x86_64-unknown-linux-gnu", +] \ No newline at end of file diff --git a/examples/tokio-std-example/src/bin/embassy-internal-stack.rs b/examples/tokio-std-example/src/bin/embassy-internal-stack.rs new file mode 100644 index 0000000..5485175 --- /dev/null +++ b/examples/tokio-std-example/src/bin/embassy-internal-stack.rs @@ -0,0 +1,188 @@ +#![no_std] +#![no_main] +#![allow(stable_features)] +#![feature(type_alias_impl_trait)] + +use defmt::*; +use embassy_executor::Spawner; +use embassy_rp::gpio; +use embassy_rp::gpio::Input; + +use embassy_rp::gpio::OutputOpenDrain; +use embassy_rp::uart::BufferedUart; +use embassy_rp::uart::BufferedUartRx; +use embassy_rp::uart::BufferedUartTx; +use embassy_rp::{bind_interrupts, peripherals::UART0, uart::BufferedInterruptHandler}; +use embassy_time::{Duration, Timer}; +use static_cell::StaticCell; +use ublox_cellular::asynch::InternalRunner; +use ublox_cellular::asynch::Resources; +use {defmt_rtt as _, panic_probe as _}; + +use ublox_cellular::config::{Apn, CellularConfig}; + +use ublox_cellular::asynch::state::OperationState; + +bind_interrupts!(struct Irqs { + UART0_IRQ => BufferedInterruptHandler; +}); + +const CMD_BUF_SIZE: usize = 128; +const INGRESS_BUF_SIZE: usize = 1024; +const URC_CAPACITY: usize = 2; + +struct MyCelullarConfig { + reset_pin: Option>, + power_pin: Option>, + vint_pin: Option>, +} + +impl<'a> CellularConfig<'a> for MyCelullarConfig { + type ResetPin = OutputOpenDrain<'static>; + type PowerPin = OutputOpenDrain<'static>; + type VintPin = Input<'static>; + + const FLOW_CONTROL: bool = true; + const HEX_MODE: bool = true; + const APN: Apn<'a> = Apn::Given { + name: "em", + username: None, + password: None, + }; + fn reset_pin(&mut self) -> Option<&mut Self::ResetPin> { + info!("reset_pin"); + self.reset_pin.as_mut() + } + fn power_pin(&mut self) -> Option<&mut Self::PowerPin> { + info!("power_pin"); + self.power_pin.as_mut() + } + fn vint_pin(&mut self) -> Option<&mut Self::VintPin> { + info!("vint_pin = {}", self.vint_pin.as_mut()?.is_high()); + self.vint_pin.as_mut() + } +} + +#[embassy_executor::main] +async fn main(spawner: Spawner) { + let p = { + let config = + embassy_rp::config::Config::new(embassy_rp::clocks::ClockConfig::crystal(12_000_000)); + embassy_rp::init(config) + }; + + static TX_BUF: StaticCell<[u8; 16]> = StaticCell::new(); + static RX_BUF: StaticCell<[u8; 16]> = StaticCell::new(); + + let cell_uart = BufferedUart::new_with_rtscts( + p.UART0, + Irqs, + p.PIN_0, + p.PIN_1, + p.PIN_3, + p.PIN_2, + TX_BUF.init([0; 16]), + RX_BUF.init([0; 16]), + embassy_rp::uart::Config::default(), + ); + + let (uart_rx, uart_tx) = cell_uart.split(); + let cell_nrst = gpio::OutputOpenDrain::new(p.PIN_4, gpio::Level::High); + let cell_pwr = gpio::OutputOpenDrain::new(p.PIN_5, gpio::Level::High); + let cell_vint = gpio::Input::new(p.PIN_6, gpio::Pull::None); + + static RESOURCES: StaticCell< + Resources, CMD_BUF_SIZE, INGRESS_BUF_SIZE, URC_CAPACITY>, + > = StaticCell::new(); + + let (_net_device, mut control, runner) = ublox_cellular::asynch::new_internal( + uart_rx, + uart_tx, + RESOURCES.init(Resources::new()), + MyCelullarConfig { + reset_pin: Some(cell_nrst), + power_pin: Some(cell_pwr), + vint_pin: Some(cell_vint), + }, + ); + + // defmt::info!("{:?}", runner.init().await); + // control.set_desired_state(PowerState::Connected).await; + // control + // .send(&crate::command::network_service::SetOperatorSelection { + // mode: crate::command::network_service::types::OperatorSelectionMode::Automatic, + // format: Some(0), + // }) + // .await; + + defmt::unwrap!(spawner.spawn(cell_task(runner))); + + Timer::after(Duration::from_millis(1000)).await; + loop { + control + .set_desired_state(OperationState::DataEstablished) + .await; + info!("set_desired_state(PowerState::Alive)"); + while control.operation_state() != OperationState::DataEstablished { + Timer::after(Duration::from_millis(1000)).await; + } + Timer::after(Duration::from_millis(10000)).await; + + loop { + Timer::after(Duration::from_millis(1000)).await; + let operator = control.get_operator().await; + info!("{}", operator); + let signal_quality = control.get_signal_quality().await; + info!("{}", signal_quality); + if signal_quality.is_err() { + let desired_state = control.desired_state(); + control.set_desired_state(desired_state).await + } + if let Ok(sq) = signal_quality { + if let Ok(op) = operator { + if op.oper.is_none() { + continue; + } + } + if sq.rxlev > 0 && sq.rsrp != 255 { + break; + } + } + } + let dns = control + .send(&ublox_cellular::command::dns::ResolveNameIp { + resolution_type: + ublox_cellular::command::dns::types::ResolutionType::DomainNameToIp, + ip_domain_string: "www.google.com", + }) + .await; + info!("dns: {:?}", dns); + Timer::after(Duration::from_millis(10000)).await; + control.set_desired_state(OperationState::PowerDown).await; + info!("set_desired_state(PowerState::PowerDown)"); + while control.operation_state() != OperationState::PowerDown { + Timer::after(Duration::from_millis(1000)).await; + } + + Timer::after(Duration::from_millis(5000)).await; + } +} + +// #[embassy_executor::task] +// async fn net_task(stack: &'static Stack>) -> ! { +// stack.run().await +// } + +#[embassy_executor::task] +async fn cell_task( + mut runner: InternalRunner< + 'static, + BufferedUartRx<'static, UART0>, + BufferedUartTx<'static, UART0>, + MyCelullarConfig, + INGRESS_BUF_SIZE, + URC_CAPACITY, + >, +) -> ! { + runner.run().await +} diff --git a/examples/tokio-std-example/src/bin/tokio-smoltcp-ppp-mqtt.rs b/examples/tokio-std-example/src/bin/tokio-smoltcp-ppp-mqtt.rs new file mode 100644 index 0000000..02eaffa --- /dev/null +++ b/examples/tokio-std-example/src/bin/tokio-smoltcp-ppp-mqtt.rs @@ -0,0 +1,168 @@ +#![cfg(feature = "ppp")] + +use embassy_net::dns::DnsSocket; +use embassy_net::tcp::client::TcpClient; +use embassy_net::tcp::client::TcpClientState; +use embassy_net::Stack; +use embassy_net::StackResources; + +use embassy_net_ppp::Device; +use embassy_sync::blocking_mutex::raw::NoopRawMutex; +use embassy_time::Duration; +use embedded_mqtt::transport::embedded_tls::TlsNalTransport; +use embedded_mqtt::transport::embedded_tls::TlsState; +use embedded_mqtt::DomainBroker; +use embedded_tls::Aes128GcmSha256; +use embedded_tls::Certificate; +use embedded_tls::TlsConfig; +use embedded_tls::UnsecureProvider; +use log::*; +use rand_chacha::rand_core::SeedableRng; +use rand_chacha::ChaCha8Rng; +use static_cell::StaticCell; +use tokio_serial::SerialPort; +use tokio_serial::SerialPortBuilderExt; +use ublox_cellular::asynch::Resources; + +use ublox_cellular::config::NoPin; +use ublox_cellular::config::{Apn, CellularConfig}; + +const CMD_BUF_SIZE: usize = 128; +const INGRESS_BUF_SIZE: usize = 512; +const URC_CAPACITY: usize = 2; + +struct MyCelullarConfig; + +impl<'a> CellularConfig<'a> for MyCelullarConfig { + type ResetPin = NoPin; + type PowerPin = NoPin; + type VintPin = NoPin; + + const FLOW_CONTROL: bool = true; + + const APN: Apn<'a> = Apn::Given { + name: "em", + username: None, + password: None, + }; + + const PPP_CONFIG: embassy_net_ppp::Config<'a> = embassy_net_ppp::Config { + username: b"", + password: b"", + }; +} + +const TTY: &str = "/dev/ttyUSB0"; +const HOSTNAME: &str = "a2twqv2u8qs5xt-ats.iot.eu-west-1.amazonaws.com"; +const MQTT_MAX_SUBSCRIBERS: usize = 2; + +#[tokio::main] +async fn main() -> Result<(), Box> { + env_logger::init(); + + info!("HELLO"); + + let mut ppp_iface = tokio_serial::new(TTY, 115200).open_native_async()?; + ppp_iface + .set_flow_control(tokio_serial::FlowControl::Hardware) + .unwrap(); + + let (rx, tx) = tokio::io::split(ppp_iface); + let rx = embedded_io_adapters::tokio_1::FromTokio::new(tokio::io::BufReader::new(rx)); + let tx = embedded_io_adapters::tokio_1::FromTokio::new(tx); + + static RESOURCES: StaticCell> = + StaticCell::new(); + let mut runner = ublox_cellular::asynch::Runner::new( + (rx, tx), + RESOURCES.init(Resources::new()), + MyCelullarConfig, + ); + + static PPP_STATE: StaticCell> = StaticCell::new(); + let net_device = runner.ppp_stack(PPP_STATE.init(embassy_net_ppp::State::new())); + + // Generate random seed + let seed = 0x0123_4567_89ab_cdef; // chosen by fair dice roll. guarenteed to be random. + + // Init network stack + static STACK: StaticCell>> = StaticCell::new(); + static STACK_RESOURCES: StaticCell> = StaticCell::new(); + + let stack = &*STACK.init(Stack::new( + net_device, + embassy_net::Config::default(), + STACK_RESOURCES.init(StackResources::new()), + seed, + )); + + let mqtt_fut = async { + stack.wait_config_up().await; + + info!("We have network!"); + + static DNS: StaticCell> = StaticCell::new(); + let broker = DomainBroker::<_, 64>::new(HOSTNAME, DNS.init(DnsSocket::new(stack))).unwrap(); + + static MQTT_STATE: StaticCell< + embedded_mqtt::State, + > = StaticCell::new(); + + let (mut mqtt_stack, mqtt_client) = embedded_mqtt::new( + MQTT_STATE.init(embedded_mqtt::State::new()), + embedded_mqtt::Config::new("csr_test", broker) + .keepalive_interval(Duration::from_secs(50)), + ); + + let mqtt_tcp_state = TcpClientState::<1, 4096, 4096>::new(); + let mqtt_tcp_client = TcpClient::new(stack, &mqtt_tcp_state); + + let provider = UnsecureProvider::new::(ChaCha8Rng::seed_from_u64(seed)); + + let tls_config = TlsConfig::new() + .with_server_name(HOSTNAME) + // .with_ca(Certificate::X509(include_bytes!( + // "/home/mathias/Downloads/AmazonRootCA3.cer" + // ))) + .with_cert(Certificate::X509(include_bytes!( + "/home/mathias/Downloads/embedded-tls-test-certs/cert.der" + ))) + .with_priv_key(include_bytes!( + "/home/mathias/Downloads/embedded-tls-test-certs/private.der" + )); + + let tls_state = TlsState::<16640, 16640>::new(); + let mut transport = + TlsNalTransport::new(&mqtt_tcp_client, &tls_state, &tls_config, provider); + + mqtt_stack.run(&mut transport).await; + }; + + embassy_futures::join::join3( + stack.run(), + runner.run(|ipv4| { + let Some(addr) = ipv4.address else { + warn!("PPP did not provide an IP address."); + return; + }; + let mut dns_servers = heapless::Vec::new(); + for s in ipv4.dns_servers.iter().flatten() { + let _ = dns_servers.push(embassy_net::Ipv4Address::from_bytes(&s.0)); + } + let config = embassy_net::ConfigV4::Static(embassy_net::StaticConfigV4 { + address: embassy_net::Ipv4Cidr::new( + embassy_net::Ipv4Address::from_bytes(&addr.0), + 0, + ), + gateway: None, + dns_servers, + }); + + stack.set_config_v4(config); + }), + mqtt_fut, + ) + .await; + + unreachable!(); +} diff --git a/examples/tokio-std-example/src/bin/tokio-smoltcp-ppp.rs b/examples/tokio-std-example/src/bin/tokio-smoltcp-ppp.rs new file mode 100644 index 0000000..87b65a5 --- /dev/null +++ b/examples/tokio-std-example/src/bin/tokio-smoltcp-ppp.rs @@ -0,0 +1,182 @@ +#![cfg(feature = "ppp")] + +use embassy_net::tcp::TcpSocket; +use embassy_net::Stack; +use embassy_net::StackResources; + +use embassy_time::Duration; +use embedded_tls::Aes128GcmSha256; +use embedded_tls::TlsConfig; +use embedded_tls::TlsConnection; +use embedded_tls::TlsContext; +use embedded_tls::UnsecureProvider; +use log::*; +use rand_chacha::rand_core::SeedableRng; +use rand_chacha::ChaCha8Rng; +use reqwless::headers::ContentType; +use reqwless::request::Request; +use reqwless::request::RequestBuilder as _; +use reqwless::response::Response; +use static_cell::StaticCell; +use tokio_serial::SerialPort; +use tokio_serial::SerialPortBuilderExt; +use ublox_cellular::asynch::Resources; + +use ublox_cellular::config::NoPin; +use ublox_cellular::config::{Apn, CellularConfig}; + +const CMD_BUF_SIZE: usize = 128; +const INGRESS_BUF_SIZE: usize = 512; +const URC_CAPACITY: usize = 2; + +struct MyCelullarConfig; + +impl<'a> CellularConfig<'a> for MyCelullarConfig { + type ResetPin = NoPin; + type PowerPin = NoPin; + type VintPin = NoPin; + + const FLOW_CONTROL: bool = true; + + const APN: Apn<'a> = Apn::Given { + name: "em", + username: None, + password: None, + }; + + const PPP_CONFIG: embassy_net_ppp::Config<'a> = embassy_net_ppp::Config { + username: b"", + password: b"", + }; +} + +const TTY: &str = "/dev/ttyUSB0"; + +#[tokio::main] +async fn main() -> Result<(), Box> { + env_logger::init(); + + info!("HELLO"); + + let mut ppp_iface = tokio_serial::new(TTY, 115200).open_native_async()?; + ppp_iface + .set_flow_control(tokio_serial::FlowControl::Hardware) + .unwrap(); + + let (rx, tx) = tokio::io::split(ppp_iface); + let rx = embedded_io_adapters::tokio_1::FromTokio::new(tokio::io::BufReader::new(rx)); + let tx = embedded_io_adapters::tokio_1::FromTokio::new(tx); + + static RESOURCES: StaticCell> = + StaticCell::new(); + let mut runner = ublox_cellular::asynch::Runner::new( + (rx, tx), + RESOURCES.init(Resources::new()), + MyCelullarConfig, + ); + + static PPP_STATE: StaticCell> = StaticCell::new(); + let net_device = runner.ppp_stack(PPP_STATE.init(embassy_net_ppp::State::new())); + + // Generate random seed + let seed = 0x0123_4567_89ab_cdef; // chosen by fair dice roll. guarenteed to be random. + + // Init network stack + static STACK: StaticCell>> = StaticCell::new(); + static STACK_RESOURCES: StaticCell> = StaticCell::new(); + + let stack = &*STACK.init(Stack::new( + net_device, + embassy_net::Config::default(), + STACK_RESOURCES.init(StackResources::new()), + seed, + )); + + let http_fut = async { + stack.wait_config_up().await; + + info!("We have network!"); + + let mut rx_buffer = [0; 4096]; + let mut tx_buffer = [0; 4096]; + let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); + socket.set_timeout(Some(Duration::from_secs(10))); + + let hostname = "ecdsa-test.germancoding.com"; + + let mut remote = stack + .dns_query(hostname, smoltcp::wire::DnsQueryType::A) + .await + .unwrap(); + let remote_endpoint = (remote.pop().unwrap(), 443); + info!("connecting to {:?}...", remote_endpoint); + let r = socket.connect(remote_endpoint).await; + if let Err(e) = r { + warn!("connect error: {:?}", e); + return; + } + info!("TCP connected!"); + + let mut read_record_buffer = [0; 16384]; + let mut write_record_buffer = [0; 16384]; + let config = TlsConfig::new().with_server_name(hostname); + let mut tls = TlsConnection::new(socket, &mut read_record_buffer, &mut write_record_buffer); + + tls.open(TlsContext::new( + &config, + UnsecureProvider::new::(ChaCha8Rng::seed_from_u64(seed)), + )) + .await + .expect("error establishing TLS connection"); + + info!("TLS Established!"); + + let request = Request::get("/") + .host(hostname) + .content_type(ContentType::TextPlain) + .build(); + request.write(&mut tls).await.unwrap(); + + let mut rx_buf = [0; 4096]; + let response = Response::read(&mut tls, reqwless::request::Method::GET, &mut rx_buf) + .await + .unwrap(); + + let mut buf = vec![0; 16384]; + let len = response + .body() + .reader() + .read_to_end(&mut buf) + .await + .unwrap(); + info!("{:?}", core::str::from_utf8(&buf[..len])); + }; + + embassy_futures::join::join3( + stack.run(), + runner.run(|ipv4| { + let Some(addr) = ipv4.address else { + warn!("PPP did not provide an IP address."); + return; + }; + let mut dns_servers = heapless::Vec::new(); + for s in ipv4.dns_servers.iter().flatten() { + let _ = dns_servers.push(embassy_net::Ipv4Address::from_bytes(&s.0)); + } + let config = embassy_net::ConfigV4::Static(embassy_net::StaticConfigV4 { + address: embassy_net::Ipv4Cidr::new( + embassy_net::Ipv4Address::from_bytes(&addr.0), + 0, + ), + gateway: None, + dns_servers, + }); + + stack.set_config_v4(config); + }), + http_fut, + ) + .await; + + unreachable!(); +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 3cd5460..4bf8d87 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,7 +1,4 @@ [toolchain] -channel = "nightly-2023-06-28" -components = [ "rust-src", "rustfmt", "llvm-tools-preview", "clippy" ] -targets = [ - "x86_64-unknown-linux-gnu", - "thumbv7em-none-eabihf" -] +channel = "1.80" +components = ["rust-src", "rustfmt", "llvm-tools", "clippy"] +targets = ["x86_64-unknown-linux-gnu", "thumbv7em-none-eabihf"] diff --git a/src/asynch/control.rs b/src/asynch/control.rs new file mode 100644 index 0000000..9d4a298 --- /dev/null +++ b/src/asynch/control.rs @@ -0,0 +1,169 @@ +use core::cell::Cell; + +use atat::{asynch::AtatClient, response_slot::ResponseSlotGuard}; +use embassy_sync::{blocking_mutex::raw::NoopRawMutex, channel::Sender}; +use embassy_time::{with_timeout, Duration, Timer}; + +use crate::{ + command::{ + general::{types::FirmwareVersion, GetFirmwareVersion}, + gpio::{types::GpioMode, SetGpioConfiguration}, + network_service::{ + responses::{OperatorSelection, SignalQuality}, + GetOperatorSelection, GetSignalQuality, + }, + }, + error::Error, +}; + +use super::{ + runner::MAX_CMD_LEN, + state::{self, LinkState, OperationState}, +}; + +pub(crate) struct ProxyClient<'a, const INGRESS_BUF_SIZE: usize> { + pub(crate) req_sender: Sender<'a, NoopRawMutex, heapless::Vec, 1>, + pub(crate) res_slot: &'a atat::ResponseSlot, + cooldown_timer: Cell>, +} + +impl<'a, const INGRESS_BUF_SIZE: usize> ProxyClient<'a, INGRESS_BUF_SIZE> { + pub fn new( + req_sender: Sender<'a, NoopRawMutex, heapless::Vec, 1>, + res_slot: &'a atat::ResponseSlot, + ) -> Self { + Self { + req_sender, + res_slot, + cooldown_timer: Cell::new(None), + } + } + + async fn wait_response( + &self, + timeout: Duration, + ) -> Result, atat::Error> { + with_timeout(timeout, self.res_slot.get()) + .await + .map_err(|_| atat::Error::Timeout) + } +} + +impl<'a, const INGRESS_BUF_SIZE: usize> atat::asynch::AtatClient + for &ProxyClient<'a, INGRESS_BUF_SIZE> +{ + async fn send(&mut self, cmd: &Cmd) -> Result { + let mut buf = [0u8; MAX_CMD_LEN]; + let len = cmd.write(&mut buf); + + if len < 50 { + trace!( + "Sending command: {:?}", + atat::helpers::LossyStr(&buf[..len]) + ); + } else { + trace!("Sending command with long payload ({} bytes)", len); + } + + if let Some(cooldown) = self.cooldown_timer.take() { + cooldown.await + } + + // TODO: Guard against race condition! + with_timeout( + Duration::from_secs(1), + self.req_sender + .send(heapless::Vec::try_from(&buf[..len]).unwrap()), + ) + .await + .map_err(|_| atat::Error::Timeout)?; + + self.cooldown_timer.set(Some(Timer::after_millis(20))); + + if !Cmd::EXPECTS_RESPONSE_CODE { + cmd.parse(Ok(&[])) + } else { + let response = self + .wait_response(Duration::from_millis(Cmd::MAX_TIMEOUT_MS.into())) + .await?; + let response: &atat::Response = &response.borrow(); + cmd.parse(response.into()) + } + } +} + +pub struct Control<'a, const INGRESS_BUF_SIZE: usize> { + state_ch: state::Runner<'a>, + at_client: ProxyClient<'a, INGRESS_BUF_SIZE>, +} + +impl<'a, const INGRESS_BUF_SIZE: usize> Control<'a, INGRESS_BUF_SIZE> { + pub(crate) fn new( + state_ch: state::Runner<'a>, + req_sender: Sender<'a, NoopRawMutex, heapless::Vec, 1>, + res_slot: &'a atat::ResponseSlot, + ) -> Self { + Self { + state_ch, + at_client: ProxyClient::new(req_sender, res_slot), + } + } + + pub fn link_state(&self) -> LinkState { + self.state_ch.link_state(None) + } + + pub fn operation_state(&self) -> OperationState { + self.state_ch.operation_state(None) + } + + pub fn desired_state(&self) -> OperationState { + self.state_ch.desired_state(None) + } + + pub fn set_desired_state(&self, ps: OperationState) { + self.state_ch.set_desired_state(ps); + } + + pub async fn wait_for_desired_state(&self, ps: OperationState) { + self.state_ch.wait_for_desired_state(ps).await + } + + pub async fn wait_for_operation_state(&self, ps: OperationState) { + self.state_ch.wait_for_operation_state(ps).await + } + + pub async fn get_signal_quality(&self) -> Result { + Ok(self.send(&GetSignalQuality).await?) + } + + pub async fn get_operator(&self) -> Result { + Ok(self.send(&GetOperatorSelection).await?) + } + + pub async fn get_version(&self) -> Result { + let res = self.send(&GetFirmwareVersion).await?; + Ok(res.version) + } + + pub async fn set_gpio_configuration( + &self, + gpio_id: u8, + gpio_mode: GpioMode, + ) -> Result<(), Error> { + self.send(&SetGpioConfiguration { gpio_id, gpio_mode }) + .await?; + Ok(()) + } + + /// Send an AT command to the modem This is useful if you have special + /// configuration but might break the drivers functionality if your settings + /// interfere with the drivers settings + pub async fn send(&self, cmd: &Cmd) -> Result { + if self.operation_state() == OperationState::PowerDown { + return Err(Error::Uninitialized); + } + + Ok((&self.at_client).send_retry::(cmd).await?) + } +} diff --git a/src/asynch/mod.rs b/src/asynch/mod.rs new file mode 100644 index 0000000..10d2bd6 --- /dev/null +++ b/src/asynch/mod.rs @@ -0,0 +1,12 @@ +pub mod control; +mod network; +mod pwr; +mod resources; +pub mod runner; +pub mod state; +mod urc_handler; + +pub use resources::Resources; +pub use runner::Runner; +#[cfg(feature = "internal-network-stack")] +pub use state::Device; diff --git a/src/asynch/network.rs b/src/asynch/network.rs new file mode 100644 index 0000000..7bce97b --- /dev/null +++ b/src/asynch/network.rs @@ -0,0 +1,650 @@ +use core::{cmp::Ordering, future::poll_fn, marker::PhantomData, task::Poll}; + +use crate::{ + asynch::state::OperationState, + command::{ + general::GetCIMI, + mobile_control::{ + responses::ModuleFunctionality, + types::{Functionality, PowerMode}, + GetModuleFunctionality, SetModuleFunctionality, + }, + network_service::{ + responses::OperatorSelection, + types::{NetworkRegistrationUrcConfig, OperatorSelectionMode}, + GetNetworkRegistrationStatus, GetOperatorSelection, SetNetworkRegistrationStatus, + SetOperatorSelection, + }, + psn::{ + responses::GPRSAttached, + types::{ + ContextId, EPSNetworkRegistrationUrcConfig, GPRSAttachedState, + GPRSNetworkRegistrationUrcConfig, PDPContextStatus, ProfileId, + }, + GetEPSNetworkRegistrationStatus, GetGPRSAttached, GetGPRSNetworkRegistrationStatus, + GetPDPContextState, SetEPSNetworkRegistrationStatus, SetGPRSNetworkRegistrationStatus, + SetPDPContextState, + }, + }, + config::CellularConfig, + error::Error, + modules::ModuleParams, + registration::ProfileState, +}; + +use super::state; + +use atat::asynch::AtatClient; +use embassy_futures::select::{select, Either}; + +use embassy_time::{Duration, Timer}; + +pub struct NetDevice<'a, 'b, C, A> { + ch: &'b state::Runner<'a>, + at_client: A, + _config: PhantomData, +} + +impl<'a, 'b, C, A> NetDevice<'a, 'b, C, A> +where + C: CellularConfig<'a>, + A: AtatClient, +{ + pub fn new(ch: &'b state::Runner<'a>, at_client: A) -> Self { + Self { + ch, + at_client, + _config: PhantomData, + } + } + + /// Register with the cellular network + /// + /// # Errors + /// + /// Returns an error if any of the internal network operations fail. + /// + async fn register_network(&mut self, mcc_mnc: Option<()>) -> Result<(), Error> { + self.prepare_connect().await?; + + if mcc_mnc.is_none() { + // If no MCC/MNC is given, make sure we are in automatic network + // selection mode. + + // Set automatic operator selection, if not already set + let OperatorSelection { mode, .. } = self.at_client.send(&GetOperatorSelection).await?; + + if mode != OperatorSelectionMode::Automatic { + // Don't check error code here as some modules can + // return an error as we still have the radio off (but they still + // obey) + let _ = self + .at_client + .send(&SetOperatorSelection { + mode: OperatorSelectionMode::Automatic, + format: None, + }) + .await; + } + } + + // Reset the current registration status + self.ch.update_registration_with(|f| f.reset()); + + self.at_client + .send(&SetModuleFunctionality { + fun: Functionality::Full, + rst: None, + }) + .await?; + + if mcc_mnc.is_some() { + // TODO: If MCC & MNC is set, register with manual operator selection. + // This is currently not supported! + + // let crate::command::network_service::responses::OperatorSelection { mode, .. } = self + // .at_client + // .send(&crate::command::network_service::GetOperatorSelection) + // .await?; + + // // Only run AT+COPS=0 if currently de-registered, to avoid PLMN reselection + // if !matches!( + // mode, + // crate::command::network_service::types::OperatorSelectionMode::Automatic + // | crate::command::network_service::types::OperatorSelectionMode::Manual + // ) { + // self.at_client + // .send(&crate::command::network_service::SetOperatorSelection { + // mode: crate::command::network_service::types::OperatorSelectionMode::Automatic, + // format: Some(C::OPERATOR_FORMAT as u8), + // }) + // .await?; + // } + unimplemented!() + } + + Ok(()) + } + + async fn prepare_connect(&mut self) -> Result<(), Error> { + // CREG URC + self.at_client + .send(&SetNetworkRegistrationStatus { + n: NetworkRegistrationUrcConfig::UrcEnabled, + }) + .await?; + + // CGREG URC + self.at_client + .send(&SetGPRSNetworkRegistrationStatus { + n: GPRSNetworkRegistrationUrcConfig::UrcEnabled, + }) + .await?; + + // CEREG URC + self.at_client + .send(&SetEPSNetworkRegistrationStatus { + n: EPSNetworkRegistrationUrcConfig::UrcEnabled, + }) + .await?; + + for _ in 0..10 { + if self.at_client.send(&GetCIMI).await.is_ok() { + break; + } + + Timer::after_secs(1).await; + } + + Ok(()) + } + + // Perform at full factory reset of the module, clearing all NVM sectors in the process + // TODO: Should this be moved to control? + // async fn factory_reset(&mut self) -> Result<(), Error> { + // self.at_client + // .send(&SetFactoryConfiguration { + // fs_op: FSFactoryRestoreType::NoRestore, + // nvm_op: NVMFactoryRestoreType::NVMFlashSectors, + // }) + // .await?; + + // info!("Successfully factory reset modem!"); + + // if self.soft_reset(true).await.is_err() { + // self.pwr.reset().await?; + // } + + // Ok(()) + // } + + /// Reset the module by sending AT CFUN command + async fn soft_reset(&mut self, sim_reset: bool) -> Result<(), Error> { + trace!( + "Attempting to soft reset of the modem with sim reset: {}.", + sim_reset + ); + + let fun = if sim_reset { + Functionality::SilentResetWithSimReset + } else { + Functionality::SilentReset + }; + + match self + .at_client + .send(&SetModuleFunctionality { fun, rst: None }) + .await + { + Ok(_) => { + info!("Successfully soft reset modem!"); + Ok(()) + } + Err(err) => { + error!("Failed to soft reset modem: {:?}", err); + Err(Error::Atat(err)) + } + } + } + + /// Check if we are registered to a network technology (uses +CxREG family + /// commands) + async fn wait_network_registered(&mut self, timeout: Duration) -> Result<(), Error> { + let state_runner = self.ch.clone(); + let update_fut = async { + loop { + self.update_registration().await; + + Timer::after_millis(300).await; + } + }; + + Ok(embassy_time::with_timeout( + timeout, + select( + update_fut, + poll_fn(|cx| match state_runner.is_registered(Some(cx)) { + true => Poll::Ready(()), + false => Poll::Pending, + }), + ), + ) + .await + .map(drop)?) + } + + async fn update_registration(&mut self) { + if let Ok(reg) = self.at_client.send(&GetNetworkRegistrationStatus).await { + self.ch + .update_registration_with(|state| state.compare_and_set(reg.into())); + } + + if let Ok(reg) = self.at_client.send(&GetGPRSNetworkRegistrationStatus).await { + self.ch + .update_registration_with(|state| state.compare_and_set(reg.into())); + } + + if let Ok(reg) = self.at_client.send(&GetEPSNetworkRegistrationStatus).await { + self.ch + .update_registration_with(|state| state.compare_and_set(reg.into())); + } + } + + async fn radio_off(&mut self) -> Result<(), Error> { + #[cfg(not(feature = "use-upsd-context-activation"))] + self.ch.set_profile_state(ProfileState::ShouldBeDown); + + let module_cfun = self + .ch + .module() + .ok_or(Error::Uninitialized)? + .radio_off_cfun(); + + let cfun_power_mode = PowerMode::try_from(module_cfun as u8).ok(); + + let mut last_err = None; + for _ in 0..3 { + match self + .at_client + .send(&SetModuleFunctionality { + fun: module_cfun, + rst: None, + }) + .await + { + Ok(_) => return Ok(()), + Err(e) => { + last_err.replace(e); + + if let Some(expected_mode) = cfun_power_mode { + match self.at_client.send(&GetModuleFunctionality).await { + Ok(ModuleFunctionality { power_mode, .. }) + if power_mode == expected_mode => + { + // If we got no response, abort the command and + // check the status + return Ok(()); + } + _ => {} + } + } + } + } + } + + Err(last_err.unwrap().into()) + } + + pub async fn run(&mut self) -> Result<(), Error> { + self.run_to_desired().await?; + + loop { + match select( + self.ch.wait_for_desired_state_change(), + self.ch.wait_registration_change(), + ) + .await + { + Either::First(_) => { + self.run_to_desired().await?; + } + Either::Second(false) => { + warn!("Lost network registration. Setting operating state back to initialized"); + self.ch.set_operation_state(OperationState::Initialized); + self.run_to_desired().await?; + } + Either::Second(true) => { + // This flag will be set if we had been knocked out + // of our PDP context by a network outage and need + // to get it back again; make sure to get this in the + // queue before any user registratioon status callback + // so that everything is sorted for them + #[cfg(not(feature = "use-upsd-context-activation"))] + if self.ch.get_profile_state() == ProfileState::RequiresReactivation { + self.activate_context(C::CONTEXT_ID, C::PROFILE_ID).await?; + self.ch.set_profile_state(ProfileState::ShouldBeUp); + } + } + } + } + } + + async fn run_to_desired(&mut self) -> Result<(), Error> { + loop { + let current_state = self.ch.operation_state(None); + let desired_state = self.ch.desired_state(None); + + debug!( + "State transition: {:?} -> {:?}", + current_state, desired_state + ); + + match (current_state, desired_state.cmp(¤t_state)) { + (_, Ordering::Equal) => break, + + (OperationState::PowerDown, Ordering::Greater) => { + self.ch + .wait_for_operation_state(OperationState::Initialized) + .await + } + (OperationState::Initialized, Ordering::Greater) => { + self.register_network(None).await?; + self.wait_network_registered(Duration::from_secs(180)) + .await?; + self.ch.set_operation_state(OperationState::Connected); + } + (OperationState::Connected, Ordering::Greater) => { + match self.connect(C::APN, C::PROFILE_ID, C::CONTEXT_ID).await { + Ok(_) => { + #[cfg(not(feature = "use-upsd-context-activation"))] + self.ch + .set_profile_state(crate::registration::ProfileState::ShouldBeUp); + + self.ch.set_operation_state(OperationState::DataEstablished); + } + Err(err) => { + // Switch radio off after failure + let _ = self.radio_off().await; + return Err(err); + } + } + } + + // TODO: do proper backwards "single stepping" + (OperationState::Connected, Ordering::Less) => { + self.ch.set_operation_state(OperationState::Initialized); + } + (OperationState::DataEstablished, Ordering::Less) => { + self.ch.set_operation_state(OperationState::Connected); + } + + (OperationState::DataEstablished, Ordering::Greater) => unreachable!(), + (OperationState::Initialized, Ordering::Less) => return Err(Error::PoweredDown), + (OperationState::PowerDown, _) => return Err(Error::PoweredDown), + } + } + Ok(()) + } + + #[allow(unused_variables)] + async fn connect( + &mut self, + apn_info: crate::config::Apn<'_>, + profile_id: ProfileId, + context_id: ContextId, + ) -> Result<(), Error> { + #[cfg(not(feature = "use-upsd-context-activation"))] + self.define_context(context_id, apn_info).await?; + + // This step _shouldn't_ be necessary. However, for reasons I don't + // understand, SARA-R4 can be registered but not attached (i.e. AT+CGATT + // returns 0) on both RATs (unh?). Phil Ware, who knows about these + // things, always goes through (a) register, (b) wait for AT+CGATT to + // return 1 and then (c) check that a context is active with AT+CGACT or + // using AT+UPSD (even for EUTRAN). Since this sequence works for both + // RANs, it is best to be consistent. + let mut attached = false; + for _ in 0..10 { + if let Ok(true) = self.is_network_attached().await { + attached = true; + break; + }; + Timer::after_secs(1).await; + } + if !attached { + return Err(Error::AttachTimeout); + } + + // Activate the context + #[cfg(feature = "use-upsd-context-activation")] + self.activate_context_upsd(profile_id, apn_info).await?; + #[cfg(not(feature = "use-upsd-context-activation"))] + self.activate_context(context_id, profile_id).await?; + + Ok(()) + } + + /// Define a PDP context + #[cfg(not(feature = "use-upsd-context-activation"))] + async fn define_context( + &mut self, + cid: ContextId, + apn_info: crate::config::Apn<'_>, + ) -> Result<(), Error> { + use crate::command::psn::{ + types::AuthenticationType, SetAuthParameters, SetPDPContextDefinition, + }; + + self.at_client + .send(&SetModuleFunctionality { + fun: self + .ch + .module() + .ok_or(Error::Uninitialized)? + .radio_off_cfun(), + rst: None, + }) + .await?; + + if let crate::config::Apn::Given { + name, + username, + password, + } = apn_info + { + self.at_client + .send(&SetPDPContextDefinition { + cid, + pdp_type: "IP", + apn: name, + }) + .await?; + + if let Some(username) = username { + self.at_client + .send(&SetAuthParameters { + cid, + auth_type: AuthenticationType::Auto, + username, + password: password.unwrap_or_default(), + }) + .await?; + } + } + + self.at_client + .send(&SetModuleFunctionality { + fun: Functionality::Full, + rst: None, + }) + .await?; + + Ok(()) + } + + // Make sure we are attached to the cellular network. + async fn is_network_attached(&mut self) -> Result { + // Check for AT+CGATT to return 1 + let GPRSAttached { state } = self.at_client.send(&GetGPRSAttached).await?; + Ok(state == GPRSAttachedState::Attached) + } + + /// Activate context using AT+UPSD commands. + #[cfg(feature = "use-upsd-context-activation")] + async fn activate_context_upsd( + &mut self, + profile_id: ProfileId, + apn_info: crate::config::Apn<'_>, + ) -> Result<(), Error> { + // SARA-U2 pattern: everything is done through AT+UPSD + // Set up the APN + if let crate::config::Apn::Given { + name, + username, + password, + } = apn_info + { + self.at_client + .send(&psn::SetPacketSwitchedConfig { + profile_id, + param: psn::types::PacketSwitchedParam::APN( + String::<99>::try_from(name).unwrap(), + ), + }) + .await?; + + // Set up the user name + if let Some(user_name) = username { + self.at_client + .send(&psn::SetPacketSwitchedConfig { + profile_id, + param: psn::types::PacketSwitchedParam::Username( + String::<64>::try_from(user_name).unwrap(), + ), + }) + .await?; + } + + // Set up the password + if let Some(password) = password { + self.at_client + .send(&psn::SetPacketSwitchedConfig { + profile_id, + param: psn::types::PacketSwitchedParam::Password( + String::<64>::try_from(password).unwrap(), + ), + }) + .await?; + } + } + // Set up the dynamic IP address assignment. + self.at_client + .send(&psn::SetPacketSwitchedConfig { + profile_id, + param: psn::types::PacketSwitchedParam::IPAddress(Ipv4Addr::unspecified().into()), + }) + .await?; + + // Automatic authentication protocol selection + self.at_client + .send(&psn::SetPacketSwitchedConfig { + profile_id, + param: psn::types::PacketSwitchedParam::Authentication(AuthenticationType::Auto), + }) + .await?; + + self.at_client + .send(&psn::SetPacketSwitchedAction { + profile_id, + action: psn::types::PacketSwitchedAction::Activate, + }) + .await?; + + Ok(()) + } + + /// Activate context using 3GPP commands + #[cfg(not(feature = "use-upsd-context-activation"))] + async fn activate_context( + &mut self, + cid: ContextId, + _profile_id: ProfileId, + ) -> Result<(), Error> { + for _ in 0..5 { + #[cfg(feature = "sara-r422")] + { + // Note: it seems a bit strange to do this first, + // rather than just querying the +CGACT status, + // but a specific case has been found where SARA-R422 + // indicated that it was activated whereas in fact, + // at least for the internal clients (so sockets, HTTP + // and MQTT), it was not. Forcing with AT+CGACT=1,x has + // been shown to fix that. We don't do it in all + // cases as SARA-R41x modules object to that. + self.at_client + .send(&SetPDPContextState { + status: PDPContextStatus::Activated, + cid: Some(cid), + }) + .await?; + } + + let context_states = self.at_client.send(&GetPDPContextState).await?; + + let activated = context_states + .iter() + .find_map(|state| { + if state.cid == cid { + Some(state.status == PDPContextStatus::Activated) + } else { + None + } + }) + .unwrap_or(false); + + if activated { + // [Re]attach a PDP context to an internal module profile + #[cfg(feature = "context-mapping-required")] + { + self.at_client + .send(&psn::SetPacketSwitchedConfig { + profile_id: _profile_id, + param: psn::types::PacketSwitchedParam::ProtocolType( + psn::types::ProtocolType::IPv4, + ), + }) + .await?; + + self.at_client + .send(&psn::SetPacketSwitchedConfig { + profile_id: _profile_id, + param: psn::types::PacketSwitchedParam::MapProfile(cid), + }) + .await?; + + // SARA-R5 pattern: the context also has to be + // activated and we're not actually done + // until the +UUPSDA URC comes back, + #[cfg(feature = "sara-r5")] + self.at_client + .send(&psn::SetPacketSwitchedAction { + profile_id, + action: psn::types::PacketSwitchedAction::Activate, + }) + .await?; + } + + return Ok(()); + } else { + #[cfg(not(feature = "sara-r422"))] + self.at_client + .send(&SetPDPContextState { + status: PDPContextStatus::Activated, + cid: Some(cid), + }) + .await?; + } + } + Err(Error::ContextActivationTimeout) + } +} diff --git a/src/asynch/pwr.rs b/src/asynch/pwr.rs new file mode 100644 index 0000000..b3a573a --- /dev/null +++ b/src/asynch/pwr.rs @@ -0,0 +1,138 @@ +use embassy_time::{Duration, Timer}; +use embedded_hal::digital::{InputPin as _, OutputPin as _}; + +use crate::{ + asynch::state::OperationState, + config::CellularConfig, + error::Error, + modules::{Generic, ModuleParams as _}, +}; + +use super::state; + +const GENERIC_PWR_ON_TIMES: [u16; 2] = [300, 2000]; + +pub(crate) struct PwrCtrl<'a, 'b, C> { + config: &'b mut C, + ch: &'b state::Runner<'a>, +} + +impl<'a, 'b, C> PwrCtrl<'a, 'b, C> +where + C: CellularConfig<'a>, +{ + pub(crate) fn new(ch: &'b state::Runner<'a>, config: &'b mut C) -> Self { + Self { ch, config } + } + + pub(crate) fn has_power(&mut self) -> Result { + if let Some(pin) = self.config.vint_pin() { + if pin.is_high().map_err(|_| Error::IoPin)? { + Ok(true) + } else { + Ok(false) + } + } else { + info!("No VInt pin configured"); + Ok(true) + } + } + + /// Reset the module by driving it's `RESET_N` pin low for + /// `Module::reset_hold()` duration + /// + /// **NOTE** This function will reset NVM settings! + pub(crate) async fn reset(&mut self) -> Result<(), Error> { + warn!("Hard resetting Ublox Cellular Module"); + if let Some(pin) = self.config.reset_pin() { + pin.set_low().ok(); + Timer::after( + self.ch + .module() + .map(|m| m.reset_hold()) + .unwrap_or(Generic.reset_hold()), + ) + .await; + pin.set_high().ok(); + Timer::after( + self.ch + .module() + .map(|m| m.boot_wait()) + .unwrap_or(Generic.boot_wait()), + ) + .await; + } else { + warn!("No reset pin configured"); + } + Ok(()) + } + + pub(crate) async fn power_up(&mut self) -> Result<(), Error> { + if !self.has_power()? { + debug!("Attempting to power up device"); + + for generic_time in GENERIC_PWR_ON_TIMES { + let pull_time = self + .ch + .module() + .map(|m| m.power_on_pull_time()) + .unwrap_or(Generic.power_on_pull_time()) + .unwrap_or(Duration::from_millis(generic_time as _)); + if let Some(pin) = self.config.power_pin() { + pin.set_low().map_err(|_| Error::IoPin)?; + Timer::after(pull_time).await; + pin.set_high().map_err(|_| Error::IoPin)?; + + Timer::after( + self.ch + .module() + .map(|m| m.boot_wait()) + .unwrap_or(Generic.boot_wait()), + ) + .await; + + if !self.has_power()? { + if self.ch.module().is_some() { + return Err(Error::PoweredDown); + } + continue; + } + + debug!("Powered up"); + return Ok(()); + } else { + warn!("No power pin configured"); + return Ok(()); + } + } + Err(Error::PoweredDown) + } else { + Ok(()) + } + } + + pub(crate) async fn power_down(&mut self) -> Result<(), Error> { + if self.has_power()? { + if let Some(pin) = self.config.power_pin() { + pin.set_low().map_err(|_| Error::IoPin)?; + Timer::after( + self.ch + .module() + .map(|m| m.power_off_pull_time()) + .unwrap_or(Generic.power_off_pull_time()), + ) + .await; + pin.set_high().map_err(|_| Error::IoPin)?; + self.ch.set_operation_state(OperationState::PowerDown); + debug!("Powered down"); + + Timer::after_secs(1).await; + } else { + warn!("No power pin configured"); + } + } else { + self.ch.set_operation_state(OperationState::PowerDown); + } + Ok(()) + } +} diff --git a/src/asynch/resources.rs b/src/asynch/resources.rs new file mode 100644 index 0000000..0bdc7c0 --- /dev/null +++ b/src/asynch/resources.rs @@ -0,0 +1,46 @@ +use atat::{ResponseSlot, UrcChannel}; +use embassy_sync::{blocking_mutex::raw::NoopRawMutex, channel::Channel}; + +use super::{ + runner::{CMUX_CHANNELS, CMUX_CHANNEL_SIZE, MAX_CMD_LEN, URC_SUBSCRIBERS}, + state, +}; +use crate::command::Urc; + +pub struct Resources { + pub(crate) ch: state::State, + + pub(crate) res_slot: ResponseSlot, + pub(crate) req_slot: Channel, 1>, + + pub(crate) urc_channel: UrcChannel, + pub(crate) ingress_buf: [u8; INGRESS_BUF_SIZE], + + pub(crate) mux: embassy_at_cmux::Mux, +} + +impl Default + for Resources +{ + fn default() -> Self { + Self::new() + } +} + +impl + Resources +{ + pub fn new() -> Self { + Self { + ch: state::State::new(), + + res_slot: ResponseSlot::new(), + req_slot: Channel::new(), + + urc_channel: UrcChannel::new(), + ingress_buf: [0; INGRESS_BUF_SIZE], + + mux: embassy_at_cmux::Mux::new(), + } + } +} diff --git a/src/asynch/runner.rs b/src/asynch/runner.rs new file mode 100644 index 0000000..ae1216a --- /dev/null +++ b/src/asynch/runner.rs @@ -0,0 +1,694 @@ +use core::{future::poll_fn, task::Poll}; + +use crate::{ + asynch::{network::NetDevice, state::OperationState}, + command::{ + control::{ + types::{ + BaudRate, Circuit108Behaviour, Circuit109Behaviour, Echo, ResultCodeSelection, + }, + SetCircuit108Behaviour, SetCircuit109Behaviour, SetDataRate, SetEcho, + SetResultCodeSelection, + }, + device_lock::{responses::PinStatus, types::PinStatusCode, GetPinStatus}, + general::{responses::FirmwareVersion, GetCCID, GetFirmwareVersion, GetModelId}, + ipc::SetMultiplexing, + mobile_control::{ + types::{Functionality, TerminationErrorMode}, + SetModuleFunctionality, SetReportMobileTerminationError, + }, + network_service::SetChannelAndNetworkEnvDesc, + networking::SetEmbeddedPortFiltering, + psn::EnterPPP, + system_features::{types::PowerSavingMode, SetPowerSavingControl}, + Urc, AT, + }, + config::{CellularConfig, Transport}, + error::Error, + modules::{Module, ModuleParams as _}, + DEFAULT_BAUD_RATE, +}; + +use super::{ + control::{Control, ProxyClient}, + pwr::PwrCtrl, + state, + urc_handler::UrcHandler, + Resources, +}; + +use atat::{ + asynch::{AtatClient, SimpleClient}, + AtatIngress as _, UrcChannel, +}; + +use embassy_futures::{ + join::join, + select::{select3, Either3}, +}; +use embassy_sync::{blocking_mutex::raw::NoopRawMutex, channel::Channel}; +use embassy_time::{with_timeout, Duration, Instant, Timer}; +use embedded_io_async::Write as _; + +pub(crate) const URC_SUBSCRIBERS: usize = 2; + +pub(crate) const MAX_CMD_LEN: usize = 128; + +pub const CMUX_MAX_FRAME_SIZE: usize = 128; +pub const CMUX_CHANNEL_SIZE: usize = CMUX_MAX_FRAME_SIZE * 4; + +pub const CMUX_CHANNELS: usize = 2; + +async fn at_bridge<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize>( + (rx, tx): ( + &mut embassy_at_cmux::ChannelRx<'a, CMUX_CHANNEL_SIZE>, + &mut embassy_at_cmux::ChannelTx<'a, CMUX_CHANNEL_SIZE>, + ), + req_slot: &Channel, 1>, + ingress: &mut atat::Ingress< + 'a, + atat::AtDigester, + Urc, + INGRESS_BUF_SIZE, + URC_CAPACITY, + URC_SUBSCRIBERS, + >, +) -> ! { + ingress.clear(); + + let tx_fut = async { + loop { + let msg = req_slot.receive().await; + let _ = tx.write_all(&msg).await; + } + }; + + embassy_futures::join::join(tx_fut, ingress.read_from(rx)).await; + + unreachable!() +} + +/// Background runner for the Ublox Module. +/// +/// You must call `.run()` in a background task for the Ublox Module to operate. +pub struct Runner<'a, T, C, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> { + transport: T, + + pub ch: state::Runner<'a>, + pub config: C, + pub urc_channel: &'a UrcChannel, + + pub ingress: atat::Ingress< + 'a, + atat::AtDigester, + Urc, + INGRESS_BUF_SIZE, + URC_CAPACITY, + URC_SUBSCRIBERS, + >, + pub res_slot: &'a atat::ResponseSlot, + pub req_slot: &'a Channel, 1>, + + pub mux_runner: embassy_at_cmux::Runner<'a, CMUX_CHANNELS, CMUX_CHANNEL_SIZE>, + + at_channel: ( + embassy_at_cmux::ChannelRx<'a, CMUX_CHANNEL_SIZE>, + embassy_at_cmux::ChannelTx<'a, CMUX_CHANNEL_SIZE>, + embassy_at_cmux::ChannelLines<'a, CMUX_CHANNEL_SIZE>, + ), + data_channel: embassy_at_cmux::Channel<'a, CMUX_CHANNEL_SIZE>, + + #[cfg(feature = "ppp")] + pub ppp_runner: Option>, +} + +impl<'a, T, C, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> + Runner<'a, T, C, INGRESS_BUF_SIZE, URC_CAPACITY> +where + T: Transport, + C: CellularConfig<'a> + 'a, +{ + pub fn new( + transport: T, + resources: &'a mut Resources, + config: C, + ) -> (Self, Control<'a, INGRESS_BUF_SIZE>) { + let ch_runner = state::Runner::new(&mut resources.ch); + + let ingress = atat::Ingress::new( + atat::AtDigester::::new(), + &mut resources.ingress_buf, + &resources.res_slot, + &resources.urc_channel, + ); + + let (mux_runner, channels) = resources.mux.start(); + let mut channel_iter = channels.into_iter(); + + let at_channel = channel_iter.next().unwrap().split(); + let data_channel = channel_iter.next().unwrap(); + + let control = Control::new( + ch_runner.clone(), + resources.req_slot.sender(), + &resources.res_slot, + ); + + ( + Self { + transport, + + ch: ch_runner, + config, + urc_channel: &resources.urc_channel, + + ingress, + res_slot: &resources.res_slot, + req_slot: &resources.req_slot, + + mux_runner, + + at_channel, + data_channel, + + #[cfg(feature = "ppp")] + ppp_runner: None, + }, + control, + ) + } + + #[cfg(feature = "ppp")] + pub fn ppp_stack<'d: 'a, const N_RX: usize, const N_TX: usize>( + &mut self, + ppp_state: &'d mut embassy_net_ppp::State, + ) -> embassy_net_ppp::Device<'d> { + let (net_device, ppp_runner) = embassy_net_ppp::new(ppp_state); + self.ppp_runner.replace(ppp_runner); + net_device + } + + #[cfg(feature = "internal-network-stack")] + pub fn internal_stack(&mut self) -> state::Device { + // let data_channel = self.data_channel; + state::Device { + shared: &self.ch.shared, + desired_state_pub_sub: &self.ch.desired_state_pub_sub, + urc_subscription: self.urc_channel.subscribe().unwrap(), + } + } + + /// Probe a given baudrate with the goal of establishing initial + /// communication with the module, so we can reconfigure it for desired + /// baudrate + async fn probe_baud(&mut self, baudrate: BaudRate) -> Result<(), Error> { + info!( + "Probing cellular module using baud rate: {}", + baudrate as u32 + ); + self.transport.set_baudrate(baudrate as u32); + let mut cmd_buf = [0u8; 16]; + + { + let mut at_client = SimpleClient::new( + &mut self.transport, + atat::AtDigester::::new(), + &mut cmd_buf, + C::AT_CONFIG, + ); + + // Allow auto bauding to kick in + embassy_time::with_timeout(Duration::from_secs(5), async { + loop { + if at_client.send(&AT).await.is_ok() { + break; + } + Timer::after(Duration::from_millis(100)).await; + } + }) + .await?; + + // Lets take a shortcut if we successfully probed for the desired + // baudrate + if baudrate == C::BAUD_RATE { + return Ok(()); + } + + at_client + .send_retry(&SetDataRate { rate: C::BAUD_RATE }) + .await?; + } + + self.transport.set_baudrate(C::BAUD_RATE as u32); + + // On the UART AT interface, after the reception of the "OK" result code + // for the +IPR command, the DTE shall wait for at least 100 ms before + // issuing a new AT command; this is to guarantee a proper baud rate + // reconfiguration + Timer::after_millis(100).await; + + // Verify communication + SimpleClient::new( + &mut self.transport, + atat::AtDigester::::new(), + &mut cmd_buf, + C::AT_CONFIG, + ) + .send_retry(&AT) + .await?; + + Ok(()) + } + + async fn init(&mut self) -> Result<(), Error> { + // Initialize a new ublox device to a known state (set RS232 settings) + debug!("Initializing cellular module"); + + let mut pwr = PwrCtrl::new(&self.ch, &mut self.config); + if let Err(e) = pwr.power_up().await { + pwr.power_down().await?; + return Err(e); + }; + + // Probe all possible baudrates with the goal of establishing initial + // communication with the module, so we can reconfigure it for desired + // baudrate. + // + // Start with the two most likely + let mut found_baudrate = false; + + for baudrate in [ + C::BAUD_RATE, + DEFAULT_BAUD_RATE, + BaudRate::B9600, + BaudRate::B19200, + BaudRate::B38400, + BaudRate::B57600, + BaudRate::B115200, + BaudRate::B230400, + BaudRate::B460800, + BaudRate::B921600, + BaudRate::B3000000, + ] { + match with_timeout(Duration::from_secs(6), self.probe_baud(baudrate)).await { + Ok(Ok(_)) => { + if baudrate != C::BAUD_RATE { + // Attempt to store the desired baudrate, so we can shortcut + // this probing next time. Ignore any potential failures, as + // this is purely an optimization. + + // TODO: Is this correct? + // Some modules seem to persist baud rates by themselves. + // Nothing to do here for now. + } + found_baudrate = true; + break; + } + _ => {} + } + } + + if !found_baudrate { + // TODO: Attempt to do some better recovery here? + PwrCtrl::new(&self.ch, &mut self.config) + .power_down() + .await?; + + return Err(Error::BaudDetection); + } + + let mut cmd_buf = [0u8; 64]; + let mut at_client = SimpleClient::new( + &mut self.transport, + atat::AtDigester::::new(), + &mut cmd_buf, + C::AT_CONFIG, + ); + + // FIXME: + // // Tell module whether we support flow control + // let flow_control = if C::FLOW_CONTROL { + // FlowControl::RtsCts + // } else { + // FlowControl::Disabled + // }; + + // at_client + // .send_retry(&SetFlowControl { + // value: flow_control, + // }) + // .await?; + + let model_id = at_client.send_retry(&GetModelId).await?; + self.ch.set_module(Module::from_model_id(&model_id)); + + let FirmwareVersion { version } = at_client.send_retry(&GetFirmwareVersion).await?; + info!("Found module to be: {:?}, {:?}", self.ch.module(), version); + + at_client + .send_retry(&SetEmbeddedPortFiltering { + mode: C::EMBEDDED_PORT_FILTERING, + }) + .await?; + + // Echo off + at_client + .send_retry(&SetEcho { enabled: Echo::Off }) + .await?; + + // Extended errors on + at_client + .send_retry(&SetReportMobileTerminationError { + n: TerminationErrorMode::Enabled, + }) + .await?; + + #[cfg(feature = "internal-network-stack")] + if C::HEX_MODE { + at_client + .send_retry(&crate::command::ip_transport_layer::SetHexMode { + hex_mode_disable: crate::command::ip_transport_layer::types::HexMode::Enabled, + }) + .await?; + } else { + at_client + .send_retry(&crate::command::ip_transport_layer::SetHexMode { + hex_mode_disable: crate::command::ip_transport_layer::types::HexMode::Disabled, + }) + .await?; + } + + // DCD circuit (109) changes in accordance with the carrier + at_client + .send_retry(&SetCircuit109Behaviour { + value: Circuit109Behaviour::AlwaysPresent, + }) + .await?; + + // Ignore changes to DTR + at_client + .send_retry(&SetCircuit108Behaviour { + value: Circuit108Behaviour::Ignore, + }) + .await?; + + // Check sim status + let sim_status = async { + for _ in 0..2 { + if let Ok(PinStatus { + code: PinStatusCode::Ready, + }) = at_client.send(&GetPinStatus).await + { + debug!("SIM is ready"); + return Ok(()); + } + + Timer::after_secs(1).await; + } + + // There was an error initializing the SIM + // We've seen issues on uBlox-based devices, as a precation, we'll cycle + // the modem here through minimal/full functional state. + at_client + .send(&SetModuleFunctionality { + fun: self + .ch + .module() + .ok_or(Error::Uninitialized)? + .radio_off_cfun(), + rst: None, + }) + .await?; + at_client + .send(&SetModuleFunctionality { + fun: Functionality::Full, + rst: None, + }) + .await?; + + Err(Error::SimCard) + }; + + sim_status.await?; + + let ccid = at_client.send_retry(&GetCCID).await?; + info!("CCID: {}", ccid.ccid); + + at_client + .send_retry(&SetResultCodeSelection { + value: ResultCodeSelection::ConnectOnly, + }) + .await?; + + #[cfg(all( + feature = "ucged", + any( + feature = "sara-r410m", + feature = "sara-r412m", + feature = "sara-r422", + feature = "lara-r6" + ) + ))] + at_client + .send_retry(&SetChannelAndNetworkEnvDesc { + mode: if cfg!(feature = "ucged5") { 5 } else { 2 }, + }) + .await?; + + // Switch off UART power saving until it is integrated into this API + at_client + .send_retry(&SetPowerSavingControl { + mode: PowerSavingMode::Disabled, + timeout: None, + }) + .await?; + + if self.ch.desired_state(None) == OperationState::Initialized { + at_client + .send_retry(&SetModuleFunctionality { + fun: self + .ch + .module() + .ok_or(Error::Uninitialized)? + .radio_off_cfun(), + rst: None, + }) + .await?; + } + + Ok(()) + } + + pub async fn run(&mut self, stack: embassy_net::Stack<'_>) -> ! { + loop { + let _ = PwrCtrl::new(&self.ch, &mut self.config).power_down().await; + + // Wait for the desired state to change to anything but `PowerDown` + poll_fn(|cx| match self.ch.desired_state(Some(cx)) { + OperationState::PowerDown => Poll::Pending, + _ => Poll::Ready(()), + }) + .await; + + if self.init().await.is_err() { + continue; + } + + #[cfg(feature = "ppp")] + let ppp_fut = async { + self.ch + .wait_for_operation_state(OperationState::DataEstablished) + .await; + + Timer::after_secs(1).await; + + let mut fails = 0; + let mut last_start = None; + + loop { + if let Some(last_start) = last_start { + Timer::at(last_start + Duration::from_secs(10)).await; + // Do not attempt to start too fast. + + // If was up stably for at least 1 min, reset fail counter. + if Instant::now() > last_start + Duration::from_secs(60) { + fails = 0; + } else { + fails += 1; + if fails == 10 { + warn!("modem: PPP failed too much, rebooting modem."); + break; + } + } + } + last_start = Some(Instant::now()); + + { + // Must be large enough to hold 'ATD*99***1#\r\n' + let mut buf = [0u8; 16]; + + let mut at_client = SimpleClient::new( + &mut self.data_channel, + atat::AtDigester::::new(), + &mut buf, + C::AT_CONFIG, + ); + + // let _ = at_client.send(&DeactivatePDPContext).await; + + // Send AT command to enter PPP mode + let res = at_client.send(&EnterPPP { cid: C::CONTEXT_ID }).await; + + if let Err(e) = res { + warn!("ppp dial failed {:?}", e); + continue; + } + + Timer::after(Duration::from_millis(100)).await; + } + + // Check for CTS low (bit 2) + self.data_channel.set_hangup_detection(0x04, 0x00); + + info!("RUNNING PPP"); + let res = self + .ppp_runner + .as_mut() + .unwrap() + .run(&mut self.data_channel, C::PPP_CONFIG, |ipv4| { + debug!("Running on_ipv4_up for cellular!"); + + let Some(addr) = ipv4.address else { + warn!("PPP did not provide an IP address."); + return; + }; + let mut dns_servers = heapless::Vec::new(); + for s in ipv4.dns_servers.iter().flatten() { + let _ = dns_servers.push(*s); + } + let config = + embassy_net::ConfigV4::Static(embassy_net::StaticConfigV4 { + address: embassy_net::Ipv4Cidr::new(addr, 0), + gateway: None, + dns_servers, + }); + + stack.set_config_v4(config); + }) + .await; + + info!("ppp failed: {:?}", res); + + self.data_channel.clear_hangup_detection(); + + // escape back to data mode. + self.data_channel + .set_lines(embassy_at_cmux::Control::from_bits(0x44 << 1), None); + Timer::after(Duration::from_millis(100)).await; + self.data_channel + .set_lines(embassy_at_cmux::Control::from_bits(0x46 << 1), None); + + if self.ch.desired_state(None) != OperationState::DataEstablished { + break; + } + } + }; + + let mux_fut = async { + // Must be large enough to hold 'AT+CMUX=0,0,5,512,10,3,40,10,2\r\n' + let mut buf = [0u8; 32]; + { + let mut at_client = SimpleClient::new( + &mut self.transport, + atat::AtDigester::::new(), + &mut buf, + C::AT_CONFIG, + ); + + at_client + .send(&SetMultiplexing { + mode: 0, + subset: Some(0), + port_speed: Some(5), + n1: Some(CMUX_MAX_FRAME_SIZE as u16), + t1: None, //Some(10), + n2: None, //Some(3), + t2: None, //Some(30), + t3: None, //Some(10), + k: None, //Some(2), + }) + .await + .unwrap(); + } + + // The UART interface takes around 200 ms to reconfigure itself + // after the multiplexer configuration through the +CMUX AT + // command. + Timer::after_millis(200).await; + + // Drain the UART of any leftover AT stuff before setting up multiplexer + let _ = embassy_time::with_timeout(Duration::from_millis(100), async { + loop { + let _ = self.transport.read(&mut buf).await; + } + }) + .await; + + let (mut tx, mut rx) = self.transport.split_ref(); + + // Signal to the rest of the driver, that the MUX is operational and running + let fut = async { + // TODO: It should be possible to check on ChannelLines, + // when the MUX is opened successfully, instead of waiting a fixed time + Timer::after_secs(3).await; + self.ch.set_operation_state(OperationState::Initialized); + }; + + join( + self.mux_runner.run(&mut rx, &mut tx, CMUX_MAX_FRAME_SIZE), + fut, + ) + .await + }; + + let device_fut = async { + let (at_rx, at_tx, _) = &mut self.at_channel; + + let at_client = ProxyClient::new(self.req_slot.sender(), self.res_slot); + let mut cell_device = NetDevice::::new(&self.ch, &at_client); + + let mut urc_handler = UrcHandler::new(&self.ch, self.urc_channel); + + select3( + at_bridge((at_rx, at_tx), self.req_slot, &mut self.ingress), + urc_handler.run(), + cell_device.run(), + ) + .await + }; + + #[cfg(feature = "ppp")] + match select3(mux_fut, ppp_fut, device_fut).await { + Either3::First(_) => { + warn!("Breaking to reboot modem from multiplexer"); + } + Either3::Second(_) => { + warn!("Breaking to reboot modem from PPP"); + } + Either3::Third(_) => { + warn!("Breaking to reboot modem from network runner"); + } + } + + #[cfg(not(feature = "ppp"))] + match embassy_futures::select::select(mux_fut, device_fut).await { + embassy_futures::select::Either::First(_) => { + warn!("Breaking to reboot modem from multiplexer"); + } + embassy_futures::select::Either::Second(_) => { + warn!("Breaking to reboot modem from network runner"); + } + } + } + } +} diff --git a/src/asynch/state.rs b/src/asynch/state.rs new file mode 100644 index 0000000..20a82c9 --- /dev/null +++ b/src/asynch/state.rs @@ -0,0 +1,319 @@ +#![allow(dead_code)] + +use core::cell::RefCell; +use core::future::poll_fn; +use core::task::{Context, Poll}; + +use embassy_sync::blocking_mutex::raw::NoopRawMutex; +use embassy_sync::blocking_mutex::Mutex; +use embassy_sync::waitqueue::WakerRegistration; + +/// The link state of a network device. +#[derive(PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum LinkState { + /// The link is down. + Down, + /// The link is up. + Up, +} + +/// If the celular modem is up and responding to AT. +#[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum OperationState { + PowerDown = 0, + Initialized = 1, + Connected = 2, + DataEstablished = 3, +} + +use crate::modules::Module; +use crate::registration::{ProfileState, RegistrationState}; + +pub struct State { + shared: Mutex>, +} + +impl Default for State { + fn default() -> Self { + Self::new() + } +} + +impl State { + pub const fn new() -> Self { + Self { + shared: Mutex::new(RefCell::new(Shared { + link_state: LinkState::Down, + operation_state: OperationState::PowerDown, + module: None, + desired_state: OperationState::Initialized, + registration_state: RegistrationState::new(), + state_waker: WakerRegistration::new(), + registration_waker: WakerRegistration::new(), + })), + } + } +} + +/// State of the LinkState +pub struct Shared { + link_state: LinkState, + operation_state: OperationState, + desired_state: OperationState, + module: Option, + registration_state: RegistrationState, + state_waker: WakerRegistration, + registration_waker: WakerRegistration, +} + +#[derive(Clone)] +pub struct Runner<'d> { + pub(crate) shared: &'d Mutex>, +} + +impl<'d> Runner<'d> { + pub fn new(state: &'d mut State) -> Self { + Self { + shared: &state.shared, + } + } + + pub(crate) fn module(&self) -> Option { + self.shared.lock(|s| s.borrow().module) + } + + pub(crate) fn set_module(&self, module: Module) { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + s.module.replace(module); + }); + } + + pub fn update_registration_with(&self, f: impl FnOnce(&mut RegistrationState)) { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + let prev = s.registration_state.is_registered(); + f(&mut s.registration_state); + if prev != s.registration_state.is_registered() { + info!( + "Cellular registration status changed! Registered: {:?} -> {:?}", + prev, + s.registration_state.is_registered() + ); + } + s.registration_waker.wake(); + }) + } + + pub fn is_registered(&self, cx: Option<&mut Context>) -> bool { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + if let Some(cx) = cx { + s.registration_waker.register(cx.waker()); + } + s.registration_state.is_registered() + }) + } + + #[cfg(not(feature = "use-upsd-context-activation"))] + pub fn set_profile_state(&self, state: ProfileState) { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + s.registration_state.profile_state = state; + }) + } + + #[cfg(not(feature = "use-upsd-context-activation"))] + pub fn get_profile_state(&self) -> ProfileState { + self.shared + .lock(|s| s.borrow().registration_state.profile_state) + } + + pub fn set_link_state(&self, state: LinkState) { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + s.link_state = state; + s.state_waker.wake(); + }); + } + + pub fn link_state(&self, cx: Option<&mut Context>) -> LinkState { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + if let Some(cx) = cx { + s.state_waker.register(cx.waker()); + } + s.link_state + }) + } + + pub fn set_operation_state(&self, state: OperationState) { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + s.operation_state = state; + s.state_waker.wake(); + }); + } + + pub fn operation_state(&self, cx: Option<&mut Context>) -> OperationState { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + if let Some(cx) = cx { + s.state_waker.register(cx.waker()); + } + s.operation_state + }) + } + + pub fn set_desired_state(&self, ps: OperationState) { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + s.desired_state = ps; + s.state_waker.wake(); + }); + } + + pub fn desired_state(&self, cx: Option<&mut Context>) -> OperationState { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + if let Some(cx) = cx { + s.state_waker.register(cx.waker()); + } + s.desired_state + }) + } + + pub async fn wait_for_desired_state(&self, ps: OperationState) { + if self.desired_state(None) == ps { + return; + } + + poll_fn(|cx| { + if self.desired_state(Some(cx)) == ps { + return Poll::Ready(()); + } + Poll::Pending + }) + .await + } + + pub async fn wait_for_operation_state(&self, ps: OperationState) { + if self.operation_state(None) == ps { + return; + } + + poll_fn(|cx| { + if self.operation_state(Some(cx)) == ps { + return Poll::Ready(()); + } + Poll::Pending + }) + .await + } + + pub async fn wait_for_desired_state_change(&self) -> OperationState { + let old_desired = self.desired_state(None); + + poll_fn(|cx| { + let current_desired = self.desired_state(Some(cx)); + if current_desired != old_desired { + return Poll::Ready(current_desired); + } + Poll::Pending + }) + .await + } + + pub async fn wait_registration_change(&self) -> bool { + let old_state = self.is_registered(None); + + poll_fn(|cx| { + let current_state = self.is_registered(Some(cx)); + if current_state != old_state { + return Poll::Ready(current_state); + } + Poll::Pending + }) + .await + } +} + +#[cfg(feature = "internal-network-stack")] +pub struct Device<'d, const URC_CAPACITY: usize> { + pub(crate) shared: &'d Mutex>, + // pub(crate) at: AtHandle<'d, AT>, + pub(crate) urc_subscription: UrcSubscription<'d, Urc, URC_CAPACITY, 2>, +} + +#[cfg(feature = "internal-network-stack")] +impl<'d, const URC_CAPACITY: usize> Device<'d, URC_CAPACITY> { + pub fn link_state(&self, cx: &mut Context) -> LinkState { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + s.state_waker.register(cx.waker()); + s.link_state + }) + } + + pub fn operation_state(&self, cx: &mut Context) -> OperationState { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + s.state_waker.register(cx.waker()); + s.operation_state + }) + } + + pub fn link_state(&self) -> LinkState { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + s.link_state + }) + } + + pub fn operation_state(&self) -> OperationState { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + s.operation_state + }) + } + + pub fn desired_state(&self, cx: &mut Context) -> OperationState { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + s.state_waker.register(cx.waker()); + s.desired_state + }) + } + + pub fn set_desired_state(&self, ps: OperationState) { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + s.desired_state = ps; + s.state_waker.wake(); + }); + } + + pub async fn wait_for_desired_state(&self, ps: OperationState) { + poll_fn(|cx| { + if self.desired_state(cx) == ps { + return Poll::Ready(()); + } + Poll::Pending + }) + .await + } + + pub async fn wait_for_desired_state_change(&self) -> OperationState { + let current_desired = self.shared.lock(|s| s.borrow().desired_state); + + poll_fn(|cx| { + if self.desired_state(cx) != current_desired { + return Poll::Ready(ps); + } + Poll::Pending + }) + .await + } +} diff --git a/src/asynch/ublox_stack/dns.rs b/src/asynch/ublox_stack/dns.rs new file mode 100644 index 0000000..5b1be83 --- /dev/null +++ b/src/asynch/ublox_stack/dns.rs @@ -0,0 +1,148 @@ +#![cfg(feature = "dontbuild")] + +use core::{cell::RefCell, future::poll_fn, task::Poll}; + +use atat::asynch::AtatClient; +use core::net::IpAddr; +use embedded_nal_async::AddrType; + +use crate::asynch::ublox_stack::DnsState; + +use super::{DnsQuery, SocketStack, UbloxStack}; + +/// Errors returned by DnsSocket. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Error { + /// Invalid name + InvalidName, + /// Name too long + NameTooLong, + /// Name lookup failed + Failed, +} + +/// DNS client compatible with the `embedded-nal-async` traits. +/// +/// This exists only for compatibility with crates that use `embedded-nal-async`. +/// Prefer using [`Stack::dns_query`](crate::Stack::dns_query) directly if you're +/// not using `embedded-nal-async`. +pub struct DnsSocket<'a> { + stack: &'a RefCell, +} + +impl<'a> DnsSocket<'a> { + /// Create a new DNS socket using the provided stack. + pub fn new( + stack: &'a UbloxStack, + ) -> Self { + Self { + stack: &stack.socket, + } + } + + /// Make a query for a given name and return the corresponding IP addresses. + pub async fn query(&self, name: &str, addr_type: AddrType) -> Result { + match addr_type { + AddrType::IPv4 => { + if let Ok(ip) = name.parse().map(IpAddr::V4) { + return Ok(ip); + } + } + AddrType::IPv6 => { + if let Ok(ip) = name.parse().map(IpAddr::V6) { + return Ok(ip); + } + } + _ => {} + } + + { + let mut s = self.stack.borrow_mut(); + if s.dns_queries + .insert(heapless::String::try_from(name).unwrap(), DnsQuery::new()) + .is_err() + { + error!("Attempted to start more simultaneous DNS requests than the (4) supported"); + } + s.waker.wake(); + } + + #[must_use = "to delay the drop handler invocation to the end of the scope"] + struct OnDrop { + f: core::mem::MaybeUninit, + } + + impl OnDrop { + fn new(f: F) -> Self { + Self { + f: core::mem::MaybeUninit::new(f), + } + } + + fn defuse(self) { + core::mem::forget(self) + } + } + + impl Drop for OnDrop { + fn drop(&mut self) { + unsafe { self.f.as_ptr().read()() } + } + } + + let drop = OnDrop::new(|| { + let mut s = self.stack.borrow_mut(); + s.dns_queries + .remove(&heapless::String::try_from(name).unwrap()); + }); + + let res = poll_fn(|cx| { + let mut s = self.stack.borrow_mut(); + let query = s + .dns_queries + .get_mut(&heapless::String::try_from(name).unwrap()) + .unwrap(); + match query.state { + DnsState::Ok(ip) => { + s.dns_queries + .remove(&heapless::String::try_from(name).unwrap()); + return Poll::Ready(Ok(ip)); + } + DnsState::Err => { + s.dns_queries + .remove(&heapless::String::try_from(name).unwrap()); + return Poll::Ready(Err(Error::Failed)); + } + _ => { + query.waker.register(cx.waker()); + Poll::Pending + } + } + }) + .await; + + drop.defuse(); + + res + } +} + +impl<'a> embedded_nal_async::Dns for DnsSocket<'a> { + type Error = Error; + + async fn get_host_by_name( + &self, + host: &str, + addr_type: AddrType, + ) -> Result { + self.query(host, addr_type).await + } + + async fn get_host_by_address( + &self, + _addr: embedded_nal_async::IpAddr, + ) -> Result, Self::Error> { + unimplemented!() + } +} diff --git a/src/asynch/ublox_stack/mod.rs b/src/asynch/ublox_stack/mod.rs new file mode 100644 index 0000000..1fb401c --- /dev/null +++ b/src/asynch/ublox_stack/mod.rs @@ -0,0 +1,519 @@ +#![cfg(feature = "dontbuild")] + +#[cfg(feature = "socket-tcp")] +pub mod tcp; +// #[cfg(feature = "socket-udp")] +// pub mod udp; + +pub mod dns; + +use core::cell::RefCell; +use core::future::poll_fn; +use core::ops::{DerefMut, Rem}; +use core::task::Poll; + +use crate::asynch::state::Device; +use crate::command::data_mode::responses::ConnectPeerResponse; +use crate::command::data_mode::urc::PeerDisconnected; +use crate::command::data_mode::{ClosePeerConnection, ConnectPeer}; +use crate::command::edm::types::{DataEvent, Protocol, DATA_PACKAGE_SIZE}; +use crate::command::edm::urc::EdmEvent; +use crate::command::edm::EdmDataCommand; +use crate::command::ping::urc::{PingErrorResponse, PingResponse}; +use crate::command::ping::Ping; +use crate::command::Urc; +use crate::peer_builder::PeerUrlBuilder; + +use self::dns::DnsSocket; + +use super::state::{self, LinkState}; +use super::AtHandle; + +use atat::asynch::AtatClient; +use atomic_polyfill::{AtomicBool, AtomicU8, Ordering}; +use core::net::IpAddr; +use embassy_futures::select::{select4, Either4}; +use embassy_sync::waitqueue::WakerRegistration; +use embassy_time::{Duration, Ticker}; +use embedded_nal_async::SocketAddr; +use futures::pin_mut; +use ublox_sockets::{ + AnySocket, ChannelId, PeerHandle, Socket, SocketHandle, SocketSet, SocketStorage, +}; + +#[cfg(feature = "socket-tcp")] +use ublox_sockets::TcpState; + +const MAX_HOSTNAME_LEN: usize = 64; + +pub struct StackResources { + sockets: [SocketStorage<'static>; SOCK], +} + +impl StackResources { + pub fn new() -> Self { + Self { + sockets: [SocketStorage::EMPTY; SOCK], + } + } +} + +pub struct UbloxStack { + socket: RefCell, + device: RefCell>, + last_tx_socket: AtomicU8, + should_tx: AtomicBool, + link_up: AtomicBool, +} + +enum DnsState { + New, + Pending, + Ok(IpAddr), + Err, +} + +struct DnsQuery { + state: DnsState, + waker: WakerRegistration, +} + +impl DnsQuery { + pub fn new() -> Self { + Self { + state: DnsState::New, + waker: WakerRegistration::new(), + } + } +} + +struct SocketStack { + sockets: SocketSet<'static>, + waker: WakerRegistration, + dns_queries: heapless::FnvIndexMap, DnsQuery, 4>, + dropped_sockets: heapless::Vec, +} + +impl UbloxStack { + pub fn new( + device: state::Device<'static, AT, URC_CAPACITY>, + resources: &'static mut StackResources, + ) -> Self { + let sockets = SocketSet::new(&mut resources.sockets[..]); + + let socket = SocketStack { + sockets, + dns_queries: heapless::IndexMap::new(), + waker: WakerRegistration::new(), + dropped_sockets: heapless::Vec::new(), + }; + + Self { + socket: RefCell::new(socket), + device: RefCell::new(device), + last_tx_socket: AtomicU8::new(0), + link_up: AtomicBool::new(false), + should_tx: AtomicBool::new(false), + } + } + + pub async fn run(&self) -> ! { + loop { + // FIXME: It feels like this can be written smarter/simpler? + let should_tx = poll_fn(|cx| match self.should_tx.load(Ordering::Relaxed) { + true => { + self.should_tx.store(false, Ordering::Relaxed); + Poll::Ready(()) + } + false => { + self.should_tx.store(true, Ordering::Relaxed); + self.socket.borrow_mut().waker.register(cx.waker()); + Poll::<()>::Pending + } + }); + + let ticker = Ticker::every(Duration::from_millis(100)); + pin_mut!(ticker); + + let mut device = self.device.borrow_mut(); + let Device { + ref mut urc_subscription, + ref mut shared, + ref mut at, + } = device.deref_mut(); + + match select4( + urc_subscription.next_message_pure(), + should_tx, + ticker.next(), + poll_fn(|cx| { + match ( + self.link_up.load(Ordering::Relaxed), + device.link_state_poll_fn(cx), + ) { + (true, LinkState::Down) => Poll::Ready(LinkState::Down), + (false, LinkState::Up) => Poll::Ready(LinkState::Up), + _ => Poll::Pending, + } + }), + ) + .await + { + Either4::First(event) => { + Self::socket_rx(event, &self.socket); + } + Either4::Second(_) | Either4::Third(_) => { + if let Some(ev) = self.tx_event() { + Self::socket_tx(ev, &self.socket, at).await; + } + } + Either4::Fourth(new_state) => { + // Update link up + let old_link_up = self.link_up.load(Ordering::Relaxed); + let new_link_up = new_state == LinkState::Up; + self.link_up.store(new_link_up, Ordering::Relaxed); + + // Print when changed + if old_link_up != new_link_up { + info!("link_up = {:?}", new_link_up); + } + } + } + } + } + + /// Make a query for a given name and return the corresponding IP addresses. + // #[cfg(feature = "dns")] + pub async fn dns_query( + &self, + name: &str, + addr_type: embedded_nal_async::AddrType, + ) -> Result { + DnsSocket::new(self).query(name, addr_type).await + } + + fn socket_rx(event: EdmEvent, socket: &RefCell) { + match event { + EdmEvent::IPv4ConnectEvent(ev) => { + let endpoint = SocketAddr::new(ev.remote_ip.into(), ev.remote_port); + Self::connect_event(ev.channel_id, ev.protocol, endpoint, socket); + } + EdmEvent::IPv6ConnectEvent(ev) => { + let endpoint = SocketAddr::new(ev.remote_ip.into(), ev.remote_port); + Self::connect_event(ev.channel_id, ev.protocol, endpoint, socket); + } + EdmEvent::DisconnectEvent(channel_id) => { + let mut s = socket.borrow_mut(); + for (_handle, socket) in s.sockets.iter_mut() { + match socket { + #[cfg(feature = "socket-udp")] + Socket::Udp(udp) if udp.edm_channel == Some(channel_id) => { + udp.edm_channel = None; + break; + } + #[cfg(feature = "socket-tcp")] + Socket::Tcp(tcp) if tcp.edm_channel == Some(channel_id) => { + tcp.edm_channel = None; + break; + } + _ => {} + } + } + } + EdmEvent::DataEvent(DataEvent { channel_id, data }) => { + let mut s = socket.borrow_mut(); + for (_handle, socket) in s.sockets.iter_mut() { + match socket { + #[cfg(feature = "socket-udp")] + Socket::Udp(udp) + if udp.edm_channel == Some(channel_id) && udp.may_recv() => + { + let n = udp.rx_enqueue_slice(&data); + if n < data.len() { + error!( + "[{}] UDP RX data overflow! Discarding {} bytes", + udp.peer_handle, + data.len() - n + ); + } + break; + } + #[cfg(feature = "socket-tcp")] + Socket::Tcp(tcp) + if tcp.edm_channel == Some(channel_id) && tcp.may_recv() => + { + let n = tcp.rx_enqueue_slice(&data); + if n < data.len() { + error!( + "[{}] TCP RX data overflow! Discarding {} bytes", + tcp.peer_handle, + data.len() - n + ); + } + break; + } + _ => {} + } + } + } + EdmEvent::ATEvent(Urc::PeerDisconnected(PeerDisconnected { handle })) => { + let mut s = socket.borrow_mut(); + for (_handle, socket) in s.sockets.iter_mut() { + match socket { + #[cfg(feature = "socket-udp")] + Socket::Udp(udp) if udp.peer_handle == Some(handle) => { + udp.peer_handle = None; + udp.set_state(UdpState::TimeWait); + break; + } + #[cfg(feature = "socket-tcp")] + Socket::Tcp(tcp) if tcp.peer_handle == Some(handle) => { + tcp.peer_handle = None; + tcp.set_state(TcpState::TimeWait); + break; + } + _ => {} + } + } + } + EdmEvent::ATEvent(Urc::PingResponse(PingResponse { + ip, hostname, rtt, .. + })) => { + let mut s = socket.borrow_mut(); + if let Some(query) = s.dns_queries.get_mut(&hostname) { + match query.state { + DnsState::Pending if rtt == -1 => { + // According to AT manual, rtt = -1 means the PING has timed out + query.state = DnsState::Err; + query.waker.wake(); + } + DnsState::Pending => { + query.state = DnsState::Ok(ip); + query.waker.wake(); + } + _ => {} + } + } + } + EdmEvent::ATEvent(Urc::PingErrorResponse(PingErrorResponse { error: _ })) => { + let mut s = socket.borrow_mut(); + for (_, query) in s.dns_queries.iter_mut() { + match query.state { + DnsState::Pending => { + query.state = DnsState::Err; + query.waker.wake(); + } + _ => {} + } + } + } + _ => {} + } + } + + fn tx_event(&self) -> Option { + let mut s = self.socket.borrow_mut(); + for (hostname, query) in s.dns_queries.iter_mut() { + if let DnsState::New = query.state { + query.state = DnsState::Pending; + return Some(TxEvent::Dns { + hostname: hostname.clone(), + }); + } + } + + // Handle delayed close-by-drop here + if let Some(dropped_peer_handle) = s.dropped_sockets.pop() { + warn!("Handling dropped socket {}", dropped_peer_handle); + return Some(TxEvent::Close { + peer_handle: dropped_peer_handle, + }); + } + + // Make sure to give all sockets an even opportunity to TX + let skip = self + .last_tx_socket + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |v| { + let next = v + 1; + Some(next.rem(s.sockets.sockets.len() as u8)) + }) + .unwrap(); + + for (handle, socket) in s.sockets.iter_mut().skip(skip as usize) { + match socket { + #[cfg(feature = "socket-udp")] + Socket::Udp(udp) => todo!(), + #[cfg(feature = "socket-tcp")] + Socket::Tcp(tcp) => { + tcp.poll(); + + match tcp.state() { + TcpState::Closed => { + if let Some(addr) = tcp.remote_endpoint() { + let url = PeerUrlBuilder::new() + .address(&addr) + .set_local_port(tcp.local_port) + .tcp::<128>() + .unwrap(); + + return Some(TxEvent::Connect { + socket_handle: handle, + url, + }); + } + } + // We transmit data in all states where we may have data in the buffer, + // or the transmit half of the connection is still open. + TcpState::Established | TcpState::CloseWait | TcpState::LastAck => { + if let Some(edm_channel) = tcp.edm_channel { + warn!("{}", tcp); + return tcp.tx_dequeue(|payload| { + let len = core::cmp::min(payload.len(), DATA_PACKAGE_SIZE); + let res = if len != 0 { + Some(TxEvent::Send { + edm_channel, + data: heapless::Vec::from_slice(payload).unwrap(), + }) + } else { + None + }; + + (len, res) + }); + } + } + TcpState::FinWait1 => { + return Some(TxEvent::Close { + peer_handle: tcp.peer_handle.unwrap(), + }); + } + TcpState::Listen => todo!(), + TcpState::SynReceived => todo!(), + _ => {} + }; + } + _ => {} + }; + } + + None + } + + async fn socket_tx(ev: TxEvent, socket: &RefCell, at: &mut AtHandle<'_, AT>) { + match ev { + TxEvent::Connect { socket_handle, url } => { + match at.send(ConnectPeer { url: &url }).await { + Ok(ConnectPeerResponse { peer_handle }) => { + let mut s = socket.borrow_mut(); + let tcp = s + .sockets + .get_mut::(socket_handle); + tcp.peer_handle = Some(peer_handle); + tcp.set_state(TcpState::SynSent); + } + Err(e) => { + error!("Failed to connect?! {}", e) + } + } + } + TxEvent::Send { edm_channel, data } => { + warn!("Sending {} bytes on {}", data.len(), edm_channel); + at.send(EdmDataCommand { + channel: edm_channel, + data: &data, + }) + .await + .ok(); + } + TxEvent::Close { peer_handle } => { + at.send(ClosePeerConnection { peer_handle }).await.ok(); + } + TxEvent::Dns { hostname } => { + match at + .send(Ping { + hostname: &hostname, + retry_num: 1, + }) + .await + { + Ok(_) => {} + Err(_) => { + let mut s = socket.borrow_mut(); + if let Some(query) = s.dns_queries.get_mut(&hostname) { + match query.state { + DnsState::Pending => { + query.state = DnsState::Err; + query.waker.wake(); + } + _ => {} + } + } + } + } + } + } + } + + fn connect_event( + channel_id: ChannelId, + protocol: Protocol, + endpoint: SocketAddr, + socket: &RefCell, + ) { + let mut s = socket.borrow_mut(); + for (_handle, socket) in s.sockets.iter_mut() { + match protocol { + #[cfg(feature = "socket-tcp")] + Protocol::TCP => match ublox_sockets::tcp::Socket::downcast_mut(socket) { + Some(tcp) if tcp.remote_endpoint == Some(endpoint) => { + tcp.edm_channel = Some(channel_id); + tcp.set_state(TcpState::Established); + break; + } + _ => {} + }, + #[cfg(feature = "socket-udp")] + Protocol::UDP => match ublox_sockets::udp::Socket::downcast_mut(socket) { + Some(udp) if udp.remote_endpoint == Some(endpoint) => { + udp.edm_channel = Some(channel_id); + udp.set_state(ublox_sockets::UdpState::Established); + break; + } + _ => {} + }, + _ => {} + } + } + } +} + +// TODO: This extra data clone step can probably be avoided by adding a +// waker/context based API to ATAT. +enum TxEvent { + Connect { + socket_handle: SocketHandle, + url: heapless::String<128>, + }, + Send { + edm_channel: ChannelId, + data: heapless::Vec, + }, + Close { + peer_handle: PeerHandle, + }, + Dns { + hostname: heapless::String, + }, +} + +#[cfg(feature = "defmt")] +impl defmt::Format for TxEvent { + fn format(&self, fmt: defmt::Formatter) { + match self { + TxEvent::Connect { .. } => defmt::write!(fmt, "TxEvent::Connect"), + TxEvent::Send { .. } => defmt::write!(fmt, "TxEvent::Send"), + TxEvent::Close { .. } => defmt::write!(fmt, "TxEvent::Close"), + TxEvent::Dns { .. } => defmt::write!(fmt, "TxEvent::Dns"), + } + } +} diff --git a/src/asynch/ublox_stack/tcp.rs b/src/asynch/ublox_stack/tcp.rs new file mode 100644 index 0000000..b01fff1 --- /dev/null +++ b/src/asynch/ublox_stack/tcp.rs @@ -0,0 +1,540 @@ +#![cfg(feature = "dontbuild")] + +use core::cell::RefCell; +use core::future::poll_fn; +use core::mem; +use core::task::Poll; + +use atat::asynch::AtatClient; +use embedded_nal_async::SocketAddr; +use ublox_sockets::{tcp, SocketHandle, TcpState}; + +use super::{SocketStack, UbloxStack}; + +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Error { + ConnectionReset, +} + +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ConnectError { + /// The socket is already connected or listening. + InvalidState, + /// The remote host rejected the connection with a RST packet. + ConnectionReset, + /// Connect timed out. + TimedOut, + /// No route to host. + NoRoute, +} + +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum AcceptError { + /// The socket is already connected or listening. + InvalidState, + /// Invalid listen port + InvalidPort, + /// The remote host rejected the connection with a RST packet. + ConnectionReset, +} + +pub struct TcpSocket<'a> { + io: TcpIo<'a>, +} + +pub struct TcpReader<'a> { + io: TcpIo<'a>, +} + +pub struct TcpWriter<'a> { + io: TcpIo<'a>, +} + +impl<'a> TcpReader<'a> { + pub async fn read(&mut self, buf: &mut [u8]) -> Result { + self.io.read(buf).await + } +} + +impl<'a> TcpWriter<'a> { + pub async fn write(&mut self, buf: &[u8]) -> Result { + self.io.write(buf).await + } + + pub async fn flush(&mut self) -> Result<(), Error> { + self.io.flush().await + } +} + +impl<'a> TcpSocket<'a> { + pub fn new( + stack: &'a UbloxStack, + rx_buffer: &'a mut [u8], + tx_buffer: &'a mut [u8], + ) -> Self { + let s = &mut *stack.socket.borrow_mut(); + let rx_buffer: &'static mut [u8] = unsafe { mem::transmute(rx_buffer) }; + let tx_buffer: &'static mut [u8] = unsafe { mem::transmute(tx_buffer) }; + let handle = s.sockets.add(tcp::Socket::new( + tcp::SocketBuffer::new(rx_buffer), + tcp::SocketBuffer::new(tx_buffer), + )); + + Self { + io: TcpIo { + stack: &stack.socket, + handle, + }, + } + } + + pub fn split(&mut self) -> (TcpReader<'_>, TcpWriter<'_>) { + (TcpReader { io: self.io }, TcpWriter { io: self.io }) + } + + pub async fn connect(&mut self, remote_endpoint: T) -> Result<(), ConnectError> + where + T: Into, + { + match { self.io.with_mut(|s| s.connect(remote_endpoint, None)) } { + Ok(()) => {} + Err(_) => return Err(ConnectError::InvalidState), + // Err(tcp::ConnectError::Unaddressable) => return Err(ConnectError::NoRoute), + } + + poll_fn(|cx| { + self.io.with_mut(|s| match s.state() { + tcp::State::TimeWait => Poll::Ready(Err(ConnectError::ConnectionReset)), + tcp::State::Listen => unreachable!(), + tcp::State::Closed | tcp::State::SynSent | tcp::State::SynReceived => { + s.register_send_waker(cx.waker()); + Poll::Pending + } + _ => Poll::Ready(Ok(())), + }) + }) + .await + } + + // FIXME: + // pub async fn accept(&mut self, local_endpoint: T) -> Result<(), AcceptError> + // where + // T: Into, + // { + // match self.io.with_mut(|s, _| s.listen(local_endpoint)) { + // Ok(()) => {} + // Err(tcp::ListenError::InvalidState) => return Err(AcceptError::InvalidState), + // Err(tcp::ListenError::Unaddressable) => return Err(AcceptError::InvalidPort), + // } + + // poll_fn(|cx| { + // self.io.with_mut(|s, _| match s.state() { + // tcp::State::Listen | tcp::State::SynSent | tcp::State::SynReceived => { + // s.register_send_waker(cx.waker()); + // Poll::Pending + // } + // _ => Poll::Ready(Ok(())), + // }) + // }) + // .await + // } + + pub async fn read(&mut self, buf: &mut [u8]) -> Result { + self.io.read(buf).await + } + + pub async fn write(&mut self, buf: &[u8]) -> Result { + self.io.write(buf).await + } + + pub async fn flush(&mut self) -> Result<(), Error> { + self.io.flush().await + } + + // pub fn set_timeout(&mut self, duration: Option) { + // self.io.with_mut(|s| s.set_timeout(duration)) + // } + + // pub fn set_keep_alive(&mut self, interval: Option) { + // self.io.with_mut(|s| s.set_keep_alive(interval)) + // } + + // pub fn local_endpoint(&self) -> Option { + // self.io.with(|s, _| s.local_endpoint()) + // } + + pub fn remote_endpoint(&self) -> Option { + self.io.with(|s| s.remote_endpoint()) + } + + pub fn state(&self) -> tcp::State { + self.io.with(|s| s.state()) + } + + pub fn close(&mut self) { + self.io.with_mut(|s| s.close()) + } + + pub fn abort(&mut self) { + self.io.with_mut(|s| s.abort()) + } + + pub fn may_send(&self) -> bool { + self.io.with(|s| s.may_send()) + } + + pub fn may_recv(&self) -> bool { + self.io.with(|s| s.may_recv()) + } +} + +impl<'a> Drop for TcpSocket<'a> { + fn drop(&mut self) { + if matches!(self.state(), TcpState::Listen | TcpState::Established) { + if let Some(peer_handle) = self.io.with(|s| s.peer_handle) { + self.io + .stack + .borrow_mut() + .dropped_sockets + .push(peer_handle) + .ok(); + } + } + let mut stack = self.io.stack.borrow_mut(); + stack.sockets.remove(self.io.handle); + stack.waker.wake(); + } +} + +// ======================= + +#[derive(Copy, Clone)] +struct TcpIo<'a> { + stack: &'a RefCell, + handle: SocketHandle, +} + +impl<'d> TcpIo<'d> { + fn with(&self, f: impl FnOnce(&tcp::Socket) -> R) -> R { + let s = &*self.stack.borrow(); + let socket = s.sockets.get::(self.handle); + f(socket) + } + + fn with_mut(&mut self, f: impl FnOnce(&mut tcp::Socket) -> R) -> R { + let s = &mut *self.stack.borrow_mut(); + let socket = s.sockets.get_mut::(self.handle); + let res = f(socket); + s.waker.wake(); + res + } + + async fn read(&mut self, buf: &mut [u8]) -> Result { + poll_fn(move |cx| { + // CAUTION: smoltcp semantics around EOF are different to what you'd expect + // from posix-like IO, so we have to tweak things here. + self.with_mut(|s| match s.recv_slice(buf) { + // No data ready + Ok(0) => { + s.register_recv_waker(cx.waker()); + Poll::Pending + } + // Data ready! + Ok(n) => Poll::Ready(Ok(n)), + // EOF + Err(_) => Poll::Ready(Ok(0)), + // FIXME: + // Err(tcp::RecvError::Finished) => Poll::Ready(Ok(0)), + // Connection reset. TODO: this can also be timeouts etc, investigate. + // Err(tcp::RecvError::InvalidState) => Poll::Ready(Err(Error::ConnectionReset)), + }) + }) + .await + } + + async fn write(&mut self, buf: &[u8]) -> Result { + poll_fn(move |cx| { + self.with_mut(|s| match s.send_slice(buf) { + // Not ready to send (no space in the tx buffer) + Ok(0) => { + s.register_send_waker(cx.waker()); + Poll::Pending + } + // Some data sent + Ok(n) => Poll::Ready(Ok(n)), + // Connection reset. TODO: this can also be timeouts etc, investigate. + Err(_) => Poll::Ready(Err(Error::ConnectionReset)), + // FIXME: + // Err(tcp::SendError::InvalidState) => Poll::Ready(Err(Error::ConnectionReset)), + }) + }) + .await + } + + async fn flush(&mut self) -> Result<(), Error> { + poll_fn(move |cx| { + self.with_mut(|s| { + // If there are outstanding send operations, register for wake up and wait + // smoltcp issues wake-ups when octets are dequeued from the send buffer + if s.send_queue() > 0 { + s.register_send_waker(cx.waker()); + Poll::Pending + // No outstanding sends, socket is flushed + } else { + Poll::Ready(Ok(())) + } + }) + }) + .await + } +} + +mod embedded_io_impls { + use super::*; + + impl embedded_io_async::Error for ConnectError { + fn kind(&self) -> embedded_io_async::ErrorKind { + embedded_io_async::ErrorKind::Other + } + } + + impl embedded_io_async::Error for Error { + fn kind(&self) -> embedded_io_async::ErrorKind { + embedded_io_async::ErrorKind::Other + } + } + + impl<'d> embedded_io_async::ErrorType for TcpSocket<'d> { + type Error = Error; + } + + impl<'d> embedded_io_async::Read for TcpSocket<'d> { + async fn read(&mut self, buf: &mut [u8]) -> Result { + self.io.read(buf).await + } + } + + impl<'d> embedded_io_async::Write for TcpSocket<'d> { + async fn write(&mut self, buf: &[u8]) -> Result { + self.io.write(buf).await + } + + async fn flush(&mut self) -> Result<(), Self::Error> { + self.io.flush().await + } + } + + impl<'d> embedded_io_async::ErrorType for TcpReader<'d> { + type Error = Error; + } + + impl<'d> embedded_io_async::Read for TcpReader<'d> { + async fn read(&mut self, buf: &mut [u8]) -> Result { + self.io.read(buf).await + } + } + + impl<'d> embedded_io_async::ErrorType for TcpWriter<'d> { + type Error = Error; + } + + impl<'d> embedded_io_async::Write for TcpWriter<'d> { + async fn write(&mut self, buf: &[u8]) -> Result { + self.io.write(buf).await + } + + async fn flush(&mut self) -> Result<(), Self::Error> { + self.io.flush().await + } + } +} + +// #[cfg(all(feature = "unstable-traits", feature = "nightly"))] +pub mod client { + use core::cell::UnsafeCell; + use core::mem::MaybeUninit; + use core::ptr::NonNull; + + use atomic_polyfill::{AtomicBool, Ordering}; + + use super::*; + + /// TCP client capable of creating up to N multiple connections with tx and rx buffers according to TX_SZ and RX_SZ. + pub struct TcpClient< + 'd, + AT: AtatClient + 'static, + const N: usize, + const URC_CAPACITY: usize, + const TX_SZ: usize = 1024, + const RX_SZ: usize = 1024, + > { + pub(crate) stack: &'d UbloxStack, + pub(crate) state: &'d TcpClientState, + } + + impl< + 'd, + AT: AtatClient, + const N: usize, + const URC_CAPACITY: usize, + const TX_SZ: usize, + const RX_SZ: usize, + > TcpClient<'d, AT, N, URC_CAPACITY, TX_SZ, RX_SZ> + { + /// Create a new TcpClient + pub fn new( + stack: &'d UbloxStack, + state: &'d TcpClientState, + ) -> Self { + Self { stack, state } + } + } + + impl< + 'd, + AT: AtatClient, + const N: usize, + const URC_CAPACITY: usize, + const TX_SZ: usize, + const RX_SZ: usize, + > embedded_nal_async::TcpConnect for TcpClient<'d, AT, N, URC_CAPACITY, TX_SZ, RX_SZ> + { + type Error = Error; + type Connection<'m> = TcpConnection<'m, N, TX_SZ, RX_SZ> where Self: 'm; + + async fn connect<'a>( + &'a self, + remote: embedded_nal_async::SocketAddr, + ) -> Result, Self::Error> + where + Self: 'a, + { + let remote_endpoint = (remote.ip(), remote.port()); + let mut socket = TcpConnection::new(&self.stack, self.state)?; + socket + .socket + .connect(remote_endpoint) + .await + .map_err(|_| Error::ConnectionReset)?; + Ok(socket) + } + } + + pub struct TcpConnection<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> { + socket: TcpSocket<'d>, + state: &'d TcpClientState, + bufs: NonNull<([u8; TX_SZ], [u8; RX_SZ])>, + } + + impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> + TcpConnection<'d, N, TX_SZ, RX_SZ> + { + fn new( + stack: &'d UbloxStack, + state: &'d TcpClientState, + ) -> Result { + let mut bufs = state.pool.alloc().ok_or(Error::ConnectionReset)?; + Ok(Self { + socket: unsafe { + TcpSocket::new(stack, &mut bufs.as_mut().1, &mut bufs.as_mut().0) + }, + state, + bufs, + }) + } + } + + impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> Drop + for TcpConnection<'d, N, TX_SZ, RX_SZ> + { + fn drop(&mut self) { + unsafe { + self.socket.close(); + self.state.pool.free(self.bufs); + } + } + } + + impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_io_async::ErrorType + for TcpConnection<'d, N, TX_SZ, RX_SZ> + { + type Error = Error; + } + + impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_io_async::Read + for TcpConnection<'d, N, TX_SZ, RX_SZ> + { + async fn read(&mut self, buf: &mut [u8]) -> Result { + self.socket.read(buf).await + } + } + + impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_io_async::Write + for TcpConnection<'d, N, TX_SZ, RX_SZ> + { + async fn write(&mut self, buf: &[u8]) -> Result { + self.socket.write(buf).await + } + + async fn flush(&mut self) -> Result<(), Self::Error> { + self.socket.flush().await + } + } + + /// State for TcpClient + pub struct TcpClientState { + pool: Pool<([u8; TX_SZ], [u8; RX_SZ]), N>, + } + + impl TcpClientState { + pub const fn new() -> Self { + Self { pool: Pool::new() } + } + } + + unsafe impl Sync + for TcpClientState + { + } + + struct Pool { + used: [AtomicBool; N], + data: [UnsafeCell>; N], + } + + impl Pool { + const VALUE: AtomicBool = AtomicBool::new(false); + const UNINIT: UnsafeCell> = UnsafeCell::new(MaybeUninit::uninit()); + + const fn new() -> Self { + Self { + used: [Self::VALUE; N], + data: [Self::UNINIT; N], + } + } + } + + impl Pool { + fn alloc(&self) -> Option> { + for n in 0..N { + if self.used[n].swap(true, Ordering::SeqCst) == false { + let p = self.data[n].get() as *mut T; + return Some(unsafe { NonNull::new_unchecked(p) }); + } + } + None + } + + /// safety: p must be a pointer obtained from self.alloc that hasn't been freed yet. + unsafe fn free(&self, p: NonNull) { + let origin = self.data.as_ptr() as *mut T; + let n = p.as_ptr().offset_from(origin); + assert!(n >= 0); + assert!((n as usize) < N); + self.used[n as usize].store(false, Ordering::SeqCst); + } + } +} diff --git a/src/asynch/urc_handler.rs b/src/asynch/urc_handler.rs new file mode 100644 index 0000000..3a271d6 --- /dev/null +++ b/src/asynch/urc_handler.rs @@ -0,0 +1,73 @@ +use atat::{UrcChannel, UrcSubscription}; + +use crate::command::Urc; + +use super::{runner::URC_SUBSCRIBERS, state}; + +pub struct UrcHandler<'a, 'b, const URC_CAPACITY: usize> { + ch: &'b state::Runner<'a>, + urc_subscription: UrcSubscription<'a, Urc, URC_CAPACITY, URC_SUBSCRIBERS>, +} + +impl<'a, 'b, const URC_CAPACITY: usize> UrcHandler<'a, 'b, URC_CAPACITY> { + pub fn new( + ch: &'b state::Runner<'a>, + urc_channel: &'a UrcChannel, + ) -> Self { + Self { + ch, + urc_subscription: urc_channel.subscribe().unwrap(), + } + } + + pub async fn run(&mut self) -> ! { + loop { + let event = self.urc_subscription.next_message_pure().await; + self.handle_urc(event).await; + } + } + + async fn handle_urc(&mut self, event: Urc) { + match event { + // Handle network URCs + Urc::NetworkDetach => warn!("Network detached"), + Urc::MobileStationDetach => warn!("Mobile station detached"), + Urc::NetworkDeactivate => warn!("Network deactivated"), + Urc::MobileStationDeactivate => warn!("Mobile station deactivated"), + Urc::NetworkPDNDeactivate => warn!("Network PDN deactivated"), + Urc::MobileStationPDNDeactivate => warn!("Mobile station PDN deactivated"), + #[cfg(feature = "internal-network-stack")] + Urc::SocketDataAvailable(_) => warn!("Socket data available"), + #[cfg(feature = "internal-network-stack")] + Urc::SocketDataAvailableUDP(_) => warn!("Socket data available UDP"), + Urc::DataConnectionActivated(_) => warn!("Data connection activated"), + Urc::DataConnectionDeactivated(_) => { + warn!("Data connection deactivated"); + #[cfg(not(feature = "use-upsd-context-activation"))] + if self.ch.get_profile_state() == crate::registration::ProfileState::ShouldBeUp { + // Set the state so that, should we re-register with the + // network, we will reactivate the internal profile + self.ch + .set_profile_state(crate::registration::ProfileState::RequiresReactivation); + } + } + #[cfg(feature = "internal-network-stack")] + Urc::SocketClosed(_) => warn!("Socket closed"), + Urc::MessageWaitingIndication(_) => warn!("Message waiting indication"), + Urc::ExtendedPSNetworkRegistration(_) => warn!("Extended PS network registration"), + Urc::HttpResponse(_) => warn!("HTTP response"), + Urc::NetworkRegistration(reg) => { + self.ch + .update_registration_with(|state| state.compare_and_set(reg.into())); + } + Urc::GPRSNetworkRegistration(reg) => { + self.ch + .update_registration_with(|state| state.compare_and_set(reg.into())); + } + Urc::EPSNetworkRegistration(reg) => { + self.ch + .update_registration_with(|state| state.compare_and_set(reg.into())); + } + }; + } +} diff --git a/src/command/call_control/mod.rs b/src/command/call_control/mod.rs new file mode 100644 index 0000000..6e8e038 --- /dev/null +++ b/src/command/call_control/mod.rs @@ -0,0 +1,57 @@ +//! ### 6 - Call control +mod types; + +use atat::atat_derive::AtatCmd; + +use self::types::AddressType; + +use super::NoResponse; + +/// 6.1 Select type of address +CSTA +/// +/// Selects the type of number for further dialling commands (D) according to +/// 3GPP specifications. +/// +/// **NOTES:** +/// - The type of address is automatically detected from the dialling string +/// thus the +CSTA command has no effect. +#[derive(Clone, AtatCmd)] +#[at_cmd("+CSTA", NoResponse)] +pub struct SetAddressType { + #[at_arg(position = 0)] + pub typ: AddressType, +} + +/// 6.2 Dial command D +/// +/// Lists characters that may be used in a dialling string for making a call +/// (voice, data or fax call) or controlling supplementary services in +/// accordance with 3GPP TS 22.030 [77] and initiates the indicated kind of +/// call. No further commands may follow in the command line in case of data or +/// fax calls. +/// +/// **NOTES:** +/// - **LARA-L6 / LARA-R6**: Supplementary services strings are not supported in +/// the dial command. Set the DTR line to ON state before making a data call +/// - **LARA-L6004D / LARA-R6001D / LARA-R6401D**: Voice calls are not +/// supported. +#[derive(Clone, AtatCmd)] +#[at_cmd( + "D", + NoResponse, + abortable = true, + timeout_ms = 180000, + value_sep = false +)] +pub struct Dial<'a> { + /// Dial string; the allowed characters are: 1 2 3 4 5 6 7 8 9 0 * # + A B C + /// D , T P ! W @ (see the 3GPP TS 27.007 [75]). The following characters + /// are ignored: , T ! W @. + /// + /// **NOTE**: The first occurrence of P is interpreted as pause and + /// separator between the dialling number and the DTMF string. The following + /// occurrences are interpreted only as pause. The use of P as pause has + /// been introduced for AT&T certification. + #[at_arg(position = 0, len = 32)] + pub number: &'a str, +} diff --git a/src/command/call_control/types.rs b/src/command/call_control/types.rs new file mode 100644 index 0000000..26ff082 --- /dev/null +++ b/src/command/call_control/types.rs @@ -0,0 +1,14 @@ +//! Argument and parameter types used by Call Control Commands and Responses + +use atat::atat_derive::AtatEnum; + +/// Type of address in integer format +#[derive(Clone, Default, PartialEq, Eq, AtatEnum)] +pub enum AddressType { + /// 145: dialing string includes international access code character '+' + IncludeNationalAccessCode = 145, + + /// 129 (default value): national coded dialing string + #[default] + NationalCodedString = 129, +} diff --git a/ublox-cellular/src/command/control/mod.rs b/src/command/control/mod.rs similarity index 72% rename from ublox-cellular/src/command/control/mod.rs rename to src/command/control/mod.rs index 6d75312..24d2a5a 100644 --- a/ublox-cellular/src/command/control/mod.rs +++ b/src/command/control/mod.rs @@ -2,13 +2,16 @@ //! These commands, unless specifically stated, do not implement set syntax using "=", read ("?"), or test ("=?"). //! If such commands are used, the "+CME ERROR: unknown" or "+CME ERROR: 100" error result code is provided //! (depending on the +CMEE AT command setting). -// pub mod responses; +pub mod responses; pub mod types; -use atat::atat_derive::AtatCmd; -use types::{BaudRate, Circuit108Behaviour, Circuit109Behaviour, FlowControl, SoftwareFlowControl}; - use super::NoResponse; +use atat::atat_derive::AtatCmd; +use responses::DataRate; +use types::{ + BaudRate, Circuit108Behaviour, Circuit109Behaviour, Echo, FlowControl, ResultCodeSelection, + SoftwareFlowControl, +}; /// 15.2 Circuit 109 behavior &C /// @@ -81,6 +84,14 @@ pub struct SetDataRate { pub rate: BaudRate, } +/// 15.9 UART data rate configuration +IPR +/// +/// Specifies the data rate at which the DCE accepts commands on the UART +/// interface. The full range of data rates depends on HW or other criteria. +#[derive(Clone, AtatCmd)] +#[at_cmd("+IPR?", DataRate)] +pub struct GetDataRate; + /// 15.25 Set to factory defined configuration &F /// /// Resets the current profile to factory-programmed setting. Other NVM @@ -92,3 +103,29 @@ pub struct SetDataRate { #[derive(Clone, AtatCmd)] #[at_cmd("&F", NoResponse)] pub struct FactoryResetConfig; + +/// 15.25 Set to factory defined configuration &F +/// +/// Resets the current profile to factory-programmed setting. Other NVM +/// settings, not included in the profiles, are not affected. In case of +/// success, the response is issued using the configuration of the result codes +/// format (Q, V, S3, S4 AT commands) loaded from the factory-programmed +/// profile. The other DCE settings are applied after the response has been +/// sent. +#[derive(Clone, AtatCmd)] +#[at_cmd("E", NoResponse, value_sep = false)] +pub struct SetEcho { + #[at_arg(position = 0)] + pub enabled: Echo, +} + +/// 14.21 Result code selection and call progress monitoring control X +/// +/// In a CS data call, determines how the DCE transmits to the DTE the CONNECT +/// result code. +#[derive(Clone, AtatCmd)] +#[at_cmd("X", NoResponse, value_sep = false)] +pub struct SetResultCodeSelection { + #[at_arg(position = 0)] + pub value: ResultCodeSelection, +} diff --git a/src/command/control/responses.rs b/src/command/control/responses.rs new file mode 100644 index 0000000..0c16269 --- /dev/null +++ b/src/command/control/responses.rs @@ -0,0 +1,10 @@ +//! Responses for Control Commands +use super::types::BaudRate; +use atat::atat_derive::AtatResp; + +#[derive(Clone, Debug, PartialEq, Eq, AtatResp)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct DataRate { + #[at_arg(position = 0)] + pub rate: BaudRate, +} diff --git a/ublox-cellular/src/command/control/types.rs b/src/command/control/types.rs similarity index 85% rename from ublox-cellular/src/command/control/types.rs rename to src/command/control/types.rs index a035b83..7c64b83 100644 --- a/ublox-cellular/src/command/control/types.rs +++ b/src/command/control/types.rs @@ -13,6 +13,22 @@ pub enum Circuit109Behaviour { ChangesWithCarrier = 1, } +#[derive(Clone, PartialEq, Eq, AtatEnum)] +pub enum Echo { + /// 0: Echo off + Off = 0, + /// 1 (default value and factory-programmed value): Echo on + On = 1, +} + +#[derive(Clone, PartialEq, Eq, AtatEnum)] +pub enum ResultCodeSelection { + /// 0: CONNECT result code is given upon entering online data state; + ConnectOnly = 0, + /// 1-4: CONNECT result code is given upon entering online data state; + ConnectSpeed = 1, +} + /// Indicates the behavior of circuit 108 #[derive(Clone, PartialEq, Eq, AtatEnum)] pub enum Circuit108Behaviour { @@ -51,7 +67,8 @@ pub enum SoftwareFlowControl { Circuit105_106 = 3, } -#[derive(Clone, PartialEq, Eq, AtatEnum)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[at_enum(u32)] pub enum BaudRate { #[cfg(any( @@ -65,7 +82,7 @@ pub enum BaudRate { feature = "sara-g3", feature = "sara-g4" ))] - B0 = 0, + Auto = 0, #[cfg(any(feature = "lisa-u1", feature = "lisa-u2", feature = "sara-u2",))] B1200 = 1200, #[cfg(any( @@ -100,6 +117,7 @@ pub enum BaudRate { feature = "sara-u2", feature = "toby-r2", feature = "lara-r2", + feature = "lara-r6", feature = "toby-l4", ))] B230400 = 230_400, @@ -111,6 +129,7 @@ pub enum BaudRate { feature = "sara-u2", feature = "toby-r2", feature = "lara-r2", + feature = "lara-r6", feature = "toby-l4", ))] B460800 = 460_800, @@ -122,10 +141,11 @@ pub enum BaudRate { feature = "sara-u2", feature = "toby-r2", feature = "lara-r2", + feature = "lara-r6", feature = "toby-l4", ))] B921600 = 921_600, - #[cfg(any(feature = "toby-r2", feature = "lara-r2",))] + #[cfg(any(feature = "toby-r2", feature = "lara-r2", feature = "lara-r6"))] B3000000 = 3_000_000, #[cfg(any(feature = "toby-r2", feature = "lara-r2",))] B3250000 = 3_250_000, diff --git a/ublox-cellular/src/command/device_data_security/mod.rs b/src/command/device_data_security/mod.rs similarity index 99% rename from ublox-cellular/src/command/device_data_security/mod.rs rename to src/command/device_data_security/mod.rs index a1e7dec..a4b05fa 100644 --- a/ublox-cellular/src/command/device_data_security/mod.rs +++ b/src/command/device_data_security/mod.rs @@ -42,13 +42,13 @@ pub mod responses; pub mod types; +use crate::command::device_data_security::types::SecurityProfileId; use atat::atat_derive::AtatCmd; use heapless::Vec; use responses::{SecurityData, SecurityDataImport}; use types::{SecurityDataType, SecurityProfileOperation}; use super::NoResponse; -use crate::services::data::ssl::SecurityProfileId; /// 26.1.2 SSL/TLS certificates and private keys manager +USECMNG /// diff --git a/ublox-cellular/src/command/device_data_security/responses.rs b/src/command/device_data_security/responses.rs similarity index 100% rename from ublox-cellular/src/command/device_data_security/responses.rs rename to src/command/device_data_security/responses.rs diff --git a/ublox-cellular/src/command/device_data_security/types.rs b/src/command/device_data_security/types.rs similarity index 98% rename from ublox-cellular/src/command/device_data_security/types.rs rename to src/command/device_data_security/types.rs index 80512c2..2de9a8c 100644 --- a/ublox-cellular/src/command/device_data_security/types.rs +++ b/src/command/device_data_security/types.rs @@ -1,7 +1,8 @@ //! Argument and parameter types used by Device and data security Commands and Responses -use atat::atat_derive::AtatEnum; +use atat::atat_derive::{AtatEnum, AtatLen}; use heapless::String; +use serde::{Deserialize, Serialize}; /// Type of operation #[derive(Clone, PartialEq, Eq, AtatEnum)] @@ -218,3 +219,6 @@ pub enum SecurityProfileOperation { #[at_arg(value = 13)] TlsSessionResumption, } + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, AtatLen)] +pub struct SecurityProfileId(pub u8); diff --git a/ublox-cellular/src/command/device_lock/impl_.rs b/src/command/device_lock/impl_.rs similarity index 96% rename from ublox-cellular/src/command/device_lock/impl_.rs rename to src/command/device_lock/impl_.rs index 538cbd2..e5e0042 100644 --- a/ublox-cellular/src/command/device_lock/impl_.rs +++ b/src/command/device_lock/impl_.rs @@ -137,8 +137,7 @@ mod test { use super::*; use crate::command::device_lock::responses::PinStatus; use atat::serde_at::de::from_str; - use atat::serde_at::ser::to_string; - use heapless::String; + use atat::serde_at::ser::to_slice; #[test] fn serialize_pin_status() { @@ -146,9 +145,10 @@ mod test { value_sep: false, ..atat::serde_at::SerializeOptions::default() }; - let s = to_string::<_, 32>(&PinStatusCode::PhNetSubPin, "", options).unwrap(); + let mut buf = [0u8; 32]; + let s = to_slice(&PinStatusCode::PhNetSubPin, "", &mut buf, options).unwrap(); - assert_eq!(s, String::<32>::from("PH-NETSUB PIN")) + assert_eq!(&buf[..s], b"PH-NETSUB PIN") } #[test] diff --git a/ublox-cellular/src/command/device_lock/mod.rs b/src/command/device_lock/mod.rs similarity index 100% rename from ublox-cellular/src/command/device_lock/mod.rs rename to src/command/device_lock/mod.rs diff --git a/ublox-cellular/src/command/device_lock/responses.rs b/src/command/device_lock/responses.rs similarity index 100% rename from ublox-cellular/src/command/device_lock/responses.rs rename to src/command/device_lock/responses.rs diff --git a/ublox-cellular/src/command/device_lock/types.rs b/src/command/device_lock/types.rs similarity index 100% rename from ublox-cellular/src/command/device_lock/types.rs rename to src/command/device_lock/types.rs diff --git a/ublox-cellular/src/command/dns/mod.rs b/src/command/dns/mod.rs similarity index 100% rename from ublox-cellular/src/command/dns/mod.rs rename to src/command/dns/mod.rs diff --git a/ublox-cellular/src/command/dns/responses.rs b/src/command/dns/responses.rs similarity index 84% rename from ublox-cellular/src/command/dns/responses.rs rename to src/command/dns/responses.rs index ce3096e..4b8c03e 100644 --- a/ublox-cellular/src/command/dns/responses.rs +++ b/src/command/dns/responses.rs @@ -4,6 +4,7 @@ use heapless::String; /// 24.1 Resolve name / IP number through DNS +UDNSRN #[derive(Clone, PartialEq, Eq, AtatResp)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct ResolveNameIpResponse { #[at_arg(position = 0)] pub ip_domain_string: String<128>, diff --git a/ublox-cellular/src/command/dns/types.rs b/src/command/dns/types.rs similarity index 100% rename from ublox-cellular/src/command/dns/types.rs rename to src/command/dns/types.rs diff --git a/ublox-cellular/src/command/file_system/mod.rs b/src/command/file_system/mod.rs similarity index 100% rename from ublox-cellular/src/command/file_system/mod.rs rename to src/command/file_system/mod.rs diff --git a/ublox-cellular/src/command/file_system/responses.rs b/src/command/file_system/responses.rs similarity index 96% rename from ublox-cellular/src/command/file_system/responses.rs rename to src/command/file_system/responses.rs index fa39f9f..010f843 100644 --- a/ublox-cellular/src/command/file_system/responses.rs +++ b/src/command/file_system/responses.rs @@ -36,7 +36,7 @@ mod tests { let resp = b"+URDFILE: \"response.txt\",655,\"HTTP/1.1 400 Bad Request\r\nContent-Type: application/json\r\nContent-Length: 74\r\nConnection: close\r\nDate: Wed, 14 Jul 2021 11:50:30 GMT\r\nx-amzn-RequestId: 021e3877-1e6d-447d-996e-4bc89087bdc5\r\nx-amz-apigw-id: CdVdFFJhjoEFc0w=\r\nX-Amzn-Trace-Id: Root=1-60eecf86-227f4f986747c3113846cd63;Sampled=1\r\nVia: 1.1 32e3b86ae254a231182567c0124af893.cloudfront.net (CloudFront), 1.1 2afacc6ad96dbba3f0b477cd95f16459.cloudfront.net (CloudFront)\r\nX-Amz-Cf-Pop: FRA2-C2\r\nX-Cache: Error from cloudfront\r\nX-Amz-Cf-Pop: FRA2-C2\r\nX-Amz-Cf-Id: FELxJa2hgelObvyEP16HS4yEK-emXa1NiMsRXl-rmarzg309KeD34g==\r\n\r\n{\"uuid\": \"what\"}\""; let exp = ReadFileResponse { - filename: String::from("response.txt"), + filename: String::try_from("response.txt").unwrap(), size: 655, data: Bytes::from_slice(b"\"HTTP/1.1 400 Bad Request\r\nContent-Type: application/json\r\nContent-Length: 74\r\nConnection: close\r\nDate: Wed, 14 Jul 2021 11:50:30 GMT\r\nx-amzn-RequestId: 021e3877-1e6d-447d-996e-4bc89087bdc5\r\nx-amz-apigw-id: CdVdFFJhjoEFc0w=\r\nX-Amzn-Trace-Id: Root=1-60eecf86-227f4f986747c3113846cd63;Sampled=1\r\nVia: 1.1 32e3b86ae254a231182567c0124af893.cloudfront.net (CloudFront), 1.1 2afacc6ad96dbba3f0b477cd95f16459.cloudfront.net (CloudFront)\r\nX-Amz-Cf-Pop: FRA2-C2\r\nX-Cache: Error from cloudfront\r\nX-Amz-Cf-Pop: FRA2-C2\r\nX-Amz-Cf-Id: FELxJa2hgelObvyEP16HS4yEK-emXa1NiMsRXl-rmarzg309KeD34g==\r\n\r\n{\"uuid\": \"what\"}\"").unwrap(), }; @@ -50,7 +50,7 @@ mod tests { let resp = b"+URDBLOCK: \"response.txt\",512,\"HTTP/1.1 404 Not Found\r\nContent-Type: application/json\r\nContent-Length: 25\r\nConnection: close\r\nDate: Mon, 19 Jul 2021 07:23:35 GMT\r\nx-amzn-RequestId: 4a50eb56-5c1a-4388-9a2f-a1966ba9c8a2\r\nx-amz-apigw-id: CtNCvF1SDoEF2dw=\r\nX-Amzn-Trace-Id: Root=1-60f52877-6f5b63ac154d314436832848;Sampled=1\r\nVia: 1.1 58b222ebbb6cc6c8c8c9a46127ae3a3e.cloudfront.net (CloudFront), 1.1 6fa33d47af6f4da7007689083cfe9b9c.cloudfront.net (CloudFront)\r\nX-Amz-Cf-Pop: FRA2-C2\r\nX-Cache: Error from cloudfront\r\nX-Amz-Cf-Pop: FRA2-C2\r\nX-Amz-\""; let exp = ReadBlockResponse { - filename: String::from("response.txt"), + filename: String::try_from("response.txt").unwrap(), size: 512, data: Bytes::from_slice(b"\"HTTP/1.1 404 Not Found\r\nContent-Type: application/json\r\nContent-Length: 25\r\nConnection: close\r\nDate: Mon, 19 Jul 2021 07:23:35 GMT\r\nx-amzn-RequestId: 4a50eb56-5c1a-4388-9a2f-a1966ba9c8a2\r\nx-amz-apigw-id: CtNCvF1SDoEF2dw=\r\nX-Amzn-Trace-Id: Root=1-60f52877-6f5b63ac154d314436832848;Sampled=1\r\nVia: 1.1 58b222ebbb6cc6c8c8c9a46127ae3a3e.cloudfront.net (CloudFront), 1.1 6fa33d47af6f4da7007689083cfe9b9c.cloudfront.net (CloudFront)\r\nX-Amz-Cf-Pop: FRA2-C2\r\nX-Cache: Error from cloudfront\r\nX-Amz-Cf-Pop: FRA2-C2\r\nX-Amz-\"").unwrap(), }; @@ -64,7 +64,7 @@ mod tests { let resp = b"+URDBLOCK: \"response.txt\",512,\"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 2553\r\nConnection: close\r\nVary: Accept-Encoding\r\nDate: Mon, 19 Jul 2021 07:47:39 GMT\r\nx-amzn-RequestId: 436ba5b8-2aad-4089-a4fd-1b1c38773c87\r\nx-amz-apigw-id: CtQkMFE_DoEFUzg=\r\nX-Amzn-Trace-Id: Root=1-60f52e1a-0a05343260f3ba3331eea9d6;Sampled=1\r\nVia: 1.1 f99b5b46e77cfe9c3413f99dc8a4088c.cloudfront.net (CloudFront), 1.1 2f194b62c8c43859cbf5af8e53a8d2a7.cloudfront.net (CloudFront)\r\nX-Amz-Cf-Pop: FRA2-C2\r\nX-Cache: Miss from cloudfront\r\nX-Amz-Cf-Pop\""; let exp = ReadBlockResponse { - filename: String::from("response.txt"), + filename: String::try_from("response.txt").unwrap(), size: 512, data: Bytes::from_slice(b"\"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 2553\r\nConnection: close\r\nVary: Accept-Encoding\r\nDate: Mon, 19 Jul 2021 07:47:39 GMT\r\nx-amzn-RequestId: 436ba5b8-2aad-4089-a4fd-1b1c38773c87\r\nx-amz-apigw-id: CtQkMFE_DoEFUzg=\r\nX-Amzn-Trace-Id: Root=1-60f52e1a-0a05343260f3ba3331eea9d6;Sampled=1\r\nVia: 1.1 f99b5b46e77cfe9c3413f99dc8a4088c.cloudfront.net (CloudFront), 1.1 2f194b62c8c43859cbf5af8e53a8d2a7.cloudfront.net (CloudFront)\r\nX-Amz-Cf-Pop: FRA2-C2\r\nX-Cache: Miss from cloudfront\r\nX-Amz-Cf-Pop\"").unwrap(), }; @@ -77,7 +77,7 @@ mod tests { let resp = b"+URDBLOCK: \"response.txt\",512,\": FRA2-C2\r\nX-Amz-Cf-Id: _5ZSzv-MrL1yMkdklMqbtggquF-NEe6lO36pw9cYsKJVEITyIdrbqQ==\r\n\r\n{\"Data\":{\"certificate_pem\":\"-----BEGIN CERTIFICATE-----\nMIIDWjCCAkKgAwIBAgIVANeQUG3TupBxD8FLSz+AAqxU7rU0MA0GCSqGSIb3DQEB\nCwUAME0xSzBJBgNVBAsMQkFtYXpvbiBXZWIgU2VydmljZXMgTz1BbWF6b24uY29t\nIEluYy4gTD1TZWF0dGxlIFNUPVdhc2hpbmd0b24gQz1VUzAeFw0yMTA3MjIwOTA2\nMTlaFw00OTEyMzEyMzU5NTlaMB4xHDAaBgNVBAMME0FXUyBJb1QgQ2VydGlmaWNh\ndGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqCFWHSRH35wSjP0SR\nQijGwEfWArPaqr33S80y9D\""; let exp = ReadBlockResponse { - filename: String::from("response.txt"), + filename: String::try_from("response.txt").unwrap(), size: 512, data: Bytes::from_slice(b"\": FRA2-C2\r\nX-Amz-Cf-Id: _5ZSzv-MrL1yMkdklMqbtggquF-NEe6lO36pw9cYsKJVEITyIdrbqQ==\r\n\r\n{\"Data\":{\"certificate_pem\":\"-----BEGIN CERTIFICATE-----\nMIIDWjCCAkKgAwIBAgIVANeQUG3TupBxD8FLSz+AAqxU7rU0MA0GCSqGSIb3DQEB\nCwUAME0xSzBJBgNVBAsMQkFtYXpvbiBXZWIgU2VydmljZXMgTz1BbWF6b24uY29t\nIEluYy4gTD1TZWF0dGxlIFNUPVdhc2hpbmd0b24gQz1VUzAeFw0yMTA3MjIwOTA2\nMTlaFw00OTEyMzEyMzU5NTlaMB4xHDAaBgNVBAMME0FXUyBJb1QgQ2VydGlmaWNh\ndGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqCFWHSRH35wSjP0SR\nQijGwEfWArPaqr33S80y9D\"").unwrap(), }; diff --git a/ublox-cellular/src/command/general/mod.rs b/src/command/general/mod.rs similarity index 100% rename from ublox-cellular/src/command/general/mod.rs rename to src/command/general/mod.rs diff --git a/ublox-cellular/src/command/general/responses.rs b/src/command/general/responses.rs similarity index 96% rename from ublox-cellular/src/command/general/responses.rs rename to src/command/general/responses.rs index 49ed0b8..0e88e44 100644 --- a/ublox-cellular/src/command/general/responses.rs +++ b/src/command/general/responses.rs @@ -1,4 +1,5 @@ //! Responses for General Commands +use super::types; use atat::atat_derive::AtatResp; use atat::heapless_bytes::Bytes; @@ -23,7 +24,7 @@ pub struct ModelId { #[derive(Clone, Debug, AtatResp)] pub struct FirmwareVersion { #[at_arg(position = 0)] - pub version: Bytes<10>, + pub version: types::FirmwareVersion, } /// 4.7 IMEI identification +CGSN diff --git a/src/command/general/types.rs b/src/command/general/types.rs new file mode 100644 index 0000000..86a6d0c --- /dev/null +++ b/src/command/general/types.rs @@ -0,0 +1,96 @@ +//! Argument and parameter types used by General Commands and Responses + +use core::fmt::Write as _; + +use atat::atat_derive::AtatEnum; +use serde::{Deserialize, Deserializer, Serialize}; +#[derive(Clone, PartialEq, Eq, AtatEnum)] +pub enum Snt { + /// (default value): International Mobile station Equipment Identity (IMEI) + IMEI = 0, + /// International Mobile station Equipment Identity and Software Version number(IMEISV) + IMEISV = 2, + /// Software Version Number (SVN) + SVN = 3, + /// IMEI (not including the spare digit), the check digit and the SVN + IMEIExtended = 255, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FirmwareVersion { + major: u8, + minor: u8, +} + +impl FirmwareVersion { + pub fn new(major: u8, minor: u8) -> Self { + Self { major, minor } + } +} + +impl PartialOrd for FirmwareVersion { + fn partial_cmp(&self, other: &Self) -> Option { + match self.major.partial_cmp(&other.major) { + Some(core::cmp::Ordering::Equal) => {} + ord => return ord, + } + self.minor.partial_cmp(&other.minor) + } +} + +pub struct DeserializeError; + +impl core::fmt::Display for DeserializeError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "Failed to deserialize version") + } +} + +impl core::str::FromStr for FirmwareVersion { + type Err = DeserializeError; + fn from_str(s: &str) -> Result { + let mut iter = s.splitn(2, '.'); + let major = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or(DeserializeError)?; + let minor = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or(DeserializeError)?; + + Ok(Self { major, minor }) + } +} + +#[cfg(feature = "defmt")] +impl defmt::Format for FirmwareVersion { + fn format(&self, fmt: defmt::Formatter) { + defmt::write!(fmt, "{}.{}", self.major, self.minor) + } +} + +impl Serialize for FirmwareVersion { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut str = heapless::String::<7>::new(); + str.write_fmt(format_args!("{}.{}", self.major, self.minor)) + .map_err(serde::ser::Error::custom)?; + serializer.serialize_str(&str) + } +} + +impl<'de> Deserialize<'de> for FirmwareVersion { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = atat::heapless_bytes::Bytes::<7>::deserialize(deserializer)?; + core::str::FromStr::from_str( + &core::str::from_utf8(s.as_slice()).map_err(serde::de::Error::custom)?, + ) + .map_err(serde::de::Error::custom) + } +} diff --git a/ublox-cellular/src/command/gpio/mod.rs b/src/command/gpio/mod.rs similarity index 97% rename from ublox-cellular/src/command/gpio/mod.rs rename to src/command/gpio/mod.rs index 28e9d89..bf47f51 100644 --- a/ublox-cellular/src/command/gpio/mod.rs +++ b/src/command/gpio/mod.rs @@ -23,7 +23,7 @@ use super::NoResponse; /// The test command provides the list of the supported GPIOs, the supported functions and the status of all the /// GPIOs. #[derive(Clone, AtatCmd)] -#[at_cmd("+UGPIOC", NoResponse)] +#[at_cmd("+UGPIOC", NoResponse, timeout_ms = 3000)] pub struct SetGpioConfiguration { /// GPIO pin identifier: pin number /// See the GPIO mapping for the available GPIO pins, their mapping and factoryprogrammed values on different u-blox cellular modules series and product version. diff --git a/ublox-cellular/src/command/gpio/responses.rs b/src/command/gpio/responses.rs similarity index 100% rename from ublox-cellular/src/command/gpio/responses.rs rename to src/command/gpio/responses.rs diff --git a/ublox-cellular/src/command/gpio/types.rs b/src/command/gpio/types.rs similarity index 96% rename from ublox-cellular/src/command/gpio/types.rs rename to src/command/gpio/types.rs index fa6c049..c44f04b 100644 --- a/ublox-cellular/src/command/gpio/types.rs +++ b/src/command/gpio/types.rs @@ -20,13 +20,6 @@ pub enum GpioInPull { PullDown = 2, } -// #[derive(Clone, PartialEq, AtatEnum)] -// pub enum GpioNumber { -// #[cfg(feature = "toby-r2")] -// Gpio1, - -// } - #[derive(Clone, PartialEq, Eq, AtatEnum)] pub enum GpioMode { /// • 0: output diff --git a/ublox-cellular/src/command/http/mod.rs b/src/command/http/mod.rs similarity index 100% rename from ublox-cellular/src/command/http/mod.rs rename to src/command/http/mod.rs diff --git a/ublox-cellular/src/command/http/urc.rs b/src/command/http/urc.rs similarity index 100% rename from ublox-cellular/src/command/http/urc.rs rename to src/command/http/urc.rs diff --git a/ublox-cellular/src/command/ip_transport_layer/mod.rs b/src/command/ip_transport_layer/mod.rs similarity index 86% rename from ublox-cellular/src/command/ip_transport_layer/mod.rs rename to src/command/ip_transport_layer/mod.rs index 0424448..2f767c3 100644 --- a/ublox-cellular/src/command/ip_transport_layer/mod.rs +++ b/src/command/ip_transport_layer/mod.rs @@ -6,7 +6,7 @@ pub mod types; pub mod urc; use atat::atat_derive::AtatCmd; -use embedded_nal::IpAddr; +use core::net::IpAddr; use responses::{ CreateSocketResponse, SocketControlResponse, SocketData, SocketErrorResponse, UDPSendToDataResponse, UDPSocketData, WriteSocketDataResponse, @@ -53,7 +53,9 @@ pub struct CreateSocket { #[derive(Clone, AtatCmd)] #[at_cmd("+USOSEC", NoResponse)] pub struct SetSocketSslState { - #[at_arg(position = 0)] + // len 1 as ublox devices only support 7 sockets but needs to be changed if this changes! + // FIXME: having all the lines use a constant something like #[at_arg(position = 0, len = MAX_SOCKETS)] + #[at_arg(position = 0, len = 1)] pub socket: SocketHandle, #[at_arg(position = 1)] pub ssl_tls_status: SslTlsStatus, @@ -70,7 +72,8 @@ pub struct SetSocketSslState { #[derive(Clone, AtatCmd)] #[at_cmd("+USOCL", NoResponse, attempts = 1, timeout_ms = 120000)] pub struct CloseSocket { - #[at_arg(position = 0)] + // len 1 as ublox devices only support 7 sockets but needs to be changed if this changes! + #[at_arg(position = 0, len = 1)] pub socket: SocketHandle, } @@ -95,7 +98,8 @@ pub struct GetSocketError; #[derive(Clone, AtatCmd)] #[at_cmd("+USOCO", NoResponse, attempts = 1, timeout_ms = 120000)] pub struct ConnectSocket { - #[at_arg(position = 0)] + // len 1 as ublox devices only support 7 sockets but needs to be changed if this changes! + #[at_arg(position = 0, len = 1)] pub socket: SocketHandle, #[at_arg(position = 1, len = 39)] pub remote_addr: IpAddr, @@ -114,7 +118,8 @@ pub struct ConnectSocket { #[derive(Clone, AtatCmd)] #[at_cmd("+USOWR", WriteSocketDataResponse)] pub struct WriteSocketData<'a> { - #[at_arg(position = 0)] + // len 1 as ublox devices only support 7 sockets but needs to be changed if this changes! + #[at_arg(position = 0, len = 1)] pub socket: SocketHandle, #[at_arg(position = 1)] pub length: usize, @@ -134,7 +139,8 @@ pub struct WriteSocketData<'a> { #[derive(Clone, AtatCmd)] #[at_cmd("+USOWR", WriteSocketDataResponse)] pub struct WriteSocketDataHex<'a> { - #[at_arg(position = 0)] + // len 1 as ublox devices only support 7 sockets but needs to be changed if this changes! + #[at_arg(position = 0, len = 1)] pub socket: SocketHandle, #[at_arg(position = 1)] pub length: usize, @@ -153,7 +159,8 @@ pub struct WriteSocketDataHex<'a> { #[derive(Clone, AtatCmd)] #[at_cmd("+USOWR", NoResponse)] pub struct PrepareWriteSocketDataBinary { - #[at_arg(position = 0)] + // len 1 as ublox devices only support 7 sockets but needs to be changed if this changes! + #[at_arg(position = 0, len = 1)] pub socket: SocketHandle, #[at_arg(position = 1)] pub length: usize, @@ -185,7 +192,8 @@ pub struct WriteSocketDataBinary<'a> { #[derive(Clone, AtatCmd)] #[at_cmd("+USOST", NoResponse)] pub struct PrepareUDPSendToDataBinary { - #[at_arg(position = 0)] + // len 1 as ublox devices only support 7 sockets but needs to be changed if this changes! + #[at_arg(position = 0, len = 1)] pub socket: SocketHandle, #[at_arg(position = 1, len = 39)] pub remote_addr: IpAddr, @@ -228,7 +236,8 @@ pub struct UDPSendToDataBinary<'a> { #[derive(Clone, AtatCmd)] #[at_cmd("+USORD", SocketData)] pub struct ReadSocketData { - #[at_arg(position = 0)] + // len 1 as ublox devices only support 7 sockets but needs to be changed if this changes! + #[at_arg(position = 0, len = 1)] pub socket: SocketHandle, #[at_arg(position = 1)] pub length: usize, @@ -244,7 +253,8 @@ pub struct ReadSocketData { #[derive(Clone, AtatCmd)] #[at_cmd("+USORF", UDPSocketData)] pub struct ReadUDPSocketData { - #[at_arg(position = 0)] + // len 1 as ublox devices only support 7 sockets but needs to be changed if this changes! + #[at_arg(position = 0, len = 1)] pub socket: SocketHandle, #[at_arg(position = 1)] pub length: usize, @@ -267,7 +277,8 @@ pub struct SetHexMode { #[derive(Clone, AtatCmd)] #[at_cmd("+USOCTL", SocketControlResponse)] pub struct SocketControl { - #[at_arg(position = 0)] + // len 1 as ublox devices only support 7 sockets but needs to be changed if this changes! + #[at_arg(position = 0, len = 1)] pub socket: SocketHandle, #[at_arg(position = 1)] pub param_id: SocketControlParam, diff --git a/ublox-cellular/src/command/ip_transport_layer/responses.rs b/src/command/ip_transport_layer/responses.rs similarity index 96% rename from ublox-cellular/src/command/ip_transport_layer/responses.rs rename to src/command/ip_transport_layer/responses.rs index 1e5c9d6..4191ca2 100644 --- a/ublox-cellular/src/command/ip_transport_layer/responses.rs +++ b/src/command/ip_transport_layer/responses.rs @@ -1,11 +1,12 @@ //! Responses for Internet protocol transport layer Commands use super::types::SocketControlParam; -use crate::services::data::INGRESS_CHUNK_SIZE; use atat::atat_derive::AtatResp; -use embedded_nal::IpAddr; +use core::net::IpAddr; use heapless::String; use ublox_sockets::SocketHandle; +pub const INGRESS_CHUNK_SIZE: usize = 256; + /// 25.3 Create Socket +USOCR #[derive(Debug, Clone, AtatResp)] pub struct CreateSocketResponse { diff --git a/ublox-cellular/src/command/ip_transport_layer/types.rs b/src/command/ip_transport_layer/types.rs similarity index 95% rename from ublox-cellular/src/command/ip_transport_layer/types.rs rename to src/command/ip_transport_layer/types.rs index 2cb7ad0..623376f 100644 --- a/ublox-cellular/src/command/ip_transport_layer/types.rs +++ b/src/command/ip_transport_layer/types.rs @@ -1,5 +1,5 @@ //! Argument and parameter types used by Internet protocol transport layer Commands and Responses -use crate::services::data::ssl::SecurityProfileId; +use crate::command::device_data_security::types::SecurityProfileId; use atat::atat_derive::AtatEnum; #[derive(Clone, PartialEq, Eq, AtatEnum)] diff --git a/ublox-cellular/src/command/ip_transport_layer/urc.rs b/src/command/ip_transport_layer/urc.rs similarity index 100% rename from ublox-cellular/src/command/ip_transport_layer/urc.rs rename to src/command/ip_transport_layer/urc.rs diff --git a/src/command/ipc/mod.rs b/src/command/ipc/mod.rs new file mode 100644 index 0000000..b391cf6 --- /dev/null +++ b/src/command/ipc/mod.rs @@ -0,0 +1,58 @@ +//! ### 3 - IPC - Inter Processor Communication +use atat::atat_derive::AtatCmd; + +use super::NoResponse; + +/// 3.1 Multiplexing mode +CMUX +/// +/// Enables the multiplexing protocol control channel as defined in 3GPP TS +/// 27.010 [104]. The command sets the parameters for the control channel. The +/// result code is returned using the old interface speed. The parameters become +/// active only after sending the OK result code. The usage of +CMUX set command +/// during the multiplexing is not allowed. +#[derive(Clone, AtatCmd)] +#[at_cmd("+CMUX", NoResponse)] +pub struct SetMultiplexing { + /// Multiplexer transparency mechanism: + #[at_arg(position = 0)] + pub mode: u8, + + /// The way in which the multiplexer control channel is set up: + #[at_arg(position = 1)] + pub subset: Option, + + /// Transmission rate. The allowed range is 0-7. + /// 0, 9600, 19200, 38400, 57600, 115200, 230400, 460800 + #[at_arg(position = 2)] + pub port_speed: Option, + + /// Maximum frame size + /// + /// - Allowed range is 1-1509. + /// - The default value is 31. + #[at_arg(position = 3)] + pub n1: Option, + + /// Acknowledgement timer in units of ten milliseconds. + /// + /// - The allowed range is 1-255 + #[at_arg(position = 4)] + pub t1: Option, + + /// Maximum number of re-transmissions + #[at_arg(position = 5)] + pub n2: Option, + + /// Response timer for the multiplexer control channel in units of ten + /// milliseconds. + #[at_arg(position = 6)] + pub t2: Option, + + /// Wake up response timer. + #[at_arg(position = 7)] + pub t3: Option, + + /// Window size, for advanced operation with Error Recovery options. + #[at_arg(position = 8)] + pub k: Option, +} diff --git a/ublox-cellular/src/command/mobile_control/mod.rs b/src/command/mobile_control/mod.rs similarity index 100% rename from ublox-cellular/src/command/mobile_control/mod.rs rename to src/command/mobile_control/mod.rs diff --git a/ublox-cellular/src/command/mobile_control/responses.rs b/src/command/mobile_control/responses.rs similarity index 100% rename from ublox-cellular/src/command/mobile_control/responses.rs rename to src/command/mobile_control/responses.rs diff --git a/ublox-cellular/src/command/mobile_control/types.rs b/src/command/mobile_control/types.rs similarity index 90% rename from ublox-cellular/src/command/mobile_control/types.rs rename to src/command/mobile_control/types.rs index 04f8349..30b550d 100644 --- a/ublox-cellular/src/command/mobile_control/types.rs +++ b/src/command/mobile_control/types.rs @@ -1,7 +1,7 @@ //! Argument and parameter types used by Mobile equipment control and status Commands and Responses use atat::atat_derive::AtatEnum; -#[derive(Clone, PartialEq, Eq, AtatEnum)] +#[derive(Clone, Copy, PartialEq, Eq, AtatEnum)] pub enum Functionality { /// 0: Sets the MT to minimum functionality (disable both transmit and receive RF /// circuits by deactivating both CS and PS services) @@ -17,7 +17,8 @@ pub enum Functionality { feature = "toby-l4", feature = "leon-g1", feature = "sara-g3", - feature = "sara-g4" + feature = "sara-g4", + feature = "sara-r5", ))] Minimum = 0, @@ -35,7 +36,8 @@ pub enum Functionality { feature = "toby-l4", feature = "leon-g1", feature = "sara-g3", - feature = "sara-g4" + feature = "sara-g4", + feature = "sara-r5", ))] Full = 1, @@ -51,7 +53,8 @@ pub enum Functionality { feature = "toby-r2", feature = "lara-r2", feature = "lara-r6", - feature = "toby-l4" + feature = "toby-l4", + feature = "sara-r5", ))] AirplaneMode = 4, @@ -69,7 +72,8 @@ pub enum Functionality { feature = "toby-l4", feature = "leon-g1", feature = "sara-g3", - feature = "sara-g4" + feature = "sara-g4", + feature = "sara-r5", ))] DedicatedMode = 6, @@ -84,7 +88,8 @@ pub enum Functionality { feature = "toby-l4", feature = "leon-g1", feature = "sara-g3", - feature = "sara-g4" + feature = "sara-g4", + feature = "sara-r5", ))] DisableSimToolkit = 7, #[cfg(any( @@ -96,7 +101,8 @@ pub enum Functionality { feature = "toby-l4", feature = "leon-g1", feature = "sara-g3", - feature = "sara-g4" + feature = "sara-g4", + feature = "sara-r5", ))] DisableSimToolkit_ = 8, @@ -107,7 +113,8 @@ pub enum Functionality { feature = "lisa-u2", feature = "sara-u2", feature = "toby-r2", - feature = "lara-r2" + feature = "lara-r2", + feature = "sara-r5", ))] RawMode = 9, @@ -125,7 +132,8 @@ pub enum Functionality { feature = "toby-l4", feature = "leon-g1", feature = "sara-g3", - feature = "sara-g4" + feature = "sara-g4", + feature = "sara-r5", ))] SilentReset = 15, @@ -141,19 +149,25 @@ pub enum Functionality { feature = "toby-l4", feature = "leon-g1", feature = "sara-g3", - feature = "sara-g4" + feature = "sara-g4", + feature = "sara-r5", ))] SilentResetWithSimReset = 16, /// 19: Sets the MT to minimum functionality by deactivating CS and PS services and /// the SIM card - #[cfg(any(feature = "toby-l2", feature = "mpci-l2", feature = "toby-l4"))] + #[cfg(any( + feature = "toby-l2", + feature = "mpci-l2", + feature = "toby-l4", + feature = "sara-r5", + ))] MinimumWithoutSim = 19, /// 127: Sets the MT in a deep low power state "HALT" (with detach from the network /// and saving of the NVM parameters); the only way to wake up the module is a power /// cycle or a module reset - #[cfg(any(feature = "toby-l2", feature = "mpci-l2"))] + #[cfg(any(feature = "toby-l2", feature = "mpci-l2", feature = "sara-r5",))] Halt = 127, } @@ -192,7 +206,7 @@ pub enum TerminationErrorMode { Verbose = 2, } -#[derive(Clone, PartialEq, Eq, AtatEnum)] +#[derive(Clone, Copy, PartialEq, Eq, AtatEnum)] pub enum PowerMode { ///MT is switched on with minimum functionality Minimum = 0, diff --git a/src/command/mod.rs b/src/command/mod.rs new file mode 100644 index 0000000..2b5e0c5 --- /dev/null +++ b/src/command/mod.rs @@ -0,0 +1,150 @@ +//! AT Commands for u-blox cellular module family\ +//! Following the [u-blox cellular modules AT commands manual](https://content.u-blox.com/sites/default/files/u-blox-CEL_ATCommands_UBX-13002752.pdf) + +pub mod call_control; +pub mod control; +pub mod device_data_security; +pub mod device_lock; +pub mod dns; +pub mod file_system; +pub mod general; +pub mod gpio; +pub mod http; +#[cfg(feature = "internal-network-stack")] +pub mod ip_transport_layer; +pub mod ipc; +pub mod mobile_control; +pub mod network_service; +pub mod networking; +pub mod psn; +pub mod sms; +pub mod system_features; + +use atat::{ + atat_derive::{AtatCmd, AtatResp, AtatUrc}, + nom, +}; + +#[derive(Clone, AtatResp)] +pub struct NoResponse; + +#[derive(Clone, AtatCmd)] +#[at_cmd("", NoResponse, attempts = 3)] +pub struct AT; + +#[derive(Debug, Clone, AtatUrc)] +pub enum Urc { + #[at_urc("+CGEV: NW DETACH")] + NetworkDetach, + #[at_urc("+CGEV: ME DETACH")] + MobileStationDetach, + #[at_urc("+CGEV: NW DEACT")] + NetworkDeactivate, + #[at_urc("+CGEV: ME DEACT")] + MobileStationDeactivate, + #[at_urc("+CGEV: NW PDN DEACT")] + NetworkPDNDeactivate, + #[at_urc("+CGEV: ME PDN DEACT")] + MobileStationPDNDeactivate, + + #[cfg(feature = "internal-network-stack")] + #[at_urc("+UUSORD")] + SocketDataAvailable(ip_transport_layer::urc::SocketDataAvailable), + #[cfg(feature = "internal-network-stack")] + #[at_urc("+UUSORF")] + SocketDataAvailableUDP(ip_transport_layer::urc::SocketDataAvailable), + #[cfg(feature = "internal-network-stack")] + #[at_urc("+UUSOCL")] + SocketClosed(ip_transport_layer::urc::SocketClosed), + + #[at_urc("+UUPSDA")] + DataConnectionActivated(psn::urc::DataConnectionActivated), + #[at_urc("+UUPSDD")] + DataConnectionDeactivated(psn::urc::DataConnectionDeactivated), + + #[at_urc("+UMWI")] + MessageWaitingIndication(sms::urc::MessageWaitingIndication), + #[at_urc("+CREG", parse = custom_cxreg_parse)] + NetworkRegistration(network_service::urc::NetworkRegistration), + #[at_urc("+CGREG", parse = custom_cxreg_parse)] + GPRSNetworkRegistration(psn::urc::GPRSNetworkRegistration), + #[at_urc("+CEREG", parse = custom_cxreg_parse)] + EPSNetworkRegistration(psn::urc::EPSNetworkRegistration), + #[at_urc("+UREG")] + ExtendedPSNetworkRegistration(psn::urc::ExtendedPSNetworkRegistration), + + #[at_urc("+UUHTTPCR")] + HttpResponse(http::urc::HttpResponse), +} + +fn custom_cxreg_parse<'a, T, Error: nom::error::ParseError<&'a [u8]> + core::fmt::Debug>( + token: T, +) -> impl Fn(&'a [u8]) -> nom::IResult<&'a [u8], (&'a [u8], usize), Error> +where + &'a [u8]: nom::Compare + nom::FindSubstring, + T: nom::InputLength + Clone + nom::InputTake + nom::InputIter + nom::AsBytes, +{ + move |i| { + let (i, (urc, len)) = atat::digest::parser::urc_helper(token.clone())(i)?; + + let index = urc.iter().position(|&x| x == b':').unwrap_or(urc.len()); + let arguments = &urc[index + 1..]; + + // "+CxREG?" response will always have atleast 2 arguments, both being + // integers. + // + // "+CxREG:" URC will always have at least 1 integer argument, and the + // second argument, if present, will be a string. + + // Parse the first + let (rem, _) = nom::sequence::tuple(( + nom::character::complete::space0, + nom::number::complete::u8, + nom::branch::alt((nom::combinator::eof, nom::bytes::complete::tag(","))), + ))(arguments)?; + + if !rem.is_empty() { + // If we have more arguments, we want to make sure this is a quoted string for the URC case. + nom::sequence::tuple(( + nom::character::complete::space0, + nom::sequence::delimited( + nom::bytes::complete::tag("\""), + nom::bytes::complete::escaped( + nom::character::streaming::none_of("\"\\"), + '\\', + nom::character::complete::one_of("\"\\"), + ), + nom::bytes::complete::tag("\""), + ), + nom::branch::alt((nom::combinator::eof, nom::bytes::complete::tag(","))), + ))(rem)?; + } + + Ok((i, (urc, len))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_custom_parse_cxreg() { + let creg_resp = b"\r\n+CREG: 2,5,\"9E9A\",\"019624BD\",2\r\n"; + let creg_urc_min = b"\r\n+CREG: 0\r\n"; + let creg_urc_full = b"\r\n+CREG: 5,\"9E9A\",\"0196BDB0\",2\r\n"; + + assert!( + custom_cxreg_parse::<&[u8], nom::error::Error<&[u8]>>(&b"+CREG"[..])(creg_resp) + .is_err() + ); + assert!( + custom_cxreg_parse::<&[u8], nom::error::Error<&[u8]>>(&b"+CREG"[..])(creg_urc_min) + .is_ok() + ); + assert!( + custom_cxreg_parse::<&[u8], nom::error::Error<&[u8]>>(&b"+CREG"[..])(creg_urc_full) + .is_ok() + ); + } +} diff --git a/ublox-cellular/src/command/network_service/impl_.rs b/src/command/network_service/impl_.rs similarity index 93% rename from ublox-cellular/src/command/network_service/impl_.rs rename to src/command/network_service/impl_.rs index 9c7ba46..50d3fa0 100644 --- a/ublox-cellular/src/command/network_service/impl_.rs +++ b/src/command/network_service/impl_.rs @@ -1,5 +1,5 @@ +use super::types::Error; use super::types::NetworkRegistrationStat; -use crate::network::Error; impl NetworkRegistrationStat { #[must_use] diff --git a/ublox-cellular/src/command/network_service/mod.rs b/src/command/network_service/mod.rs similarity index 90% rename from ublox-cellular/src/command/network_service/mod.rs rename to src/command/network_service/mod.rs index 94bb918..ed331c5 100644 --- a/ublox-cellular/src/command/network_service/mod.rs +++ b/src/command/network_service/mod.rs @@ -139,3 +139,20 @@ pub struct SetNetworkRegistrationStatus { #[derive(Clone, AtatCmd)] #[at_cmd("+CREG?", NetworkRegistrationStatus)] pub struct GetNetworkRegistrationStatus; + +/// 7.15 Channel and network environment description +UCGED +/// +/// Enables the protocol stack and network environment information collection. +/// The information text response of the read command reports only the current +/// RAT (if any) parameters, determined by the parameter value. +/// +/// **NOTES** +/// - LARA-R6: The command provides only the information on the serving cell, +/// unless =2 (short form reporting enabled) and =2 (2G). If =2 +/// (short form reporting enabled) and =2 (2G), where supported, the module +/// returns also the information on the neighbor cells. +#[derive(Clone, AtatCmd)] +#[at_cmd("+UCGED", NoResponse)] +pub struct SetChannelAndNetworkEnvDesc { + pub mode: u8, +} diff --git a/ublox-cellular/src/command/network_service/responses.rs b/src/command/network_service/responses.rs similarity index 96% rename from ublox-cellular/src/command/network_service/responses.rs rename to src/command/network_service/responses.rs index cfc85c5..25eefd6 100644 --- a/ublox-cellular/src/command/network_service/responses.rs +++ b/src/command/network_service/responses.rs @@ -26,6 +26,7 @@ pub struct SignalQuality { /// 7.5 Operator selection +COPS #[derive(Clone, AtatResp)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct OperatorSelection { #[at_arg(position = 0)] pub mode: OperatorSelectionMode, diff --git a/ublox-cellular/src/command/network_service/types.rs b/src/command/network_service/types.rs similarity index 95% rename from ublox-cellular/src/command/network_service/types.rs rename to src/command/network_service/types.rs index 7f9512d..cdb6e3e 100644 --- a/ublox-cellular/src/command/network_service/types.rs +++ b/src/command/network_service/types.rs @@ -80,7 +80,7 @@ pub enum NetworkRegistrationStat { /// • 3: registration denied RegistrationDenied = 3, /// • 4: unknown (e.g. out of GERAN/UTRAN/E-UTRAN coverage) - Unknown = 4, + OutOfCoverage = 4, /// • 5: registered, roaming RegisteredRoaming = 5, /// • 6: registered for "SMS only", home network (applicable only when @@ -173,6 +173,7 @@ pub enum ThirdRadioAccessTechnology { } #[derive(Debug, Clone, PartialEq, Eq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum OperatorNameFormat { #[at_arg(value = 0)] Long(String<24>), @@ -181,3 +182,12 @@ pub enum OperatorNameFormat { #[at_arg(value = 2)] Numeric(String<6>), } + +#[derive(Debug, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Error { + RegistrationDenied, + UnknownProfile, + ActivationFailed, + _Unknown, +} diff --git a/ublox-cellular/src/command/network_service/urc.rs b/src/command/network_service/urc.rs similarity index 100% rename from ublox-cellular/src/command/network_service/urc.rs rename to src/command/network_service/urc.rs diff --git a/src/command/networking/mod.rs b/src/command/networking/mod.rs new file mode 100644 index 0000000..0ec06f4 --- /dev/null +++ b/src/command/networking/mod.rs @@ -0,0 +1,36 @@ +//! ### 34 - Networking +pub mod responses; +pub mod types; + +use super::NoResponse; +use atat::atat_derive::AtatCmd; + +use responses::EmbeddedPortFiltering; +use types::EmbeddedPortFilteringMode; + +/// 34.4 Configure port filtering for embedded applications +UEMBPF +/// +/// Enables/disables port filtering to allow IP data traffic from embedded +/// applications when a dial-up connection is active. When enabled, the +/// application will pick source port inside the configured range and the +/// incoming traffic to those ports will be directed to embedded application +/// instead of PPP DTE. +/// +/// **NOTE:** +/// - Each set command overwrites the previous configuration. Only one port +/// range can be configured. +/// - When set command with =0 is issued, the parameter shall +/// not be inserted otherwise the command will return an error result code. +/// - If =0 is configured, the read command will not return any range, but +/// only +UEMBPF: 0 as information text response. +#[derive(Clone, AtatCmd)] +#[at_cmd("+UEMBPF?", EmbeddedPortFiltering)] +pub struct GetEmbeddedPortFiltering; + +/// 34.4 Configure port filtering for embedded applications +UEMBPF +#[derive(Clone, AtatCmd)] +#[at_cmd("+UEMBPF", NoResponse)] +pub struct SetEmbeddedPortFiltering { + #[at_arg(position = 0)] + pub mode: EmbeddedPortFilteringMode, +} diff --git a/src/command/networking/responses.rs b/src/command/networking/responses.rs new file mode 100644 index 0000000..10878a3 --- /dev/null +++ b/src/command/networking/responses.rs @@ -0,0 +1,12 @@ +//! Responses for Networking Commands +use atat::atat_derive::AtatResp; + +/// 34.4 Configure port filtering for embedded applications +UEMBPF +#[derive(Debug, Clone, AtatResp)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct EmbeddedPortFiltering { + #[at_arg(position = 0)] + pub mode: u8, + #[at_arg(position = 1)] + pub port_range: heapless::String<16>, +} diff --git a/src/command/networking/types.rs b/src/command/networking/types.rs new file mode 100644 index 0000000..16b9197 --- /dev/null +++ b/src/command/networking/types.rs @@ -0,0 +1,74 @@ +//! Argument and parameter types used by Networking Commands and Responses +use core::fmt::Write; + +use atat::AtatLen; +use serde::{Serialize, Serializer}; + +/// Port filtering enable/disable +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum EmbeddedPortFilteringMode { + /// 0: disable. The configured range is removed. + Disable, + /// 1: enable. The parameter is mandatory + Enable(u16, u16), +} + +impl AtatLen for EmbeddedPortFilteringMode { + const LEN: usize = 20; +} + +impl Serialize for EmbeddedPortFilteringMode { + fn serialize(&self, serializer: S) -> core::result::Result + where + S: Serializer, + { + match *self { + EmbeddedPortFilteringMode::Disable => Serializer::serialize_u8(serializer, 0), + EmbeddedPortFilteringMode::Enable(port_start, port_end) => { + let mut serde_state = + atat::serde_at::serde::ser::Serializer::serialize_tuple_variant( + serializer, + "EmbeddedPortFilteringMode", + 1_u32, + "Enable", + 0, + )?; + atat::serde_at::serde::ser::SerializeTupleVariant::serialize_field( + &mut serde_state, + &{ + let mut str = heapless::String::<16>::new(); + str.write_fmt(format_args!("{}-{}", port_start, port_end)) + .ok(); + str + }, + )?; + atat::serde_at::serde::ser::SerializeTupleVariant::end(serde_state) + } + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use atat::serde_at::ser::to_slice; + + #[test] + fn serialize_embedded_port_filtering_mode() { + let options = atat::serde_at::SerializeOptions { + value_sep: false, + ..atat::serde_at::SerializeOptions::default() + }; + let mut buf = [0u8; 32]; + let s = to_slice( + &EmbeddedPortFilteringMode::Enable(6000, 6200), + "", + &mut buf, + options, + ) + .unwrap(); + + assert_eq!(&buf[..s], b"1,\"6000-6200\"") + } +} diff --git a/ublox-cellular/src/command/psn/mod.rs b/src/command/psn/mod.rs similarity index 86% rename from ublox-cellular/src/command/psn/mod.rs rename to src/command/psn/mod.rs index 9644485..8ac2a71 100644 --- a/ublox-cellular/src/command/psn/mod.rs +++ b/src/command/psn/mod.rs @@ -31,14 +31,13 @@ use responses::{ PacketSwitchedNetworkData, }; use types::{ - AuthenticationType, EPSNetworkRegistrationUrcConfig, ExtendedPSNetworkRegistrationUrcConfig, - GPRSAttachedState, GPRSNetworkRegistrationUrcConfig, PDPContextStatus, PSEventReportingMode, - PacketSwitchedAction, PacketSwitchedNetworkDataParam, PacketSwitchedParam, - PacketSwitchedParamReq, + AuthenticationType, ContextId, EPSNetworkRegistrationUrcConfig, + ExtendedPSNetworkRegistrationUrcConfig, GPRSAttachedState, GPRSNetworkRegistrationUrcConfig, + PDPContextStatus, PSEventReportingMode, PacketSwitchedAction, PacketSwitchedNetworkDataParam, + PacketSwitchedParam, PacketSwitchedParamReq, ProfileId, }; use super::NoResponse; -use crate::network::{ContextId, ProfileId}; /// 18.4 PDP context definition +CGDCONT /// @@ -94,6 +93,10 @@ pub struct SetPDPContextDefinition<'a> { pub apn: &'a str, } +#[derive(Clone, AtatCmd)] +#[at_cmd("+CGDCONT?", NoResponse)] +pub struct GetPDPContextDefinition; + /// 18.7 Set Packet switched data configuration +UPSD /// /// Sets all the parameters in a specific packet switched data (PSD) profile. @@ -266,6 +269,33 @@ pub struct SetPDPContextState { #[at_cmd("+CGACT?", heapless::Vec, attempts = 1, timeout_ms = 150000, abortable = true)] pub struct GetPDPContextState; +/// 18.21 Enter PPP state/GPRS dial-up D* +/// +/// The V.24 dial command "D", similar to the command with the syntax +/// AT+CGDATA="PPP",, causes the MT to perform the necessary actions to +/// establish the communication between the DTE and the external PDP network +/// through the PPP protocol. This can include performing a PS attach and, if +/// the PPP server on the DTE side starts communication, PDP context activation +/// on the specified PDP context identifier (if not already requested by means +/// of +CGATT and +CGACT commands). +/// +/// If the command is accepted and the preliminary PS procedures have succeeded, +/// the "CONNECT" intermediate result code is returned, the MT enters the +/// V.25ter online data state and the PPP L2 protocol between the MT and the DTE +/// is started. +#[derive(Clone, AtatCmd)] +#[at_cmd( + "D*99***", + NoResponse, + value_sep = false, + abortable = true, + termination = "#\r\n" +)] +pub struct EnterPPP { + #[at_arg(position = 0)] + pub cid: ContextId, +} + /// 18.26 Packet switched event reporting +CGEREP /// /// Configures sending of URCs from MT to the DTE, in case of certain events @@ -348,6 +378,22 @@ pub struct SetExtendedPSNetworkRegistrationStatus { #[at_cmd("+UREG?", ExtendedPSNetworkRegistrationStatus)] pub struct GetExtendedPSNetworkRegistrationStatus; +/// 18.29 Manual deactivation of a PDP context H +/// +/// Deactivates an active PDP context with PPP L2 protocol in online command +/// mode. The MT responds with a final result code. For a detailed description, +/// see the H command description. For additional information about OLCM, see +/// the AT command settings. +/// +/// **NOTES**: +/// - In GPRS online command mode, entered by typing the escape sequence "+++" +/// or "~+++" (see &D), the ATH command is needed to terminate the connection. +/// Alternatively, in data transfer mode, DTE originated DTR toggling or PPP +/// disconnection may be used. +#[derive(Clone, AtatCmd)] +#[at_cmd("H", NoResponse)] +pub struct DeactivatePDPContext; + /// 18.36 EPS network registration status +CEREG /// /// Configures the network registration URC related to EPS domain. The URC @@ -416,6 +462,7 @@ pub struct GetEPSNetworkRegistrationStatus; /// - **TOBY-L4 / TOBY-L2 / MPCI-L2 / LARA-R2 / TOBY-R2 / SARA-U2 / LISA-U2 / /// SARA-G4 / SARA-G3** - The command returns an error result code if the /// input is already active or not yet defined. + #[derive(Clone, AtatCmd)] #[at_cmd("+UAUTHREQ", NoResponse)] pub struct SetAuthParameters<'a> { @@ -423,8 +470,43 @@ pub struct SetAuthParameters<'a> { pub cid: ContextId, #[at_arg(position = 1)] pub auth_type: AuthenticationType, - #[at_arg(position = 2, len = 64)] + // For SARA-R4 and LARA-R6 modules the username and parameters are reversed + #[cfg_attr( + any( + feature = "sara-r410m", + feature = "sara-r412m", + feature = "sara-r422", + feature = "lara-r6" + ), + at_arg(position = 3, len = 64) + )] + #[cfg_attr( + not(any( + feature = "sara-r410m", + feature = "sara-r412m", + feature = "sara-r422", + feature = "lara-r6" + )), + at_arg(position = 2, len = 64) + )] pub username: &'a str, - #[at_arg(position = 3, len = 64)] + #[cfg_attr( + any( + feature = "sara-r410m", + feature = "sara-r412m", + feature = "sara-r422", + feature = "lara-r6" + ), + at_arg(position = 3, len = 64) + )] + #[cfg_attr( + not(any( + feature = "sara-r410m", + feature = "sara-r412m", + feature = "sara-r422", + feature = "lara-r6" + )), + at_arg(position = 2, len = 64) + )] pub password: &'a str, } diff --git a/ublox-cellular/src/command/psn/responses.rs b/src/command/psn/responses.rs similarity index 94% rename from ublox-cellular/src/command/psn/responses.rs rename to src/command/psn/responses.rs index 414041a..8f0ac92 100644 --- a/ublox-cellular/src/command/psn/responses.rs +++ b/src/command/psn/responses.rs @@ -1,11 +1,11 @@ //! Responses for Packet Switched Data Services Commands use super::types::{ - EPSNetworkRegistrationStat, EPSNetworkRegistrationUrcConfig, + ContextId, EPSNetworkRegistrationStat, EPSNetworkRegistrationUrcConfig, ExtendedPSNetworkRegistrationState, ExtendedPSNetworkRegistrationUrcConfig, GPRSAttachedState, GPRSNetworkRegistrationStat, GPRSNetworkRegistrationUrcConfig, PDPContextStatus, - PacketSwitchedNetworkDataParam, PacketSwitchedParam, + PacketSwitchedNetworkDataParam, PacketSwitchedParam, ProfileId, }; -use crate::{command::network_service::types::RatAct, network::ProfileId, ContextId}; +use crate::command::network_service::types::RatAct; use atat::atat_derive::AtatResp; use heapless::String; diff --git a/ublox-cellular/src/command/psn/types.rs b/src/command/psn/types.rs similarity index 98% rename from ublox-cellular/src/command/psn/types.rs rename to src/command/psn/types.rs index dcb51b6..5491bed 100644 --- a/ublox-cellular/src/command/psn/types.rs +++ b/src/command/psn/types.rs @@ -1,7 +1,7 @@ -use crate::network::ContextId; -use atat::atat_derive::AtatEnum; -use embedded_nal::IpAddr; +use atat::atat_derive::{AtatEnum, AtatLen}; +use core::net::IpAddr; use heapless::String; +use serde::{Deserialize, Serialize}; /// Indicates the state of PDP context activation #[derive(Clone, PartialEq, Eq, AtatEnum)] @@ -645,3 +645,11 @@ pub enum EPSNetworkRegistrationStat { /// considered as attached for emergency bearer services) AttachedEmergencyOnly = 8, } + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize, AtatLen)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct ProfileId(pub u8); + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, AtatLen)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct ContextId(pub u8); diff --git a/ublox-cellular/src/command/psn/urc.rs b/src/command/psn/urc.rs similarity index 94% rename from ublox-cellular/src/command/psn/urc.rs rename to src/command/psn/urc.rs index c69198c..f5ca63b 100644 --- a/ublox-cellular/src/command/psn/urc.rs +++ b/src/command/psn/urc.rs @@ -2,9 +2,9 @@ use super::types::{ EPSNetworkRegistrationStat, ExtendedPSNetworkRegistrationState, GPRSNetworkRegistrationStat, }; -use crate::{command::network_service::types::RatAct, network::ProfileId}; +use crate::{command::network_service::types::RatAct, command::psn::types::ProfileId}; use atat::atat_derive::AtatResp; -use embedded_nal::IpAddr; +use core::net::IpAddr; use heapless::String; /// +UUPSDA diff --git a/ublox-cellular/src/command/sms/mod.rs b/src/command/sms/mod.rs similarity index 100% rename from ublox-cellular/src/command/sms/mod.rs rename to src/command/sms/mod.rs diff --git a/ublox-cellular/src/command/sms/responses.rs b/src/command/sms/responses.rs similarity index 100% rename from ublox-cellular/src/command/sms/responses.rs rename to src/command/sms/responses.rs diff --git a/ublox-cellular/src/command/sms/types.rs b/src/command/sms/types.rs similarity index 100% rename from ublox-cellular/src/command/sms/types.rs rename to src/command/sms/types.rs diff --git a/ublox-cellular/src/command/sms/urc.rs b/src/command/sms/urc.rs similarity index 100% rename from ublox-cellular/src/command/sms/urc.rs rename to src/command/sms/urc.rs diff --git a/ublox-cellular/src/command/system_features/mod.rs b/src/command/system_features/mod.rs similarity index 68% rename from ublox-cellular/src/command/system_features/mod.rs rename to src/command/system_features/mod.rs index b336945..1160e55 100644 --- a/ublox-cellular/src/command/system_features/mod.rs +++ b/src/command/system_features/mod.rs @@ -12,6 +12,37 @@ use types::{FSFactoryRestoreType, NVMFactoryRestoreType, PowerSavingMode, Second use super::NoResponse; +/// Serial interfaces configuration selection +USIO +/// +/// Selects the serial interfaces' configuration. The configuration affects how +/// an available (either physical or logical) serial interface is used, i.e. the +/// meaning of the data flowing over it. +/// +/// Possible usages are: +/// - Modem interface (AT command) +/// - Trace interface (diagnostic log) +/// - Raw interface (e.g. GPS/GNSS tunneling or SAP) +/// - Digital audio interface +/// - None +/// +/// A set of configurations, that considers all the available serial interfaces' +/// and their associated usage, is called +USIO's configuration variant. +/// +/// **NOTE** +/// - The serial interfaces' configuration switch is not performed run-time. The +/// settings are saved in NVM; the new configuration will be effective at the +/// subsequent module reboot. +/// - A serial interface might not support all the usages. For instance, UART +/// cannot be used as digital audio interface. +/// - For the complete list of allowed USIO variants supported by each series +/// modules, see Notes. +#[derive(Clone, AtatCmd)] +#[at_cmd("+USIO", NoResponse)] +pub struct SetSerialInterfaceConfig { + #[at_arg(position = 0)] + pub variant: u8, +} + /// 19.8 Power saving control (Power Saving) +UPSV /// /// Sets the UART power saving configuration, but it has a global effect on the diff --git a/ublox-cellular/src/command/system_features/responses.rs b/src/command/system_features/responses.rs similarity index 100% rename from ublox-cellular/src/command/system_features/responses.rs rename to src/command/system_features/responses.rs diff --git a/ublox-cellular/src/command/system_features/types.rs b/src/command/system_features/types.rs similarity index 100% rename from ublox-cellular/src/command/system_features/types.rs rename to src/command/system_features/types.rs diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..054431d --- /dev/null +++ b/src/config.rs @@ -0,0 +1,147 @@ +use core::convert::Infallible; +use embedded_hal::digital::{ErrorType, InputPin, OutputPin, PinState}; +use embedded_io_async::{BufRead, Read, Write}; + +use crate::{ + command::{ + control::types::BaudRate, + networking::types::EmbeddedPortFilteringMode, + psn::types::{ContextId, ProfileId}, + }, + DEFAULT_BAUD_RATE, +}; + +pub struct NoPin; + +impl ErrorType for NoPin { + type Error = core::convert::Infallible; +} + +impl InputPin for NoPin { + fn is_high(&mut self) -> Result { + Ok(true) + } + + fn is_low(&mut self) -> Result { + Ok(false) + } +} + +impl OutputPin for NoPin { + fn set_low(&mut self) -> Result<(), Self::Error> { + Ok(()) + } + + fn set_high(&mut self) -> Result<(), Self::Error> { + Ok(()) + } +} + +pub struct ReverseOutputPin>(pub P); + +impl> ErrorType for ReverseOutputPin

{ + type Error = Infallible; +} + +impl> OutputPin for ReverseOutputPin

{ + fn set_low(&mut self) -> Result<(), Self::Error> { + self.0.set_high() + } + + fn set_high(&mut self) -> Result<(), Self::Error> { + self.0.set_low() + } + + fn set_state(&mut self, state: PinState) -> Result<(), Self::Error> { + match state { + PinState::Low => self.0.set_state(PinState::High), + PinState::High => self.0.set_state(PinState::Low), + } + } +} + +pub struct ReverseInputPin>(pub P); + +impl> ErrorType for ReverseInputPin

{ + type Error = Infallible; +} + +impl> InputPin for ReverseInputPin

{ + fn is_high(&mut self) -> Result { + self.0.is_low() + } + + fn is_low(&mut self) -> Result { + self.0.is_high() + } +} + +pub trait CellularConfig<'a> { + type ResetPin: OutputPin; + type PowerPin: OutputPin; + type VintPin: InputPin; + + const AT_CONFIG: atat::Config = atat::Config::new(); + + // Transport settings + const FLOW_CONTROL: bool = false; + const BAUD_RATE: BaudRate = DEFAULT_BAUD_RATE; + + #[cfg(feature = "internal-network-stack")] + const HEX_MODE: bool = true; + + const EMBEDDED_PORT_FILTERING: EmbeddedPortFilteringMode = + EmbeddedPortFilteringMode::Enable(6000, 6200); + + const OPERATOR_FORMAT: OperatorFormat = OperatorFormat::Long; + + const PROFILE_ID: ProfileId = ProfileId(1); + const CONTEXT_ID: ContextId = ContextId(1); + + const APN: Apn<'a> = Apn::None; + + #[cfg(feature = "ppp")] + const PPP_CONFIG: embassy_net_ppp::Config<'a>; + + fn reset_pin(&mut self) -> Option<&mut Self::ResetPin> { + None + } + + fn power_pin(&mut self) -> Option<&mut Self::PowerPin> { + None + } + + fn vint_pin(&mut self) -> Option<&mut Self::VintPin> { + None + } +} + +pub trait Transport: Write + Read + BufRead { + fn set_baudrate(&mut self, baudrate: u32); + fn split_ref(&mut self) -> (impl Write, impl Read + BufRead); +} + +#[repr(u8)] +pub enum OperatorFormat { + Long = 0, + Short = 1, + Numeric = 2, +} + +#[derive(Debug, Clone)] +pub enum Apn<'a> { + None, + Given { + name: &'a str, + username: Option<&'a str>, + password: Option<&'a str>, + }, + #[cfg(any(feature = "automatic-apn"))] + Automatic, +} + +impl Default for Apn<'_> { + fn default() -> Self { + Self::None + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..d917893 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,75 @@ +use crate::command::network_service::types::Error as NetworkError; + +#[derive(Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum GenericError { + Timeout, + Clock, + Unsupported, +} + +#[derive(Debug, PartialEq)] +#[non_exhaustive] +pub enum Error { + // General device errors + BaudDetection, + SimCard, + Busy, + Uninitialized, + StateTimeout, + PoweredDown, + AttachTimeout, + ContextActivationTimeout, + InvalidStateTransition, + + // Network errors + Network(NetworkError), + + // Service specific errors + // DataService(DataServiceError), + + // Generic shared errors, e.g. from `core::` + Generic(GenericError), + + Atat(atat::Error), + + _Unknown, + + IoPin, + + SubscriberOverflow(embassy_sync::pubsub::Error), +} + +impl From for Error { + fn from(_value: embassy_time::TimeoutError) -> Self { + Error::Generic(GenericError::Timeout) + } +} + +#[cfg(feature = "defmt")] +impl defmt::Format for Error { + fn format(&self, f: defmt::Formatter<'_>) { + match self { + Self::BaudDetection => defmt::write!(f, "BaudDetection"), + Self::Busy => defmt::write!(f, "Busy"), + Self::Uninitialized => defmt::write!(f, "Uninitialized"), + Self::StateTimeout => defmt::write!(f, "StateTimeout"), + Self::PoweredDown => defmt::write!(f, "PoweredDown"), + Self::AttachTimeout => defmt::write!(f, "AttachTimeout"), + Self::ContextActivationTimeout => defmt::write!(f, "ContextActivationTimeout"), + Self::InvalidStateTransition => defmt::write!(f, "InvalidStateTransition"), + Self::Network(e) => defmt::write!(f, "Network({:?})", e), + // Self::DataService(e) => defmt::write!(f, "DataService({:?})", e), + Self::Generic(e) => defmt::write!(f, "Generic({:?})", e), + Self::Atat(e) => defmt::write!(f, "Atat({:?})", e), + Self::_Unknown => defmt::write!(f, "_Unknown"), + _ => defmt::write!(f, "non_exhaustive"), + } + } +} + +impl From for Error { + fn from(e: atat::Error) -> Self { + Self::Atat(e) + } +} diff --git a/ublox-cellular/src/fmt.rs b/src/fmt.rs similarity index 78% rename from ublox-cellular/src/fmt.rs rename to src/fmt.rs index c06793e..35b929f 100644 --- a/ublox-cellular/src/fmt.rs +++ b/src/fmt.rs @@ -1,31 +1,12 @@ -// MIT License - -// Copyright (c) 2020 Dario Nieuwenhuis - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - #![macro_use] -#![allow(unused_macros)] +#![allow(unused)] + +use core::fmt::{Debug, Display, LowerHex}; #[cfg(all(feature = "defmt", feature = "log"))] compile_error!("You may not enable both `defmt` and `log` features."); +#[collapse_debuginfo(yes)] macro_rules! assert { ($($x:tt)*) => { { @@ -37,6 +18,7 @@ macro_rules! assert { }; } +#[collapse_debuginfo(yes)] macro_rules! assert_eq { ($($x:tt)*) => { { @@ -48,6 +30,7 @@ macro_rules! assert_eq { }; } +#[collapse_debuginfo(yes)] macro_rules! assert_ne { ($($x:tt)*) => { { @@ -59,6 +42,7 @@ macro_rules! assert_ne { }; } +#[collapse_debuginfo(yes)] macro_rules! debug_assert { ($($x:tt)*) => { { @@ -70,6 +54,7 @@ macro_rules! debug_assert { }; } +#[collapse_debuginfo(yes)] macro_rules! debug_assert_eq { ($($x:tt)*) => { { @@ -81,6 +66,7 @@ macro_rules! debug_assert_eq { }; } +#[collapse_debuginfo(yes)] macro_rules! debug_assert_ne { ($($x:tt)*) => { { @@ -92,6 +78,7 @@ macro_rules! debug_assert_ne { }; } +#[collapse_debuginfo(yes)] macro_rules! todo { ($($x:tt)*) => { { @@ -103,17 +90,23 @@ macro_rules! todo { }; } +#[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] macro_rules! unreachable { ($($x:tt)*) => { - { - #[cfg(not(feature = "defmt"))] - ::core::unreachable!($($x)*); - #[cfg(feature = "defmt")] - ::defmt::unreachable!($($x)*); - } + ::core::unreachable!($($x)*) }; } +#[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] +macro_rules! unreachable { + ($($x:tt)*) => { + ::defmt::unreachable!($($x)*) + }; +} + +#[collapse_debuginfo(yes)] macro_rules! panic { ($($x:tt)*) => { { @@ -125,6 +118,7 @@ macro_rules! panic { }; } +#[collapse_debuginfo(yes)] macro_rules! trace { ($s:literal $(, $x:expr)* $(,)?) => { { @@ -138,6 +132,7 @@ macro_rules! trace { }; } +#[collapse_debuginfo(yes)] macro_rules! debug { ($s:literal $(, $x:expr)* $(,)?) => { { @@ -151,6 +146,7 @@ macro_rules! debug { }; } +#[collapse_debuginfo(yes)] macro_rules! info { ($s:literal $(, $x:expr)* $(,)?) => { { @@ -164,6 +160,7 @@ macro_rules! info { }; } +#[collapse_debuginfo(yes)] macro_rules! warn { ($s:literal $(, $x:expr)* $(,)?) => { { @@ -177,6 +174,7 @@ macro_rules! warn { }; } +#[collapse_debuginfo(yes)] macro_rules! error { ($s:literal $(, $x:expr)* $(,)?) => { { @@ -191,6 +189,7 @@ macro_rules! error { } #[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] macro_rules! unwrap { ($($x:tt)*) => { ::defmt::unwrap!($($x)*) @@ -198,6 +197,7 @@ macro_rules! unwrap { } #[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] macro_rules! unwrap { ($arg:expr) => { match $crate::fmt::Try::into_result($arg) { @@ -245,3 +245,30 @@ impl Try for Result { self } } + +pub(crate) struct Bytes<'a>(pub &'a [u8]); + +impl<'a> Debug for Bytes<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +impl<'a> Display for Bytes<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +impl<'a> LowerHex for Bytes<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +#[cfg(feature = "defmt")] +impl<'a> defmt::Format for Bytes<'a> { + fn format(&self, fmt: defmt::Formatter) { + defmt::write!(fmt, "{:02x}", self.0) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..528168c --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,19 @@ +#![cfg_attr(not(test), no_std)] +#![allow(async_fn_in_trait)] + +#[cfg(all(feature = "ppp", feature = "internal-network-stack"))] +compile_error!("You may not enable both `ppp` and `internal-network-stack` features."); + +// This mod MUST go first, so that the others see its macros. +pub(crate) mod fmt; + +pub mod command; +pub mod config; +pub mod error; +mod modules; +mod registration; + +pub mod asynch; + +use command::control::types::BaudRate; +pub const DEFAULT_BAUD_RATE: BaudRate = BaudRate::B115200; diff --git a/src/modules/lara_r6.rs b/src/modules/lara_r6.rs new file mode 100644 index 0000000..fa92d81 --- /dev/null +++ b/src/modules/lara_r6.rs @@ -0,0 +1,30 @@ +use super::ModuleParams; +use embassy_time::Duration; + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct LaraR6; + +impl ModuleParams for LaraR6 { + fn power_on_pull_time(&self) -> Option { + Some(Duration::from_millis(300)) + } + fn power_off_pull_time(&self) -> Duration { + Duration::from_millis(2000) + } + fn boot_wait(&self) -> Duration { + Duration::from_secs(10) + } + fn reboot_command_wait(&self) -> Duration { + Duration::from_secs(10) + } + fn command_delay_default(&self) -> Duration { + Duration::from_millis(20) + } + fn reset_hold(&self) -> Duration { + Duration::from_millis(150) + } + fn max_num_simultaneous_rats(&self) -> u8 { + 3 + } +} diff --git a/src/modules/lena_r8.rs b/src/modules/lena_r8.rs new file mode 100644 index 0000000..48e2cbe --- /dev/null +++ b/src/modules/lena_r8.rs @@ -0,0 +1,28 @@ +use super::ModuleParams; +use crate::command::mobile_control::types::Functionality; +use embassy_time::Duration; + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct LenaR8; + +impl ModuleParams for LenaR8 { + fn power_on_pull_time(&self) -> Duration { + Duration::from_millis(2000) + } + fn power_off_pull_time(&self) -> Duration { + Duration::from_millis(3100) + } + fn command_delay_default(&self) -> Duration { + Duration::from_millis(20) + } + fn reset_hold(&self) -> Duration { + Duration::from_millis(50) + } + fn max_num_simultaneous_rats(&self) -> u8 { + 2 + } + fn at_c_fun_reboot_command(&self) -> Functionality { + Functionality::SilentResetWithSimReset + } +} diff --git a/src/modules/mod.rs b/src/modules/mod.rs new file mode 100644 index 0000000..ded0ea0 --- /dev/null +++ b/src/modules/mod.rs @@ -0,0 +1,184 @@ +#[cfg(any(feature = "any-module", feature = "lara-r6"))] +pub(crate) mod lara_r6; +#[cfg(any(feature = "any-module", feature = "lena-r8"))] +pub(crate) mod lena_r8; +#[cfg(any(feature = "any-module", feature = "sara-r410m"))] +pub(crate) mod sara_r410m; +#[cfg(any(feature = "any-module", feature = "sara-r412m"))] +pub(crate) mod sara_r412m; +#[cfg(any(feature = "any-module", feature = "sara-r422"))] +pub(crate) mod sara_r422; +#[cfg(any(feature = "any-module", feature = "sara-r5"))] +pub(crate) mod sara_r5; +#[cfg(any(feature = "any-module", feature = "sara-u201"))] +pub(crate) mod sara_u201; +#[cfg(any(feature = "any-module", feature = "toby-r2"))] +pub(crate) mod toby_r2; + +use crate::command::{general::responses::ModelId, mobile_control::types::Functionality}; +use embassy_time::Duration; + +pub trait ModuleParams: Copy { + /// The time for which PWR_ON must be pulled down to effect power-on + fn power_on_pull_time(&self) -> Option { + None + } + + /// The time for which PWR_ON must be pulled down to effect power-off + fn power_off_pull_time(&self) -> Duration { + Duration::from_millis(3100) + } + + /// How long to wait before the module is ready after boot + fn boot_wait(&self) -> Duration { + Duration::from_secs(5) + } + + /// How long to wait for a organised power-down in the ansence of VInt + fn power_down_wait(&self) -> Duration { + Duration::from_secs(35) + } + + /// How long to wait before the module is ready after it has been commanded + /// to reboot + fn reboot_command_wait(&self) -> Duration { + Duration::from_secs(5) + } + + /// How long to wait between the end of one AT command and the start of the + /// next, default value + fn command_delay_default(&self) -> Duration { + Duration::from_millis(100) + } + + /// The type of AT+CFUN state to use to switch the radio off: either 0 for + /// truly off or 4 for "airplane" mode + fn radio_off_cfun(&self) -> Functionality { + Functionality::AirplaneMode + } + + /// How long the reset line has to be held for to reset the cellular module + fn reset_hold(&self) -> Duration { + Duration::from_millis(16500) + } + + /// The maximum number of simultaneous RATs that are supported by the + /// cellular module + fn max_num_simultaneous_rats(&self) -> u8 { + 1 + } + + /// Normally 15, but in some cases 16 + fn at_c_fun_reboot_command(&self) -> Functionality { + Functionality::SilentReset + } +} + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub(crate) enum Module { + #[cfg(any(feature = "any-module", feature = "lara-r6"))] + LaraR6(lara_r6::LaraR6), + #[cfg(any(feature = "any-module", feature = "lena-r8"))] + LenaR8(lena_r8::LenaR8), + #[cfg(any(feature = "any-module", feature = "sara-r410m"))] + SaraR410m(sara_r410m::SaraR410m), + #[cfg(any(feature = "any-module", feature = "sara-r412m"))] + SaraR412m(sara_r412m::SaraR412m), + #[cfg(any(feature = "any-module", feature = "sara-r422"))] + SaraR422(sara_r422::SaraR422), + #[cfg(any(feature = "any-module", feature = "sara-r5"))] + SaraR5(sara_r5::SaraR5), + #[cfg(any(feature = "any-module", feature = "sara-u201"))] + SaraU201(sara_u201::SaraU201), + #[cfg(any(feature = "any-module", feature = "toby-r2"))] + TobyR2(toby_r2::TobyR2), + Generic(Generic), +} + +impl Module { + pub fn from_model_id(model_id: &ModelId) -> Self { + match model_id.model.as_slice() { + b"LARA-R6001D" => Self::LaraR6(lara_r6::LaraR6), + id => { + #[cfg(feature = "defmt")] + warn!("Attempting to run {=[u8]:a} using generic module parameters! This may or may not work.", id); + #[cfg(feature = "log")] + warn!("Attempting to run {:?} using generic module parameters! This may or may not work.", id); + Self::Generic(Generic) + } + } + } +} + +macro_rules! inner { + ($self: ident, $fn: ident) => { + match $self { + #[cfg(any(feature = "any-module", feature = "lara-r6"))] + Self::LaraR6(inner) => inner.$fn(), + #[cfg(any(feature = "any-module", feature = "lena-r8"))] + Self::LenaR8(inner) => inner.$fn(), + #[cfg(any(feature = "any-module", feature = "sara-r410m"))] + Self::SaraR410m(inner) => inner.$fn(), + #[cfg(any(feature = "any-module", feature = "sara-r412m"))] + Self::SaraR412m(inner) => inner.$fn(), + #[cfg(any(feature = "any-module", feature = "sara-r422"))] + Self::SaraR422(inner) => inner.$fn(), + #[cfg(any(feature = "any-module", feature = "sara-r5"))] + Self::SaraR5(inner) => inner.$fn(), + #[cfg(any(feature = "any-module", feature = "sara-u201"))] + Self::SaraU201(inner) => inner.$fn(), + #[cfg(any(feature = "any-module", feature = "toby-r2"))] + Self::TobyR2(inner) => inner.$fn(), + Self::Generic(inner) => inner.$fn(), + } + }; +} + +impl ModuleParams for Module { + fn power_on_pull_time(&self) -> Option { + inner!(self, power_on_pull_time) + } + + fn power_off_pull_time(&self) -> Duration { + inner!(self, power_off_pull_time) + } + + fn boot_wait(&self) -> Duration { + inner!(self, boot_wait) + } + + fn power_down_wait(&self) -> Duration { + inner!(self, power_down_wait) + } + + fn reboot_command_wait(&self) -> Duration { + inner!(self, reboot_command_wait) + } + + fn command_delay_default(&self) -> Duration { + inner!(self, command_delay_default) + } + + fn radio_off_cfun(&self) -> Functionality { + inner!(self, radio_off_cfun) + } + + fn reset_hold(&self) -> Duration { + inner!(self, reset_hold) + } + + fn max_num_simultaneous_rats(&self) -> u8 { + inner!(self, max_num_simultaneous_rats) + } + + fn at_c_fun_reboot_command(&self) -> Functionality { + inner!(self, at_c_fun_reboot_command) + } +} + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Generic; + +impl ModuleParams for Generic {} diff --git a/src/modules/sara_r410m.rs b/src/modules/sara_r410m.rs new file mode 100644 index 0000000..58b3c91 --- /dev/null +++ b/src/modules/sara_r410m.rs @@ -0,0 +1,22 @@ +use super::ModuleParams; +use crate::command::mobile_control::types::Functionality; +use embassy_time::Duration; + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct SaraR410m; + +impl ModuleParams for SaraR410m { + fn power_on_pull_time(&self) -> Option { + Some(Duration::from_millis(300)) + } + fn power_off_pull_time(&self) -> Duration { + Duration::from_millis(2000) + } + fn boot_wait(&self) -> Duration { + Duration::from_secs(6) + } + fn max_num_simultaneous_rats(&self) -> u8 { + 2 + } +} diff --git a/src/modules/sara_r412m.rs b/src/modules/sara_r412m.rs new file mode 100644 index 0000000..1b107a9 --- /dev/null +++ b/src/modules/sara_r412m.rs @@ -0,0 +1,29 @@ +use super::ModuleParams; +use crate::command::mobile_control::types::Functionality; +use embassy_time::Duration; + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct SaraR412m; + +impl ModuleParams for SaraR412m { + fn power_on_pull_time(&self) -> Option { + Some(Duration::from_millis(300)) + } + + fn power_off_pull_time(&self) -> Duration { + Duration::from_millis(2000) + } + fn boot_wait(&self) -> Duration { + Duration::from_secs(6) + } + fn reboot_command_wait(&self) -> Duration { + Duration::from_secs(10) + } + fn max_num_simultaneous_rats(&self) -> u8 { + 3 + } + fn at_c_fun_reboot_command(&self) -> Functionality { + Functionality::SilentReset + } +} diff --git a/src/modules/sara_r422.rs b/src/modules/sara_r422.rs new file mode 100644 index 0000000..7c8ec57 --- /dev/null +++ b/src/modules/sara_r422.rs @@ -0,0 +1,25 @@ +use super::ModuleParams; +use crate::command::mobile_control::types::Functionality; +use embassy_time::Duration; + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct SaraR422; + +impl ModuleParams for SaraR422 { + fn power_on_pull_time(&self) -> Duration { + Duration::from_millis(300) + } + fn power_off_pull_time(&self) -> Duration { + Duration::from_millis(2000) + } + fn reboot_command_wait(&self) -> Duration { + Duration::from_secs(10) + } + fn command_delay_default(&self) -> Duration { + Duration::from_millis(20) + } + fn max_num_simultaneous_rats(&self) -> u8 { + 3 + } +} diff --git a/src/modules/sara_r5.rs b/src/modules/sara_r5.rs new file mode 100644 index 0000000..ee9a4d9 --- /dev/null +++ b/src/modules/sara_r5.rs @@ -0,0 +1,34 @@ +use super::ModuleParams; +use crate::command::mobile_control::types::Functionality; +use embassy_time::Duration; + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct SaraR5; + +impl ModuleParams for SaraR5 { + fn power_on_pull_time(&self) -> Option { + Some(Duration::from_millis(1500)) + } + fn power_off_pull_time(&self) -> Duration { + Duration::from_millis(2000) + } + fn boot_wait(&self) -> Duration { + Duration::from_secs(6) + } + fn power_down_wait(&self) -> Duration { + Duration::from_secs(20) + } + fn reboot_command_wait(&self) -> Duration { + Duration::from_secs(15) + } + fn command_delay_default(&self) -> Duration { + Duration::from_millis(20) + } + fn reset_hold(&self) -> Duration { + Duration::from_millis(150) + } + fn at_c_fun_reboot_command(&self) -> Functionality { + Functionality::SilentResetWithSimReset + } +} diff --git a/src/modules/sara_u201.rs b/src/modules/sara_u201.rs new file mode 100644 index 0000000..4a14bd9 --- /dev/null +++ b/src/modules/sara_u201.rs @@ -0,0 +1,28 @@ +use super::ModuleParams; +use crate::command::mobile_control::types::Functionality; +use embassy_time::Duration; + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct SaraU201; + +impl ModuleParams for SaraU201 { + fn power_on_pull_time(&self) -> Duration { + Duration::from_millis(1) + } + fn power_off_pull_time(&self) -> Duration { + Duration::from_millis(1500) + } + fn power_down_wait(&self) -> Duration { + Duration::from_secs(5) + } + fn command_delay_default(&self) -> Duration { + Duration::from_millis(20) + } + fn reset_hold(&self) -> Duration { + Duration::from_millis(75) + } + fn max_num_simultaneous_rats(&self) -> u8 { + 2 + } +} diff --git a/src/modules/toby_r2.rs b/src/modules/toby_r2.rs new file mode 100644 index 0000000..a2e2e89 --- /dev/null +++ b/src/modules/toby_r2.rs @@ -0,0 +1,26 @@ +use super::ModuleParams; +use crate::command::mobile_control::types::Functionality; +use embassy_time::Duration; + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct TobyR2; + +impl ModuleParams for TobyR2 { + fn power_on_pull_time(&self) -> Option { + Some(Duration::from_micros(50)) + } + fn power_off_pull_time(&self) -> Duration { + Duration::from_millis(1000) + } + fn command_delay_default(&self) -> Duration { + Duration::from_millis(20) + } + fn reset_hold(&self) -> Duration { + Duration::from_millis(50) + } + fn max_num_simultaneous_rats(&self) -> u8 { + // TODO: Is this correct? + 3 + } +} diff --git a/ublox-cellular/src/registration.rs b/src/registration.rs similarity index 65% rename from ublox-cellular/src/registration.rs rename to src/registration.rs index 253c112..f508c20 100644 --- a/ublox-cellular/src/registration.rs +++ b/src/registration.rs @@ -2,12 +2,12 @@ use crate::command::{ network_service::{ responses::NetworkRegistrationStatus, types::{NetworkRegistrationStat, RatAct}, - // urc::NetworkRegistration, + urc::NetworkRegistration, }, psn::{ responses::{EPSNetworkRegistrationStatus, GPRSNetworkRegistrationStatus}, types::{EPSNetworkRegistrationStat, GPRSNetworkRegistrationStat}, - // urc::{EPSNetworkRegistration, GPRSNetworkRegistration}, + urc::{EPSNetworkRegistration, GPRSNetworkRegistration}, }, }; use embassy_time::{Duration, Instant}; @@ -21,9 +21,9 @@ pub struct CellularRegistrationStatus { } impl CellularRegistrationStatus { - pub fn new() -> Self { + pub const fn new() -> Self { Self { - status: Status::default(), + status: Status::None, updated: None, started: None, } @@ -78,31 +78,26 @@ impl From for Status { 1 => Self::Home, 2 => Self::Searching, 3 => Self::Denied, - 4 => Self::Unknown, + 4 => Self::OutOfCoverage, 5 => Self::Roaming, _ => Self::None, } } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum Status { + #[default] None, NotRegistering, Home, Searching, Denied, - Unknown, + OutOfCoverage, Roaming, } -impl Default for Status { - fn default() -> Self { - Self::None - } -} - /// Convert the 3GPP registration status from a CREG URC to [`RegistrationStatus`]. impl From for Status { fn from(v: NetworkRegistrationStat) -> Self { @@ -124,6 +119,17 @@ impl From for Status { } } +#[cfg(not(feature = "use-upsd-context-activation"))] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ProfileState { + #[default] + Unknown, + ShouldBeUp, + RequiresReactivation, + ShouldBeDown, +} + #[derive(Debug, Default)] pub struct RegistrationParams { reg_type: RegType, @@ -132,25 +138,18 @@ pub struct RegistrationParams { cell_id: Option>, lac: Option>, - // active_time: Option, - // periodic_tau: Option, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Default, Clone, Copy)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum RegType { Creg, Cgreg, Cereg, + #[default] Unknown, } -impl Default for RegType { - fn default() -> Self { - Self::Unknown - } -} - impl From for RegType { fn from(ran: RadioAccessNetwork) -> Self { match ran { @@ -185,13 +184,17 @@ pub struct CellularGlobalIdentity { // tac: u8, } +impl CellularGlobalIdentity { + pub const fn new() -> Self { + Self { + cell_id: None, + lac: None, + } + } +} + #[derive(Debug, Clone)] pub struct RegistrationState { - pub(crate) reg_check_time: Option, - pub(crate) reg_start_time: Option, - pub(crate) imsi_check_time: Option, - - pub(crate) conn_state: ConnectionState, /// CSD (Circuit Switched Data) registration status (registered/searching/roaming etc.). pub(crate) csd: CellularRegistrationStatus, /// PSD (Packet Switched Data) registration status (registered/searching/roaming etc.). @@ -199,88 +202,54 @@ pub struct RegistrationState { /// EPS (Evolved Packet Switched) registration status (registered/searching/roaming etc.). pub(crate) eps: CellularRegistrationStatus, - pub(crate) registration_interventions: u8, - check_imsi: bool, - pub(crate) cgi: CellularGlobalIdentity, - // Radio Access Technology (RAT) - // pub(crate) act: RatAct, -} -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum ConnectionState { - Disconnected, - Connecting, - Connected, + #[cfg(not(feature = "use-upsd-context-activation"))] + pub(crate) profile_state: ProfileState, } -impl Default for ConnectionState { +impl Default for RegistrationState { fn default() -> Self { - Self::Disconnected + Self::new() } } impl RegistrationState { - pub fn new() -> Self { + pub const fn new() -> Self { Self { - reg_check_time: None, - reg_start_time: None, - imsi_check_time: None, - - conn_state: ConnectionState::Disconnected, csd: CellularRegistrationStatus::new(), psd: CellularRegistrationStatus::new(), eps: CellularRegistrationStatus::new(), - registration_interventions: 1, - check_imsi: false, + cgi: CellularGlobalIdentity::new(), - cgi: CellularGlobalIdentity::default(), - // act: RatAct::default(), + #[cfg(not(feature = "use-upsd-context-activation"))] + profile_state: ProfileState::Unknown, } } + /// Determine if a given cellular network status value means that we're + /// registered with the network. + pub fn is_registered(&self) -> bool { + // If PSD or EPS are registered, we are connected! + self.psd.registered() || self.eps.registered() + } + pub fn reset(&mut self) { self.csd.reset(); self.psd.reset(); self.eps.reset(); - self.reg_start_time = Some(Instant::now()); - self.reg_check_time = Some(Instant::now()); - self.imsi_check_time = None; - self.registration_interventions = 1; - } - - pub fn set_connection_state(&mut self, state: ConnectionState) { - if self.conn_state == state { - return; - } - - trace!("Connection state changed to \"{:?}\"", state); - self.conn_state = state; } pub fn compare_and_set(&mut self, new_params: RegistrationParams) { match new_params.reg_type { RegType::Creg => { - let prev_reg_status = self.csd.registered(); self.csd.set_status(new_params.status); - if !prev_reg_status && self.csd.registered() { - self.check_imsi = true - } } RegType::Cgreg => { - let prev_reg_status = self.psd.registered(); self.psd.set_status(new_params.status); - if !prev_reg_status && self.psd.registered() { - self.check_imsi = true - } } RegType::Cereg => { - let prev_reg_status = self.eps.registered(); self.eps.set_status(new_params.status); - if !prev_reg_status && self.eps.registered() { - self.check_imsi = true - } } RegType::Unknown => { error!("unknown reg type"); @@ -296,19 +265,17 @@ impl RegistrationState { } } -// impl From for RegistrationParams { -// fn from(v: NetworkRegistration) -> Self { -// Self { -// act: RatAct::Gsm, -// reg_type: RegType::Creg, -// status: v.stat.into(), -// cell_id: None, -// lac: None, -// // active_time: None, -// // periodic_tau: None, -// } -// } -// } +impl From for RegistrationParams { + fn from(v: NetworkRegistration) -> Self { + Self { + act: RatAct::Gsm, + reg_type: RegType::Creg, + status: v.stat.into(), + cell_id: None, + lac: None, + } + } +} impl From for RegistrationParams { fn from(v: NetworkRegistrationStatus) -> Self { @@ -318,25 +285,21 @@ impl From for RegistrationParams { status: v.stat.into(), cell_id: None, lac: None, - // active_time: None, - // periodic_tau: None, } } } -// impl From for RegistrationParams { -// fn from(v: GPRSNetworkRegistration) -> Self { -// Self { -// act: v.act.unwrap_or(RatAct::Unknown), -// reg_type: RegType::Cgreg, -// status: v.stat.into(), -// cell_id: v.ci, -// lac: v.lac, -// // active_time: None, -// // periodic_tau: None, -// } -// } -// } +impl From for RegistrationParams { + fn from(v: GPRSNetworkRegistration) -> Self { + Self { + act: v.act.unwrap_or(RatAct::GsmGprsEdge), + reg_type: RegType::Cgreg, + status: v.stat.into(), + cell_id: v.ci, + lac: v.lac, + } + } +} impl From for RegistrationParams { fn from(v: GPRSNetworkRegistrationStatus) -> Self { @@ -345,26 +308,22 @@ impl From for RegistrationParams { status: v.stat.into(), cell_id: v.ci, lac: v.lac, - act: v.act.unwrap_or(RatAct::Unknown), - // active_time: None, - // periodic_tau: None, + act: v.act.unwrap_or(RatAct::GsmGprsEdge), } } } -// impl From for RegistrationParams { -// fn from(v: EPSNetworkRegistration) -> Self { -// Self { -// reg_type: RegType::Cereg, -// status: v.stat.into(), -// cell_id: v.ci, -// lac: v.tac, -// act: v.act.unwrap_or(RatAct::Unknown), -// // active_time: None, -// // periodic_tau: None, -// } -// } -// } +impl From for RegistrationParams { + fn from(v: EPSNetworkRegistration) -> Self { + Self { + reg_type: RegType::Cereg, + status: v.stat.into(), + cell_id: v.ci, + lac: v.tac, + act: v.act.unwrap_or(RatAct::Lte), + } + } +} impl From for RegistrationParams { fn from(v: EPSNetworkRegistrationStatus) -> Self { @@ -373,9 +332,7 @@ impl From for RegistrationParams { status: v.stat.into(), cell_id: v.ci, lac: v.tac, - act: v.act.unwrap_or(RatAct::Unknown), - // active_time: None, - // periodic_tau: None, + act: v.act.unwrap_or(RatAct::Lte), } } } diff --git a/ublox-cellular/Cargo.toml b/ublox-cellular/Cargo.toml deleted file mode 100644 index f41cf06..0000000 --- a/ublox-cellular/Cargo.toml +++ /dev/null @@ -1,74 +0,0 @@ -[package] -name = "ublox-cellular-rs" -version = "0.4.0" -authors = ["Mathias Koch "] -description = "Driver crate for u-blox cellular devices, implementation follows 'UBX-13002752 - R65'" -readme = "../README.md" -keywords = ["arm", "cortex-m", "ublox", "cellular", "embedded-hal-driver"] -categories = ["embedded", "no-std"] -license = "MIT OR Apache-2.0" -repository = "https://github.com/BlackbirdHQ/ublox-cellular-rs" -edition = "2021" - -[lib] -name = "ublox_cellular" -doctest = false - -[dependencies] -# atat = { version = "0.18", features = ["derive", "bytes"] } -atat = { git = "https://github.com/BlackbirdHQ/atat", rev = "c5caaf7", features = [ - "derive", - "defmt", - "bytes", -] } -embedded-hal = "=1.0.0-rc.1" -embedded-nal = "0.6" -hash32 = "^0.2.1" -hash32-derive = "^0.1.0" -heapless = { version = "^0.7", features = ["serde"] } -nb = "^1" -serde = { version = "^1", default-features = false, features = ["derive"] } -# ublox-sockets = "0.5.0" -ublox-sockets = { git = "https://github.com/BlackbirdHQ/ublox-sockets", rev = "b1ff942" } -embassy-time = "0.1" -embedded-io = "0.5" - -# Enable `serde` feature of `no-std-net` -no-std-net = { version = "^0.5", features = ["serde"] } - -log = { version = "^0.4", default-features = false, optional = true } -defmt = { version = "^0.3", optional = true } - -[features] -default = ["socket-udp", "socket-tcp"] - -async = ["atat/async"] - -# Use `defmt-impl to enable defmt based logging -defmt-impl = [ - "defmt", - "ublox-sockets/defmt", - "atat/defmt", - "heapless/defmt-impl", -] -# Use `log-impl` to enable log based logging -log-impl = ["log", "ublox-sockets/log", "atat/log"] - -lara-r2 = [] -lara-r6 = [] -leon-g1 = [] -lisa-u2 = [] -mpci-l2 = [] -sara-g3 = [] -sara-g4 = [] -sara-r5 = ["upsd-context-activation"] -sara-u1 = [] -sara-u2 = ["upsd-context-activation"] -toby-l2 = [] -toby-r2 = [] -toby-l4 = [] - -upsd-context-activation = [] - -socket-tcp = ["ublox-sockets/socket-tcp"] -socket-udp = ["ublox-sockets/socket-udp"] diff --git a/ublox-cellular/src/blocking_timer.rs b/ublox-cellular/src/blocking_timer.rs deleted file mode 100644 index 15be27b..0000000 --- a/ublox-cellular/src/blocking_timer.rs +++ /dev/null @@ -1,21 +0,0 @@ -use embassy_time::{Duration, Instant}; - -pub struct BlockingTimer { - expires_at: Instant, -} - -impl BlockingTimer { - pub fn after(duration: Duration) -> Self { - Self { - expires_at: Instant::now() + duration, - } - } - - pub fn wait(self) { - loop { - if self.expires_at <= Instant::now() { - break; - } - } - } -} diff --git a/ublox-cellular/src/client.rs b/ublox-cellular/src/client.rs deleted file mode 100644 index b296965..0000000 --- a/ublox-cellular/src/client.rs +++ /dev/null @@ -1,585 +0,0 @@ -use atat::{blocking::AtatClient, AtatUrcChannel, UrcSubscription}; -use embassy_time::Duration; -use ublox_sockets::SocketSet; - -use crate::{ - blocking_timer::BlockingTimer, - command::device_lock::{responses::PinStatus, types::PinStatusCode, GetPinStatus}, - command::{ - control::{ - types::{Circuit108Behaviour, Circuit109Behaviour, FlowControl}, - SetCircuit108Behaviour, SetCircuit109Behaviour, SetFlowControl, - }, - ip_transport_layer, - mobile_control::{ - types::{AutomaticTimezone, Functionality, ResetMode, TerminationErrorMode}, - SetAutomaticTimezoneUpdate, SetModuleFunctionality, SetReportMobileTerminationError, - }, - network_service, psn, - system_features::{types::PowerSavingMode, SetPowerSavingControl}, - Urc, - }, - command::{ - general::{GetCCID, GetFirmwareVersion, GetModelId}, - gpio::{ - types::{GpioInPull, GpioMode, GpioOutValue}, - SetGpioConfiguration, - }, - network_service::{ - responses::{OperatorSelection, SignalQuality}, - types::OperatorSelectionMode, - GetOperatorSelection, GetSignalQuality, SetOperatorSelection, - }, - psn::{types::PSEventReportingMode, SetPacketSwitchedEventReporting}, - }, - config::CellularConfig, - error::{Error, GenericError}, - network::{AtTx, Network}, - power::PowerState, - registration::ConnectionState, - services::data::ContextState, - UbloxCellularBuffers, UbloxCellularIngress, UbloxCellularUrcChannel, -}; -use ip_transport_layer::{types::HexMode, SetHexMode}; -use network_service::{types::NetworkRegistrationUrcConfig, SetNetworkRegistrationStatus}; -use psn::{ - types::{EPSNetworkRegistrationUrcConfig, GPRSNetworkRegistrationUrcConfig}, - SetEPSNetworkRegistrationStatus, SetGPRSNetworkRegistrationStatus, -}; - -pub(crate) const URC_CAPACITY: usize = 3; -pub(crate) const URC_SUBSCRIBERS: usize = 2; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum State { - /// Device is off - Off, - /// Device is able to respond to AT commands - AtInitialized, - /// Device is fully initialized - FullyInitialized, -} - -pub struct Device<'buf, 'sub, AtCl, AtUrcCh, Config, const N: usize, const L: usize> { - pub(crate) config: Config, - pub(crate) network: Network<'sub, AtCl>, - urc_channel: &'buf AtUrcCh, - urc_subscription: UrcSubscription<'sub, Urc, URC_CAPACITY, URC_SUBSCRIBERS>, - - pub(crate) state: State, - pub(crate) power_state: PowerState, - // Ublox devices can hold a maximum of 6 active sockets - pub(crate) sockets: Option<&'static mut SocketSet>, -} - -impl<'buf, 'sub, W, Config, const INGRESS_BUF_SIZE: usize, const N: usize, const L: usize> - Device< - 'buf, - 'sub, - atat::blocking::Client<'buf, W, INGRESS_BUF_SIZE>, - UbloxCellularUrcChannel, - Config, - N, - L, - > -where - 'buf: 'sub, - W: embedded_io::Write, - Config: CellularConfig, -{ - /// Create new u-blox device - /// - /// Look for [`data_service`](Device::data_service) how to handle data connection automatically. - /// - pub fn from_buffers( - buffers: &'buf UbloxCellularBuffers, - tx: W, - config: Config, - ) -> (UbloxCellularIngress, Self) { - let (ingress, client) = buffers.split_blocking( - tx, - atat::DefaultDigester::::default(), - atat::Config::default(), - ); - - (ingress, Device::new(client, &buffers.urc_channel, config)) - } -} - -impl<'buf, 'sub, AtCl, AtUrcCh, Config, const N: usize, const L: usize> - Device<'buf, 'sub, AtCl, AtUrcCh, Config, N, L> -where - 'buf: 'sub, - AtCl: AtatClient, - AtUrcCh: AtatUrcChannel, - Config: CellularConfig, -{ - pub fn new(client: AtCl, urc_channel: &'buf AtUrcCh, config: Config) -> Self { - let network_urc_subscription = urc_channel.subscribe().unwrap(); - Self { - config, - network: Network::new(AtTx::new(client, network_urc_subscription)), - state: State::Off, - power_state: PowerState::Off, - sockets: None, - urc_channel, - urc_subscription: urc_channel.subscribe().unwrap(), - } - } -} - -impl<'buf, 'sub, AtCl, AtUrcCh, Config, const N: usize, const L: usize> - Device<'buf, 'sub, AtCl, AtUrcCh, Config, N, L> -where - 'buf: 'sub, - AtCl: AtatClient, - Config: CellularConfig, -{ - /// Set storage for TCP/UDP sockets - /// - /// # Examples - /// - /// ```ignore - /// use ublox_cellular::sockets::SocketSet; - /// - /// const MAX_SOCKET_COUNT: usize = 1; - /// const SOCKET_RING_BUFFER_LEN: usize = 1024; - /// - /// static mut SOCKET_SET: Option> = None; - /// - /// unsafe { - /// SOCKET_SET = Some(SocketSet::new()); - /// } - /// - /// modem.set_socket_storage(unsafe { SOCKET_SET.as_mut().unwrap() }); - /// ``` - pub fn set_socket_storage(&mut self, socket_set: &'static mut SocketSet) { - socket_set.prune(); - self.sockets.replace(socket_set); - } - - pub fn take_socket_storage(&mut self) -> Option<&'static mut SocketSet> { - self.sockets.take() - } - - pub fn signal_strength(&mut self) -> Result { - self.send_at(&GetSignalQuality) - } - /// Run modem state machine - /// - /// Turns on modem if needed and processes URCs. - /// Typically it is not needed to use it directly. However it can be useful for manually handling network connections. - /// For fully automatic data connection handling use [`data_service`](Device::data_service). - /// - /// This must be called periodically in a loop. - pub fn spin(&mut self) -> nb::Result<(), Error> { - let res = self.initialize(); - - self.process_events().map_err(Error::from)?; - - res?; - - if self.network.is_connected().map_err(Error::from)? - && self.state == State::FullyInitialized - { - Ok(()) - } else { - // Reset context state if data connection is lost (This will act as a safeguard if a URC is missed) - if self.network.context_state == ContextState::Active { - self.network.context_state = ContextState::Activating; - } - Err(nb::Error::WouldBlock) - } - } - - /// Setup only essential settings to use AT commands - /// - /// Nornally this is not used and AT interface is setup in [`initialize`](Device::initialize). - /// However it is useful if you want to send some AT commands before modem is fully initialized. - /// After this [`send_at`](Device::send_at) can be used. - pub fn setup_at_commands(&mut self) -> Result<(), Error> { - // Always re-configure the PDP contexts if we reconfigure the module - self.network.context_state = ContextState::Setup; - - self.clear_buffers()?; - - self.power_on()?; - - // At this point, if is_alive fails, the configured Baud rate is probably wrong - if let Err(e) = self.is_alive(5).map_err(|_| Error::BaudDetection) { - if self.hard_reset().is_err() { - self.hard_power_off()?; - BlockingTimer::after(Duration::from_secs(1)).wait(); - } - return Err(e); - } - - // Extended errors on - self.network.send_internal( - &SetReportMobileTerminationError { - n: TerminationErrorMode::Enabled, - }, - false, - )?; - - // Select SIM - self.network.send_internal( - &SetGpioConfiguration { - gpio_id: 25, - gpio_mode: GpioMode::Output(GpioOutValue::High), - }, - false, - )?; - - #[cfg(any(feature = "lara-r6"))] - self.network.send_internal( - &SetGpioConfiguration { - gpio_id: 42, - gpio_mode: GpioMode::Input(GpioInPull::NoPull), - }, - false, - )?; - - self.network.send_internal(&GetModelId, false)?; - - // self.network.send_internal( - // &IdentificationInformation { - // n: 9 - // }, - // false, - // )?; - - self.network.send_internal(&GetFirmwareVersion, false)?; - - self.select_sim_card()?; - - self.network.send_internal(&GetCCID, false)?; - - // DCD circuit (109) changes in accordance with the carrier - self.network.send_internal( - &SetCircuit109Behaviour { - value: Circuit109Behaviour::ChangesWithCarrier, - }, - false, - )?; - - // Ignore changes to DTR - self.network.send_internal( - &SetCircuit108Behaviour { - value: Circuit108Behaviour::Ignore, - }, - false, - )?; - - // Switch off UART power saving until it is integrated into this API - self.network.send_internal( - &SetPowerSavingControl { - mode: PowerSavingMode::Disabled, - timeout: None, - }, - false, - )?; - - if Config::HEX_MODE { - self.network.send_internal( - &SetHexMode { - hex_mode_disable: HexMode::Enabled, - }, - false, - )?; - } else { - self.network.send_internal( - &SetHexMode { - hex_mode_disable: HexMode::Disabled, - }, - false, - )?; - } - - // Tell module whether we support flow control - // FIXME: Use AT+IFC=2,2 instead of AT&K here - if Config::FLOW_CONTROL { - self.network.send_internal( - &SetFlowControl { - value: FlowControl::RtsCts, - }, - false, - )?; - } else { - self.network.send_internal( - &SetFlowControl { - value: FlowControl::Disabled, - }, - false, - )?; - } - - self.state = State::AtInitialized; - Ok(()) - } - - /// Send AT commands and wait responses from modem - /// - /// Modem must be initialized before this works. - /// For example use [`setup_at_commands`](Device::setup_at_commands) to only initialize AT commands support and nothing else. - /// - /// # Examples - /// - /// ```ignore - /// use ublox_cellular::command::device_data_security::{types::SecurityDataType, RetrieveSecurityMd5}; - /// - /// modem.setup_at_commands()?; - /// modem.send_at(&RetrieveSecurityMd5 { - /// data_type: SecurityDataType::TrustedRootCA, - /// internal_name: "ca_cert", - /// })?; - /// info!("md5: {:}", resp.md5_string); - /// ``` - pub fn send_at(&mut self, cmd: &A) -> Result - where - A: atat::AtatCmd, - { - match self.state { - State::Off => { - error!("Device not initialized!"); - return Err(Error::Uninitialized); - } - State::AtInitialized | State::FullyInitialized => {} - } - - Ok(self.network.send_internal(cmd, true)?) - } - - fn select_sim_card(&mut self) -> Result<(), Error> { - for _ in 0..2 { - match self.network.send_internal(&GetPinStatus, true) { - Ok(PinStatus { code }) if code == PinStatusCode::Ready => { - return Ok(()); - } - _ => {} - } - - BlockingTimer::after(Duration::from_secs(1)).wait(); - } - - // There was an error initializing the SIM - // We've seen issues on uBlox-based devices, as a precation, we'll cycle - // the modem here through minimal/full functional state. - self.network.send_internal( - &SetModuleFunctionality { - fun: Functionality::Minimum, - // SARA-R5 This parameter can be used only when is 1, 4 or 19 - #[cfg(feature = "sara-r5")] - rst: None, - #[cfg(not(feature = "sara-r5"))] - rst: Some(ResetMode::DontReset), - }, - true, - )?; - self.network.send_internal( - &SetModuleFunctionality { - fun: Functionality::Full, - rst: Some(ResetMode::DontReset), - }, - true, - )?; - - Err(Error::Busy) - } - - /// Initialize modem fully - /// - /// Turns modem on if it is off, configures it and starts registering to network. - /// - /// In typical situations user should not use it directly. - /// This should be used only if you would like to handle connections manually. - /// - /// For typical use case use [`spin`][Device::spin] which handles everything automatically. - pub fn initialize(&mut self) -> Result<(), Error> { - if self.power_state != PowerState::On { - // Always re-configure the module when power has been off - self.state = State::Off; - - // Catch states where we have no vint sense, and the module is already in powered mode, - // but for some reason doesn't answer to AT commands. - // This usually happens on programming after modem power on. - if self.power_on().is_err() { - self.hard_reset()?; - } - - self.power_state = PowerState::On; - } else if matches!(self.state, State::FullyInitialized) { - return Ok(()); - } - - self.setup_at_commands()?; - self.select_sim_card()?; - - // Disable Message Waiting URCs (UMWI) - #[cfg(any(feature = "toby-r2"))] - self.network.send_internal( - &crate::command::sms::SetMessageWaitingIndication { - mode: crate::command::sms::types::MessageWaitingMode::Disabled, - }, - false, - )?; - - self.network.send_internal( - &SetAutomaticTimezoneUpdate { - on_off: AutomaticTimezone::EnabledLocal, - }, - false, - )?; - - self.network.send_internal( - &SetModuleFunctionality { - fun: Functionality::Full, - rst: None, - }, - true, - )?; - - self.network.status.reset(); - self.network - .status - .set_connection_state(ConnectionState::Connecting); - - self.enable_registration_urcs()?; - - // Set automatic operator selection, if not already set - let OperatorSelection { mode, .. } = - self.network.send_internal(&GetOperatorSelection, true)?; - - // Only run AT+COPS=0 if currently de-registered, to avoid PLMN reselection - if !matches!( - mode, - OperatorSelectionMode::Automatic | OperatorSelectionMode::Manual - ) { - self.network.send_internal( - &SetOperatorSelection { - mode: OperatorSelectionMode::Automatic, - format: Some(2), - }, - true, - )?; - } - - self.network.update_registration()?; - - self.network.reset_reg_time()?; - - self.state = State::FullyInitialized; - Ok(()) - } - - pub(crate) fn clear_buffers(&mut self) -> Result<(), Error> { - if let Some(ref mut sockets) = self.sockets.as_deref_mut() { - sockets.prune(); - } - - Ok(()) - } - - pub(crate) fn enable_registration_urcs(&mut self) -> Result<(), Error> { - // if packet domain event reporting is not set it's not a stopper. We - // might lack some events when we are dropped from the network. - // TODO: Re-enable this when it works, and is useful! - if self - .network - .send_internal( - &SetPacketSwitchedEventReporting { - mode: PSEventReportingMode::CircularBufferUrcs, - bfr: None, - }, - true, - ) - .is_err() - { - warn!("Packet domain event reporting set failed"); - } - - // FIXME: Currently `atat` is unable to distinguish `xREG` family of - // commands from URC's - - // CREG URC - self.network.send_internal( - &SetNetworkRegistrationStatus { - n: NetworkRegistrationUrcConfig::UrcDisabled, - }, - true, - )?; - - // CGREG URC - self.network.send_internal( - &SetGPRSNetworkRegistrationStatus { - n: GPRSNetworkRegistrationUrcConfig::UrcDisabled, - }, - true, - )?; - - // CEREG URC - self.network.send_internal( - &SetEPSNetworkRegistrationStatus { - n: EPSNetworkRegistrationUrcConfig::UrcDisabled, - }, - true, - )?; - - Ok(()) - } - - fn handle_urc_internal(&mut self) -> Result<(), Error> { - if let Some(ref mut sockets) = self.sockets.as_deref_mut() { - if let Some(urc) = self.urc_subscription.try_next_message_pure() { - match urc { - Urc::SocketClosed(ip_transport_layer::urc::SocketClosed { socket }) => { - info!("[URC] SocketClosed {}", socket.0); - if let Some((_, mut sock)) = - sockets.iter_mut().find(|(handle, _)| *handle == socket) - { - sock.closed_by_remote(); - } - } - Urc::SocketDataAvailable(ip_transport_layer::urc::SocketDataAvailable { - socket, - length, - }) - | Urc::SocketDataAvailableUDP(ip_transport_layer::urc::SocketDataAvailable { - socket, - length, - }) => { - trace!("[Socket({})] {} bytes available", socket.0, length as u16); - if let Some((_, mut sock)) = - sockets.iter_mut().find(|(handle, _)| *handle == socket) - { - sock.set_available_data(length); - } - } - _ => {} - } - } - Ok(()) - } else { - Ok(()) - } - } - - pub(crate) fn process_events(&mut self) -> Result<(), Error> { - if self.power_state != PowerState::On { - return Err(Error::Uninitialized); - } - - self.handle_urc_internal()?; - - match self.network.process_events() { - // Catch "Resetting the modem due to the network registration timeout" - // as well as consecutive AT timeouts and do a hard reset. - Err(crate::network::Error::Generic(GenericError::Timeout)) => { - self.hard_reset()?; - Err(Error::Generic(GenericError::Timeout)) - } - result => result.map_err(Error::from), - } - } -} diff --git a/ublox-cellular/src/command/general/types.rs b/ublox-cellular/src/command/general/types.rs deleted file mode 100644 index 0a58407..0000000 --- a/ublox-cellular/src/command/general/types.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! Argument and parameter types used by General Commands and Responses - -use atat::atat_derive::AtatEnum; -#[derive(Clone, PartialEq, Eq, AtatEnum)] -pub enum Snt { - /// (default value): International Mobile station Equipment Identity (IMEI) - IMEI = 0, - /// International Mobile station Equipment Identity and Software Version number(IMEISV) - IMEISV = 2, - /// Software Version Number (SVN) - SVN = 3, - /// IMEI (not including the spare digit), the check digit and the SVN - IMEIExtended = 255, -} diff --git a/ublox-cellular/src/command/mod.rs b/ublox-cellular/src/command/mod.rs deleted file mode 100644 index b7a588f..0000000 --- a/ublox-cellular/src/command/mod.rs +++ /dev/null @@ -1,67 +0,0 @@ -//! AT Commands for u-blox cellular module family\ -//! Following the [u-blox cellular modules AT commands manual](https://www.u-blox.com/sites/default/files/u-blox-CEL_ATCommands_%28UBX-13002752%29.pdf) - -pub mod control; -pub mod device_data_security; -pub mod device_lock; -pub mod dns; -pub mod file_system; -pub mod general; -pub mod gpio; -pub mod http; -pub mod ip_transport_layer; -pub mod mobile_control; -pub mod network_service; -pub mod psn; -pub mod sms; -pub mod system_features; - -use atat::atat_derive::{AtatCmd, AtatResp, AtatUrc}; - -#[derive(Clone, AtatResp)] -pub struct NoResponse; - -#[derive(Clone, AtatCmd)] -#[at_cmd("", NoResponse)] -pub struct AT; - -#[derive(Debug, Clone, AtatUrc)] -pub enum Urc { - #[at_urc("+CGEV: NW DETACH")] - NetworkDetach, - #[at_urc("+CGEV: ME DETACH")] - MobileStationDetach, - #[at_urc("+CGEV: NW DEACT")] - NetworkDeactivate, - #[at_urc("+CGEV: ME DEACT")] - MobileStationDeactivate, - #[at_urc("+CGEV: NW PDN DEACT")] - NetworkPDNDeactivate, - #[at_urc("+CGEV: ME PDN DEACT")] - MobileStationPDNDeactivate, - - #[at_urc("+UUSORD")] - SocketDataAvailable(ip_transport_layer::urc::SocketDataAvailable), - #[at_urc("+UUSORF")] - SocketDataAvailableUDP(ip_transport_layer::urc::SocketDataAvailable), - #[at_urc("+UUPSDA")] - DataConnectionActivated(psn::urc::DataConnectionActivated), - #[at_urc("+UUPSDD")] - DataConnectionDeactivated(psn::urc::DataConnectionDeactivated), - #[at_urc("+UUSOCL")] - SocketClosed(ip_transport_layer::urc::SocketClosed), - - #[at_urc("+UMWI")] - MessageWaitingIndication(sms::urc::MessageWaitingIndication), - // #[at_urc("+CREG")] - // NetworkRegistration(network_service::urc::NetworkRegistration), - // #[at_urc("+CGREG")] - // GPRSNetworkRegistration(psn::urc::GPRSNetworkRegistration), - // #[at_urc("+CEREG")] - // EPSNetworkRegistration(psn::urc::EPSNetworkRegistration), - #[at_urc("+UREG")] - ExtendedPSNetworkRegistration(psn::urc::ExtendedPSNetworkRegistration), - - #[at_urc("+UUHTTPCR")] - HttpResponse(http::urc::HttpResponse), -} diff --git a/ublox-cellular/src/config.rs b/ublox-cellular/src/config.rs deleted file mode 100644 index 727500b..0000000 --- a/ublox-cellular/src/config.rs +++ /dev/null @@ -1,40 +0,0 @@ -use embedded_hal::digital::{ErrorType, InputPin, OutputPin}; - -pub struct NoPin; - -impl ErrorType for NoPin { - type Error = core::convert::Infallible; -} - -impl InputPin for NoPin { - fn is_high(&self) -> Result { - Ok(true) - } - - fn is_low(&self) -> Result { - Ok(false) - } -} - -impl OutputPin for NoPin { - fn set_low(&mut self) -> Result<(), Self::Error> { - Ok(()) - } - - fn set_high(&mut self) -> Result<(), Self::Error> { - Ok(()) - } -} - -pub trait CellularConfig { - type ResetPin: OutputPin; - type PowerPin: OutputPin; - type VintPin: InputPin; - - const FLOW_CONTROL: bool = false; - const HEX_MODE: bool = true; - - fn reset_pin(&mut self) -> Option<&mut Self::ResetPin>; - fn power_pin(&mut self) -> Option<&mut Self::PowerPin>; - fn vint_pin(&mut self) -> Option<&mut Self::VintPin>; -} diff --git a/ublox-cellular/src/error.rs b/ublox-cellular/src/error.rs deleted file mode 100644 index 57e6999..0000000 --- a/ublox-cellular/src/error.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::network::Error as NetworkError; -use crate::services::data::Error as DataServiceError; - -#[derive(Debug, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum GenericError { - Timeout, - Clock, - Unsupported, -} - -#[derive(Debug, PartialEq)] -#[non_exhaustive] -pub enum Error { - // General device errors - BaudDetection, - Busy, - Uninitialized, - StateTimeout, - - // Network errors - Network(NetworkError), - - // Service specific errors - DataService(DataServiceError), - - // Generic shared errors, e.g. from `core::` - Generic(GenericError), - - _Unknown, -} - -impl From for Error { - fn from(e: DataServiceError) -> Self { - // Unwrap generic and network errors - match e { - DataServiceError::Generic(g) => Self::Generic(g), - DataServiceError::Network(g) => Self::Network(g), - _ => Self::DataService(e), - } - } -} - -impl From for Error { - fn from(e: NetworkError) -> Self { - // Unwrap generic errors - match e { - NetworkError::Generic(g) => Self::Generic(g), - _ => Self::Network(e), - } - } -} diff --git a/ublox-cellular/src/lib.rs b/ublox-cellular/src/lib.rs deleted file mode 100644 index bb2deda..0000000 --- a/ublox-cellular/src/lib.rs +++ /dev/null @@ -1,131 +0,0 @@ -#![cfg_attr(not(test), no_std)] - -//! # U-blox cellular -//! -//! This crate supports various U-blox cellular modules that are using AT commands based interface. -//! It can be used both on `no_std` and `std` platforms. -//! -//! ## Example -//! -//! By default following features are enabled: `toby-r2`, `socket-udp`, `socket-tcp`. -//! -//! An example to use a different modem with only enabling TCP support: -//! -//! ```toml -//! ublox-cellular-rs = { version = "0.4", default-features = false, features = ["sara-g3", "socket-tcp"] } -//! ``` -//! -//! ### Clock trait -//! -//! To use this crate one must implement [`Clock`][clock] trait for a timer. -//! Notice that `Clock` uses [`Duration`][duration] and [`Instant`][instant] from [fugit] crate. -//! -//! Here is an example how it would look like for a `std` platform: -//! -//! ``` -//! use ublox_cellular::fugit; -//! use ublox_cellular::prelude::*; -//! -//! pub struct SysTimer { -//! start: std::time::Instant, -//! duration: fugit::TimerDurationU32, -//! } -//! -//! impl SysTimer { -//! pub fn new() -> Self { -//! Self { -//! start: std::time::Instant::now(), -//! duration: fugit::TimerDurationU32::millis(0), -//! } -//! } -//! } -//! -//! impl Clock for SysTimer { -//! type Error = std::convert::Infallible; -//! -//! fn now(&mut self) -> fugit::TimerInstantU32 { -//! let millis = self.start.elapsed().as_millis(); -//! fugit::TimerInstantU32::from_ticks(millis as u32) -//! } -//! -//! fn start(&mut self, duration: fugit::TimerDurationU32) -> Result<(), Self::Error> { -//! self.start = std::time::Instant::now(); -//! self.duration = duration.convert(); -//! Ok(()) -//! } -//! -//! fn wait(&mut self) -> nb::Result<(), Self::Error> { -//! if std::time::Instant::now() - self.start -//! > std::time::Duration::from_millis(self.duration.ticks() as u64) -//! { -//! Ok(()) -//! } else { -//! Err(nb::Error::WouldBlock) -//! } -//! } -//! } -//! ``` -//! -//! ### Driver usage -//! -//! Modem driver usage examples can be found [here](https://github.com/BlackbirdHQ/ublox-cellular-rs/tree/master/examples). -//! -//! [clock]: prelude/trait.Clock.html -//! [duration]: ../fugit/duration/struct.Duration.html -//! [instant]: ../fugit/instant/struct.Instant.html -//! - -// This mod MUST go first, so that the others see its macros. -pub(crate) mod fmt; - -mod blocking_timer; -mod client; -pub mod command; -mod config; -pub mod error; -mod module_timing; -mod network; -mod power; -mod registration; -mod services; - -pub use atat::serde_bytes; -use client::{URC_CAPACITY, URC_SUBSCRIBERS}; -use command::Urc; -pub use ublox_sockets as sockets; - -pub use client::Device as GsmClient; -pub use config::NoPin; -pub use network::{ContextId, ProfileId}; -pub use services::data::apn::{APNInfo, Apn}; -pub use services::data::ssl::SecurityProfileId; -pub use services::data::DataService; - -// Re-export atat -pub use atat; - -pub type UbloxCellularBuffers = - atat::Buffers; - -pub type UbloxCellularIngress<'a, const INGRESS_BUF_SIZE: usize> = atat::Ingress< - 'a, - atat::DefaultDigester, - Urc, - INGRESS_BUF_SIZE, - URC_CAPACITY, - URC_SUBSCRIBERS, ->; - -pub type UbloxCellularUrcChannel = atat::UrcChannel; - -pub use config::CellularConfig; - -/// Prelude - Include traits -pub mod prelude { - #[cfg(any(feature = "socket-udp", feature = "socket-tcp"))] - pub use super::services::data::ssl::SSL; - #[cfg(any(feature = "socket-udp", feature = "socket-tcp"))] - pub use embedded_nal::{ - IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, TcpClientStack, UdpClientStack, - }; -} diff --git a/ublox-cellular/src/module_timing.rs b/ublox-cellular/src/module_timing.rs deleted file mode 100644 index 18d1fc7..0000000 --- a/ublox-cellular/src/module_timing.rs +++ /dev/null @@ -1,47 +0,0 @@ -#![allow(clippy::if_same_then_else)] - -use embassy_time::Duration; - -/// Low time of `PWR_ON` pin to trigger module switch on from power off mode -pub fn pwr_on_time() -> Duration { - if cfg!(feature = "lara-r6") { - Duration::from_millis(150) - } else if cfg!(feature = "toby-r2") { - Duration::from_micros(50) - } else { - Duration::from_micros(50) - } -} - -/// Low time of `PWR_ON` pin to trigger module graceful switch off -pub fn pwr_off_time() -> Duration { - if cfg!(feature = "lara-r6") { - Duration::from_millis(1500) - } else if cfg!(feature = "toby-r2") { - Duration::from_secs(1) - } else { - Duration::from_secs(1) - } -} - -/// Low time of `RESET_N` pin to trigger module reset (reboot) -pub fn reset_time() -> Duration { - if cfg!(feature = "lara-r6") { - Duration::from_millis(10) - } else if cfg!(feature = "toby-r2") { - Duration::from_millis(50) - } else { - Duration::from_millis(50) - } -} - -/// Low time of `RESET_N` pin to trigger module abrupt emergency switch off -/// -/// NOTE: Not all modules support this operation from `RESET_N` -pub fn kill_time() -> Option { - if cfg!(feature = "lara-r6") { - Some(Duration::from_secs(10)) - } else { - None - } -} diff --git a/ublox-cellular/src/network.rs b/ublox-cellular/src/network.rs deleted file mode 100644 index 044dd02..0000000 --- a/ublox-cellular/src/network.rs +++ /dev/null @@ -1,525 +0,0 @@ -use crate::{ - client::{URC_CAPACITY, URC_SUBSCRIBERS}, - command::{ - general::GetCIMI, - mobile_control::{ - types::{Functionality, ResetMode}, - GetExtendedErrorReport, SetModuleFunctionality, - }, - network_service::{ - types::OperatorSelectionMode, GetNetworkRegistrationStatus, SetOperatorSelection, - }, - psn::{ - self, types::PDPContextStatus, GetEPSNetworkRegistrationStatus, - GetGPRSNetworkRegistrationStatus, GetPDPContextState, SetPDPContextState, - }, - Urc, AT, - }, - error::GenericError, - registration::{self, ConnectionState, RegistrationState}, - services::data::{ContextState, PROFILE_ID}, -}; -use atat::{atat_derive::AtatLen, blocking::AtatClient, UrcSubscription}; -use embassy_time::{Duration, Instant}; -use hash32_derive::Hash32; -use serde::{Deserialize, Serialize}; - -const REGISTRATION_CHECK_INTERVAL: Duration = Duration::from_secs(15); -const REGISTRATION_TIMEOUT: Duration = Duration::from_secs(3 * 60); -const CHECK_IMSI_TIMEOUT: Duration = Duration::from_secs(60); - -#[derive(Debug, PartialEq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum Error { - Generic(GenericError), - AT(atat::Error), - RegistrationDenied, - UnknownProfile, - ActivationFailed, - _Unknown, -} - -#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash32, Serialize, Deserialize, AtatLen)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct ProfileId(pub u8); - -#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, AtatLen)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct ContextId(pub u8); - -pub struct AtTx<'sub, AtCl> { - consecutive_timeouts: u8, - urc_subscription: UrcSubscription<'sub, Urc, URC_CAPACITY, URC_SUBSCRIBERS>, - client: AtCl, -} - -impl<'sub, AtCl: AtatClient> AtTx<'sub, AtCl> { - pub fn new( - client: AtCl, - urc_subscription: UrcSubscription<'sub, Urc, URC_CAPACITY, URC_SUBSCRIBERS>, - ) -> Self { - Self { - consecutive_timeouts: 0, - urc_subscription, - client, - } - } - - pub fn send_ignore_timeout( - &mut self, - req: &A, - ) -> Result - where - A: atat::AtatCmd, - { - self.client - .send_retry(req) - .map_err(|e| match e { - atat::Error::Timeout => { - self.consecutive_timeouts = - self.consecutive_timeouts.saturating_add(A::ATTEMPTS); - Error::AT(atat::Error::Timeout) - } - atat::Error::Read => Error::AT(atat::Error::Read), - atat::Error::Write => Error::AT(atat::Error::Write), - atat::Error::InvalidResponse => Error::AT(atat::Error::InvalidResponse), - atat::Error::Aborted => Error::AT(atat::Error::Aborted), - atat::Error::Parse => Error::AT(atat::Error::Parse), - _ => Error::AT(atat::Error::Error), - }) - .map(|res| { - self.consecutive_timeouts = 0; - res - }) - } - - pub fn send(&mut self, req: &A) -> Result - where - A: atat::AtatCmd, - { - self.client - .send_retry(req) - .map_err(|e| match e { - atat::Error::Timeout => { - self.consecutive_timeouts = - self.consecutive_timeouts.saturating_add(A::ATTEMPTS); - Error::AT(atat::Error::Timeout) - } - atat::Error::Read => Error::AT(atat::Error::Read), - atat::Error::Write => Error::AT(atat::Error::Write), - atat::Error::InvalidResponse => Error::AT(atat::Error::InvalidResponse), - atat::Error::Aborted => Error::AT(atat::Error::Aborted), - atat::Error::Parse => Error::AT(atat::Error::Parse), - _ => Error::AT(atat::Error::Error), - }) - .map(|res| { - self.consecutive_timeouts = 0; - res - }) - } - - pub fn handle_urc bool>(&mut self, f: F) -> Result<(), Error> { - if let Some(urc) = self.urc_subscription.try_next_message_pure() { - f(urc); - } - Ok(()) - } -} - -pub struct Network<'sub, AtCl> { - pub(crate) status: RegistrationState, - pub(crate) context_state: ContextState, - pub(crate) at_tx: AtTx<'sub, AtCl>, -} - -impl<'sub, AtCl> Network<'sub, AtCl> -where - AtCl: AtatClient, -{ - pub(crate) fn new(at_tx: AtTx<'sub, AtCl>) -> Self { - Self { - status: RegistrationState::new(), - context_state: ContextState::Setup, - at_tx, - } - } - - pub fn is_connected(&self) -> Result { - Ok(matches!(self.status.conn_state, ConnectionState::Connected)) - } - - pub fn reset_reg_time(&mut self) -> Result<(), Error> { - self.status.reg_start_time.replace(Instant::now()); - self.status.reg_check_time = self.status.reg_start_time; - Ok(()) - } - - pub fn process_events(&mut self) -> Result<(), Error> { - if self.at_tx.consecutive_timeouts > 10 { - self.at_tx.consecutive_timeouts = 0; - warn!("Resetting the modem due to consecutive AT timeouts"); - return Err(Error::Generic(GenericError::Timeout)); - } - - self.handle_urc().ok(); // Ignore errors - self.check_registration_state(); - self.intervene_registration()?; - self.check_running_imsi().ok(); // Ignore errors - - let now = Instant::now(); - let should_check = self - .status - .reg_check_time - .and_then(|reg_check_time| { - now.checked_duration_since(reg_check_time) - .map(|dur| dur >= REGISTRATION_CHECK_INTERVAL) - }) - .unwrap_or(true); - - if !should_check { - return Ok(()); - } - - self.status.reg_check_time.replace(now); - - self.update_registration()?; - - let now = Instant::now(); - let is_timeout = self - .status - .reg_start_time - .and_then(|reg_start_time| { - now.checked_duration_since(reg_start_time) - .map(|dur| dur >= REGISTRATION_TIMEOUT) - }) - .unwrap_or(false); - - if self.status.conn_state == ConnectionState::Connecting && is_timeout { - warn!("Resetting the modem due to the network registration timeout"); - - return Err(Error::Generic(GenericError::Timeout)); - } - Ok(()) - } - - pub fn check_running_imsi(&mut self) -> Result<(), Error> { - // Check current IMSI if registered successfully in which case - // imsi_check_time will be `None`, else if not registered, check after - // CHECK_IMSI_TIMEOUT is expired - let now = Instant::now(); - let check_imsi = self - .status - .imsi_check_time - .and_then(|imsi_check_time| { - now.checked_duration_since(imsi_check_time) - .map(|dur| dur >= CHECK_IMSI_TIMEOUT) - }) - .unwrap_or(true); - - if check_imsi { - // NOTE: The CIMI command has been known to not have an immediate response on u-blox modems - // and currently has a 10 second timeout. This command is also only for - // logging purposes to monitor the currently selected IMSI on EtherSIM - // during registration. For these reasons we are intentionally not - // registering a parserError_ when this command does not return - // AtResponse::OK. Instead, in the case of a non-OK response, we will - // follow up the command with an AT/OK check and subsequent - // checkParser() call to catch/address any modem parsing issues. - match self.send_internal(&GetCIMI, false) { - Ok(_) => {} - Err(_) => { - self.send_internal(&AT, false)?; - } - } - - self.status.imsi_check_time.replace(now); - } - Ok(()) - } - - pub fn check_registration_state(&mut self) { - // Don't do anything if we are actually disconnected by choice - if self.status.conn_state == ConnectionState::Disconnected { - return; - } - - // If both (CSD + PSD) is registered, or EPS is registered, we are connected! - if (self.status.csd.registered() && self.status.psd.registered()) - || self.status.eps.registered() - { - self.status.set_connection_state(ConnectionState::Connected); - } else if self.status.conn_state == ConnectionState::Connected { - // FIXME: potentially go back into connecting state only when getting into - // a 'sticky' non-registered state - self.status.reset(); - self.status - .set_connection_state(ConnectionState::Connecting); - } - } - - pub fn intervene_registration(&mut self) -> Result<(), Error> { - if self.status.conn_state != ConnectionState::Connecting { - return Ok(()); - } - - let now = Instant::now(); - - // If EPS has been sticky for longer than `timeout` - let timeout = Duration::from_secs(self.status.registration_interventions as u64 * 15); - if self.status.eps.sticky() && self.status.eps.duration(now) >= timeout { - // If (EPS + CSD) is not attempting registration - if self.status.eps.get_status() == registration::Status::NotRegistering - && self.status.csd.get_status() == registration::Status::NotRegistering - { - debug!( - "Sticky not registering state for {}, PLMN reselection", - self.status.eps.duration(now) - ); - - self.status.csd.reset(); - self.status.psd.reset(); - self.status.eps.reset(); - self.status.registration_interventions = - self.status.registration_interventions.saturating_add(1); - - self.send_internal( - &SetOperatorSelection { - mode: OperatorSelectionMode::Automatic, - format: Some(2), - }, - false, - ) - .ok(); // Ignore result - return Ok(()); - - // If (EPS + CSD) is denied registration - } else if self.status.eps.get_status() == registration::Status::Denied - && self.status.csd.get_status() == registration::Status::Denied - { - debug!( - "Sticky denied state for {}, RF reset", - self.status.eps.duration(now) - ); - self.status.csd.reset(); - self.status.psd.reset(); - self.status.eps.reset(); - self.status.registration_interventions = - self.status.registration_interventions.saturating_add(1); - self.send_internal( - &SetModuleFunctionality { - fun: Functionality::Minimum, - // SARA-R5 This parameter can be used only when is 1, 4 or 19 - #[cfg(feature = "sara-r5")] - rst: None, - #[cfg(not(feature = "sara-r5"))] - rst: Some(ResetMode::DontReset), - }, - false, - )?; - self.send_internal( - &SetModuleFunctionality { - fun: Functionality::Full, - rst: Some(ResetMode::DontReset), - }, - false, - )?; - return Ok(()); - } - } - - // If CSD has been sticky for longer than `timeout`, - // and (CSD + PSD) is denied registration. - if self.status.csd.sticky() - && self.status.csd.duration(now) >= timeout - && matches!( - self.status.csd.get_status(), - registration::Status::Denied | registration::Status::Roaming - ) - && self.status.psd.get_status() == registration::Status::Denied - { - debug!( - "Sticky CSD and PSD denied state for {}, RF reset", - self.status.csd.duration(now) - ); - self.status.csd.reset(); - self.status.psd.reset(); - self.status.eps.reset(); - self.status.registration_interventions = - self.status.registration_interventions.saturating_add(1); - self.send_internal( - &SetModuleFunctionality { - fun: Functionality::Minimum, - // SARA-R5 This parameter can be used only when is 1, 4 or 19 - #[cfg(feature = "sara-r5")] - rst: None, - #[cfg(not(feature = "sara-r5"))] - rst: Some(ResetMode::DontReset), - }, - false, - )?; - self.send_internal( - &SetModuleFunctionality { - fun: Functionality::Full, - rst: Some(ResetMode::DontReset), - }, - false, - )?; - return Ok(()); - } - - // If CSD is registered, but PSD has been sticky for longer than `timeout`, - // and (PSD + EPS) is not attempting registration. - if self.status.csd.registered() - && self.status.psd.sticky() - && self.status.psd.duration(now) >= timeout - && self.status.psd.get_status() == registration::Status::NotRegistering - && self.status.eps.get_status() == registration::Status::NotRegistering - { - debug!( - "Sticky not registering PSD state for {}, force GPRS attach", - self.status.psd.duration(now) - ); - self.status.psd.reset(); - self.status.registration_interventions = - self.status.registration_interventions.saturating_add(1); - self.send_internal(&GetPDPContextState, true)?; - - if self - .send_internal( - &SetPDPContextState { - status: PDPContextStatus::Activated, - cid: None, - }, - true, - ) - .is_err() - { - self.status.csd.reset(); - self.status.psd.reset(); - self.status.eps.reset(); - warn!("GPRS attach failed, try PLMN reselection"); - self.send_internal( - &SetOperatorSelection { - mode: OperatorSelectionMode::Automatic, - format: Some(2), - }, - true, - )?; - } - } - - Ok(()) - } - - pub fn update_registration(&mut self) -> Result<(), Error> { - self.send_internal(&GetExtendedErrorReport, false).ok(); - - if let Ok(reg) = self.send_internal(&GetNetworkRegistrationStatus, false) { - self.status.compare_and_set(reg.into()); - } - - if let Ok(reg) = self.send_internal(&GetGPRSNetworkRegistrationStatus, false) { - self.status.compare_and_set(reg.into()); - } - - if let Ok(reg) = self.send_internal(&GetEPSNetworkRegistrationStatus, false) { - self.status.compare_and_set(reg.into()); - } - - Ok(()) - } - - pub(crate) fn handle_urc(&mut self) -> Result<(), Error> { - // TODO: How to do this cleaner? - let mut ctx_state = self.context_state; - // let mut new_reg_params: Option = None; - - self.at_tx.handle_urc(|urc| { - match urc { - Urc::NetworkDetach => { - warn!("Network Detach URC!"); - } - Urc::MobileStationDetach => { - warn!("ME Detach URC!"); - } - Urc::NetworkDeactivate => { - warn!("Network Deactivate URC!"); - } - Urc::MobileStationDeactivate => { - warn!("ME Deactivate URC!"); - } - Urc::NetworkPDNDeactivate => { - warn!("Network PDN Deactivate URC!"); - } - Urc::MobileStationPDNDeactivate => { - warn!("ME PDN Deactivate URC!"); - } - Urc::ExtendedPSNetworkRegistration(psn::urc::ExtendedPSNetworkRegistration { - state, - }) => { - info!("[URC] ExtendedPSNetworkRegistration {:?}", state); - } - // FIXME: Currently `atat` is unable to distinguish `xREG` family of - // commands from URC's - - // Urc::GPRSNetworkRegistration(reg_params) => { - // new_reg_params.replace(reg_params.into()); - // } - // Urc::EPSNetworkRegistration(reg_params) => { - // new_reg_params.replace(reg_params.into()); - // } - // Urc::NetworkRegistration(reg_params) => { - // new_reg_params.replace(reg_params.into()); - // } - Urc::DataConnectionActivated(psn::urc::DataConnectionActivated { - result, - ip_addr: _, - }) => { - info!("[URC] DataConnectionActivated {}", result); - if result == 0 { - ctx_state = ContextState::Active; - } else { - ctx_state = ContextState::Setup; - } - } - Urc::DataConnectionDeactivated(psn::urc::DataConnectionDeactivated { - profile_id, - }) => { - info!("[URC] DataConnectionDeactivated {:?}", profile_id); - if profile_id == PROFILE_ID { - ctx_state = ContextState::Activating; - } - } - Urc::MessageWaitingIndication(_) => { - info!("[URC] MessageWaitingIndication"); - } - _ => return false, - }; - true - })?; - - // if let Some(reg_params) = new_reg_params { - // self.status.compare_and_set(reg_params) - // } - - self.context_state = ctx_state; - Ok(()) - } - - pub(crate) fn send_internal( - &mut self, - req: &A, - check_urc: bool, - ) -> Result - where - A: atat::AtatCmd, - { - if check_urc { - if let Err(e) = self.handle_urc() { - error!("Failed handle URC {:?}", &e); - } - } - - self.at_tx.send(req) - } -} diff --git a/ublox-cellular/src/power.rs b/ublox-cellular/src/power.rs deleted file mode 100644 index aa9355d..0000000 --- a/ublox-cellular/src/power.rs +++ /dev/null @@ -1,248 +0,0 @@ -use atat::blocking::AtatClient; -use embassy_time::{Duration, Instant}; -use embedded_hal::digital::{InputPin, OutputPin}; - -use crate::{ - blocking_timer::BlockingTimer, - client::Device, - command::{ - mobile_control::{ - types::{Functionality, ResetMode}, - ModuleSwitchOff, SetModuleFunctionality, - }, - system_features::{ - types::{FSFactoryRestoreType, NVMFactoryRestoreType}, - SetFactoryConfiguration, - }, - AT, - }, - config::CellularConfig, - error::{Error, GenericError}, - module_timing::{pwr_off_time, pwr_on_time, reset_time}, -}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum PowerState { - Off, - On, -} - -impl<'buf, 'sub, AtCl, AtUrcCh, Config, const N: usize, const L: usize> - Device<'buf, 'sub, AtCl, AtUrcCh, Config, N, L> -where - AtCl: AtatClient, - Config: CellularConfig, -{ - /// Check that the cellular module is alive. - /// - /// See if the cellular module is responding at the AT interface by poking - /// it with "AT" up to `attempts` times, waiting 1 second for an "OK" - /// response each time - pub(crate) fn is_alive(&mut self, attempts: u8) -> Result<(), Error> { - let mut error = Error::BaudDetection; - for _ in 0..attempts { - match self.network.at_tx.send_ignore_timeout(&AT) { - Ok(_) => { - return Ok(()); - } - Err(e) => error = e.into(), - }; - } - Err(error) - } - - /// Perform at full factory reset of the module, clearing all NVM sectors in the process - pub fn factory_reset(&mut self) -> Result<(), Error> { - self.network.send_internal( - &SetFactoryConfiguration { - fs_op: FSFactoryRestoreType::AllFiles, - nvm_op: NVMFactoryRestoreType::NVMFlashSectors, - }, - false, - )?; - - info!("Successfully factory reset modem!"); - - if self.soft_reset(true).is_err() { - self.hard_reset()?; - } - - Ok(()) - } - - /// Reset the module by sending AT CFUN command - pub(crate) fn soft_reset(&mut self, sim_reset: bool) -> Result<(), Error> { - trace!( - "Attempting to soft reset of the modem with sim reset: {}.", - sim_reset - ); - - let fun = if sim_reset { - Functionality::SilentResetWithSimReset - } else { - Functionality::SilentReset - }; - - self.network.send_internal( - &SetModuleFunctionality { - fun, - // SARA-R5 This parameter can be used only when is 1, 4 or 19 - #[cfg(feature = "sara-r5")] - rst: None, - #[cfg(not(feature = "sara-r5"))] - rst: Some(ResetMode::DontReset), - }, - false, - )?; - - self.wait_power_state(PowerState::On, Duration::from_secs(30)) - .map_err(|_| Error::Generic(GenericError::Timeout))?; - - Ok(()) - } - - /// Reset the module by driving it's `RESET_N` pin low for 50 ms - /// - /// **NOTE** This function will reset NVM settings! - pub fn hard_reset(&mut self) -> Result<(), Error> { - trace!("Attempting to hard reset of the modem."); - if let Some(rst) = self.config.reset_pin() { - rst.set_low().ok(); - - BlockingTimer::after(reset_time()).wait(); - - rst.set_high().ok(); - - BlockingTimer::after(Duration::from_secs(5)).wait(); - } - - self.power_state = PowerState::Off; - - self.power_on()?; - - Ok(()) - } - - pub fn power_on(&mut self) -> Result<(), Error> { - info!( - "Attempting to power on the modem with PWR_ON pin: {} and VInt pin: {}.", - self.config.power_pin().is_some(), - self.config.vint_pin().is_some(), - ); - - if self.power_state()? != PowerState::On { - trace!("Powering modem on."); - match self.config.power_pin() { - // Apply Low pulse on PWR_ON for 50 microseconds to power on - Some(pwr) => { - pwr.set_low().ok(); - BlockingTimer::after(pwr_on_time()).wait(); - - pwr.set_high().ok(); - - BlockingTimer::after(Duration::from_secs(1)).wait(); - - if let Err(e) = self.wait_power_state(PowerState::On, Duration::from_secs(10)) { - error!("Failed to power on modem"); - return Err(e); - } else { - trace!("Modem powered on"); - } - } - _ => { - // Software restart - if self.soft_reset(false).is_err() { - self.hard_reset()?; - } - } - } - } else { - debug!("module is already on"); - } - Ok(()) - } - - pub fn soft_power_off(&mut self) -> Result<(), Error> { - trace!("Attempting to soft power off the modem."); - - self.network.send_internal(&ModuleSwitchOff, false)?; - - self.power_state = PowerState::Off; - trace!("Modem powered off"); - - BlockingTimer::after(Duration::from_secs(10)).wait(); - - Ok(()) - } - - pub fn hard_power_off(&mut self) -> Result<(), Error> { - trace!("Attempting to hard power off the modem."); - - if self.power_state()? == PowerState::On { - match self.config.power_pin() { - Some(pwr) => { - // Apply Low pulse on PWR_ON >= 1 second to power off - pwr.set_low().ok(); - BlockingTimer::after(pwr_off_time()).wait(); - - pwr.set_high().ok(); - self.power_state = PowerState::Off; - trace!("Modem powered off"); - } - _ => { - return Err(Error::Generic(GenericError::Unsupported)); - } - } - } - - Ok(()) - } - - /// Check the power state of the module, by probing `Vint` pin if available, - /// fallbacking to checking for AT responses through `is_alive` - pub fn power_state(&mut self) -> Result { - match self.config.vint_pin() { - Some(vint) => { - if vint - .is_high() - .map_err(|_| Error::Generic(GenericError::Unsupported))? - { - Ok(PowerState::On) - } else { - Ok(PowerState::Off) - } - } - _ => Ok(self.is_alive(2).map_or(PowerState::Off, |_| PowerState::On)), - } - } - - /// Wait for the power state to change into `expected`, with a timeout - fn wait_power_state(&mut self, expected: PowerState, timeout: Duration) -> Result<(), Error> { - let start = Instant::now(); - - let mut res = false; - - trace!("Waiting for the modem to reach {:?}.", expected); - - while Instant::now() - .checked_duration_since(start) - .map_or(false, |dur| dur < timeout) - { - if self.power_state()? == expected { - res = true; - break; - } - - BlockingTimer::after(Duration::from_millis(5)).wait(); - } - - if res { - trace!("Success."); - Ok(()) - } else { - error!("Modem never reach {:?}.", expected); - Err(Error::Generic(GenericError::Timeout)) - } - } -} diff --git a/ublox-cellular/src/services/data/apn.rs b/ublox-cellular/src/services/data/apn.rs deleted file mode 100644 index 9729778..0000000 --- a/ublox-cellular/src/services/data/apn.rs +++ /dev/null @@ -1,29 +0,0 @@ -#[derive(Debug, Clone)] -pub enum Apn<'a> { - Given(&'a str), - Automatic, -} - -impl<'a> Default for Apn<'a> { - fn default() -> Self { - Self::Automatic - } -} - -#[derive(Debug, Clone, Default)] -pub struct APNInfo<'a> { - pub apn: Apn<'a>, - pub user_name: Option<&'a str>, - pub password: Option<&'a str>, -} - -impl<'a> APNInfo<'a> { - #[must_use] - pub fn new(apn: &'a str) -> Self { - Self { - apn: Apn::Given(apn), - user_name: None, - password: None, - } - } -} diff --git a/ublox-cellular/src/services/data/dns.rs b/ublox-cellular/src/services/data/dns.rs deleted file mode 100644 index 57bcc28..0000000 --- a/ublox-cellular/src/services/data/dns.rs +++ /dev/null @@ -1,62 +0,0 @@ -use atat::blocking::AtatClient; -use core::fmt::Write; -use embedded_nal::IpAddr; -use embedded_nal::{AddrType, Dns}; -use heapless::String; - -use super::DataService; -use crate::command::dns::{self, types::ResolutionType}; -use ublox_sockets::Error; - -impl<'a, 'sub, AtCl, const N: usize, const L: usize> Dns for DataService<'a, 'sub, AtCl, N, L> -where - AtCl: AtatClient, -{ - type Error = Error; - - fn get_host_by_address(&mut self, ip_addr: IpAddr) -> nb::Result, Self::Error> { - let mut ip_str = String::<256>::new(); - write!(&mut ip_str, "{ip_addr}").map_err(|_| Error::BadLength)?; - - match self.network.send_internal( - &dns::ResolveNameIp { - resolution_type: ResolutionType::IpToDomainName, - ip_domain_string: &ip_str, - }, - true, - ) { - Ok(resp) => Ok(String::from(resp.ip_domain_string.as_str())), - Err(e) => { - error!("get_host_by_address failed: {:?}", e); - Err(nb::Error::Other(Error::Unaddressable)) - } - } - } - - fn get_host_by_name( - &mut self, - hostname: &str, - addr_type: AddrType, - ) -> nb::Result { - if addr_type == AddrType::IPv6 { - return Err(nb::Error::Other(Error::Illegal)); - } - - match self.network.send_internal( - &dns::ResolveNameIp { - resolution_type: ResolutionType::DomainNameToIp, - ip_domain_string: hostname, - }, - true, - ) { - Ok(resp) => resp - .ip_domain_string - .parse() - .map_err(|_e| nb::Error::Other(Error::Illegal)), - Err(e) => { - error!("get_host_by_name failed: {:?}", e); - Err(nb::Error::Other(Error::Unaddressable)) - } - } - } -} diff --git a/ublox-cellular/src/services/data/error.rs b/ublox-cellular/src/services/data/error.rs deleted file mode 100644 index 4a6c557..0000000 --- a/ublox-cellular/src/services/data/error.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::error::GenericError; -use crate::network::Error as NetworkError; -use ublox_sockets::Error as SocketError; - -#[derive(Debug, PartialEq)] -pub enum Error { - InvalidApn, - SocketMemory, - WrongSocketType, - BadLength, - Dns, - BufferFull, - InvalidHex, - - Socket(SocketError), - - Network(NetworkError), - - Generic(GenericError), - - _Unknown, -} - -impl From for Error { - fn from(e: NetworkError) -> Self { - match e { - NetworkError::Generic(g) => Self::Generic(g), - _ => Self::Network(e), - } - } -} - -impl From for Error { - fn from(e: SocketError) -> Self { - Self::Socket(e) - } -} diff --git a/ublox-cellular/src/services/data/hex.rs b/ublox-cellular/src/services/data/hex.rs deleted file mode 100644 index 8195081..0000000 --- a/ublox-cellular/src/services/data/hex.rs +++ /dev/null @@ -1,34 +0,0 @@ -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FromHexError { - /// An invalid character was found. Valid ones are: `0...9`, `a...f` - /// or `A...F`. - InvalidHexCharacter, - - /// A hex string's length needs to be even, as two digits correspond to - /// one byte. - OddLength, -} - -/// Decode a single hex char to decimal. -const fn val(c: u8) -> Result { - match c { - b'A'..=b'F' => Ok(c - b'A' + 10), - b'a'..=b'f' => Ok(c - b'a' + 10), - b'0'..=b'9' => Ok(c - b'0'), - _ => Err(FromHexError::InvalidHexCharacter), - } -} - -/// Decode hexadecimal bytes to decimal bytes in-place overwriting the first n/2 -/// bytes, and returning them as a slice. -pub fn from_hex(hex: &mut [u8]) -> Result<&[u8], FromHexError> { - if hex.len() % 2 != 0 { - return Err(FromHexError::OddLength); - } - - let len = hex.len() / 2; - for i in 0..len { - hex[i] = val(hex[i * 2])? << 4 | val(hex[i * 2 + 1])? - } - Ok(&hex[..len]) -} diff --git a/ublox-cellular/src/services/data/mod.rs b/ublox-cellular/src/services/data/mod.rs deleted file mode 100644 index efc1c47..0000000 --- a/ublox-cellular/src/services/data/mod.rs +++ /dev/null @@ -1,639 +0,0 @@ -pub mod apn; -pub mod dns; -pub mod error; -pub mod ssl; - -#[cfg(feature = "socket-tcp")] -mod tcp_stack; - -#[cfg(feature = "socket-udp")] -mod udp_stack; - -mod hex; - -use crate::{ - blocking_timer::BlockingTimer, - client::Device, - command::mobile_control::types::{Functionality, ResetMode}, - command::mobile_control::SetModuleFunctionality, - command::psn::types::PDPContextStatus, - command::psn::SetPDPContextDefinition, - command::psn::SetPDPContextState, - command::Urc, - command::{ - ip_transport_layer::{ - responses::{SocketData, UDPSocketData}, - ReadSocketData, ReadUDPSocketData, - }, - psn::{self, responses::GPRSAttached, GetPDPContextState}, - }, - config::CellularConfig, - error::Error as DeviceError, - network::{ContextId, Network}, - ProfileId, -}; -use apn::{APNInfo, Apn}; -use atat::blocking::AtatClient; -use embassy_time::Duration; - -pub use error::Error; -use psn::{types::GPRSAttachedState, GetGPRSAttached}; -use ublox_sockets::{Error as SocketError, SocketSet, SocketType}; - -#[cfg(feature = "upsd-context-activation")] -use embedded_nal::Ipv4Addr; - -// NOTE: If these are changed, remember to change the corresponding `Bytes` len -// in commands for now. -pub const INGRESS_CHUNK_SIZE: usize = 256; -pub const EGRESS_CHUNK_SIZE: usize = 1024; - -pub const PROFILE_ID: ProfileId = ProfileId(1); - -#[cfg(not(feature = "upsd-context-activation"))] -const CONTEXT_ID: ContextId = ContextId(1); - -impl<'buf, 'sub, AtCl, AtUrcCh, Config, const N: usize, const L: usize> - Device<'buf, 'sub, AtCl, AtUrcCh, Config, N, L> -where - 'buf: 'sub, - AtCl: AtatClient, - Config: CellularConfig, -{ - /// Define a PDP context - #[cfg(not(feature = "upsd-context-activation"))] - fn define_context(&mut self, cid: ContextId, apn_info: &APNInfo) -> Result<(), Error> { - if self.network.context_state != ContextState::Setup { - return Ok(()); - } - - self.network.send_internal( - &SetModuleFunctionality { - fun: Functionality::Minimum, - // SARA-R5: this parameter can be used only when is 1, 4 or 19 - #[cfg(feature = "sara-r5")] - rst: None, - #[cfg(not(feature = "sara-r5"))] - rst: Some(ResetMode::DontReset), - }, - true, - )?; - - if let Apn::Given(apn) = apn_info.apn { - self.network.send_internal( - &SetPDPContextDefinition { - cid, - pdp_type: "IP", - apn, - }, - true, - )?; - } - - // self.network.send_internal( - // &SetAuthParameters { - // cid, - // auth_type: AuthenticationType::Auto, - // username: &apn_info.clone().user_name.unwrap_or_default(), - // password: &apn_info.clone().password.unwrap_or_default(), - // }, - // true, - // )?; - - self.network.send_internal( - &SetModuleFunctionality { - fun: Functionality::Full, - rst: Some(ResetMode::DontReset), - }, - true, - )?; - - self.network.context_state = ContextState::Activating; - Ok(()) - } - - /// Handle modem data connection - /// - /// For typical use case only this is needed to manage modem automatically. - /// It does everything from turning on the modem, configuring it and managing network connection. - /// This must be called periodically in a loop. - /// - /// # Examples - /// - /// ```ignore - /// loop { - /// modem.data_service(&APNInfo::new("myapn")).and_then(|mut data_service| { - /// // at this point modem is registered to network and data connection is active - /// // `data_service` can be use to send / receive data - /// - /// Ok(()) - /// }); - /// } - /// ``` - pub fn data_service<'a>( - &'a mut self, - apn_info: &APNInfo, - ) -> nb::Result, DeviceError> { - // Spin [`Device`], handling [`Network`] related URC changes and - // propagting the FSM - match self.spin() { - // If we're not using AT+UPSD-based context activation, set the - // context using AT+CGDCONT and the authentication mode - Err(nb::Error::WouldBlock) => { - #[cfg(not(feature = "upsd-context-activation"))] - self.define_context(CONTEXT_ID, apn_info) - .map_err(DeviceError::from)?; - return Err(nb::Error::WouldBlock); - } - Ok(()) => { - #[cfg(not(feature = "upsd-context-activation"))] - self.define_context(CONTEXT_ID, apn_info) - .map_err(DeviceError::from)?; - } - Err(e) => return Err(e), - } - - // At this point we WILL be registered on the network! - match DataService::try_new(apn_info, &mut self.network, self.sockets.as_deref_mut()) { - Ok(service) => Ok(service), - Err(nb::Error::Other(e)) => Err(nb::Error::Other(e.into())), - Err(nb::Error::WouldBlock) => Err(nb::Error::WouldBlock), - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum ContextState { - Setup, - Activating, - Active, -} - -pub struct DataService<'a, 'sub, AtCl, const N: usize, const L: usize> -where - AtCl: AtatClient, -{ - network: &'a mut Network<'sub, AtCl>, - pub(crate) sockets: Option<&'a mut SocketSet>, -} - -impl<'a, 'sub, AtCl, const N: usize, const L: usize> DataService<'a, 'sub, AtCl, N, L> -where - AtCl: AtatClient, -{ - pub fn try_new( - apn_info: &APNInfo, - network: &'a mut Network<'sub, AtCl>, - sockets: Option<&'a mut SocketSet>, - ) -> nb::Result { - let mut data_service = Self { network, sockets }; - - // Check if context is active, and create if not - data_service.connect(apn_info)?; - - // At this point [`data_service`] will always have a valid and active - // data context! - - // Attempt to ingress data from every open socket, into it's internal rx - // buffer - if data_service.sockets.is_some() { - data_service.socket_ingress_all()?; - } - - Ok(data_service) - } - - #[allow(unused_variables)] - fn connect(&mut self, apn_info: &APNInfo) -> nb::Result<(), Error> { - match self.network.context_state { - ContextState::Active => return Ok(()), - ContextState::Setup | ContextState::Activating => {} - } - - // This step _shouldn't_ be necessary. However, for reasons I don't - // understand, SARA-R4 can be registered but not attached (i.e. AT+CGATT - // returns 0) on both RATs (unh?). Phil Ware, who knows about these - // things, always goes through (a) register, (b) wait for AT+CGATT to - // return 1 and then (c) check that a context is active with AT+CGACT or - // using AT+UPSD (even for EUTRAN). Since this sequence works for both - // RANs, it is best to be consistent. - self.attach_network()?; - - // Activate the context - #[cfg(feature = "upsd-context-activation")] - self.activate_context_upsd(PROFILE_ID, apn_info)?; - #[cfg(not(feature = "upsd-context-activation"))] - self.activate_context(CONTEXT_ID, PROFILE_ID)?; - - Ok(()) - } - - // Make sure we are attached to the cellular network. - fn attach_network(&mut self) -> nb::Result<(), Error> { - // Wait for AT+CGATT to return 1 - for _ in 0..10 { - let GPRSAttached { state } = self - .network - .send_internal(&GetGPRSAttached, true) - .map_err(Error::from)?; - - if state == GPRSAttachedState::Attached { - return Ok(()); - } - - BlockingTimer::after(Duration::from_secs(1)).wait(); - } - - // self.network .send_internal( &SetGPRSAttached { state: - // GPRSAttachedState::Attached, }, true, ) .map_err(Error::from)?; - - Err(nb::Error::WouldBlock) - } - - /// Activate context using AT+UPSD commands - /// Required for SARA-G3, SARA-U2 SARA-R5 modules. - #[cfg(feature = "upsd-context-activation")] - fn activate_context_upsd( - &mut self, - profile_id: ProfileId, - apn_info: &APNInfo, - ) -> nb::Result<(), Error> { - if self.network.context_state == ContextState::Active { - return Ok(()); - } - - // Check if the PSD profile is activated (param_tag = 1) - let PacketSwitchedNetworkData { param_tag, .. } = self - .network - .send_internal( - &GetPacketSwitchedNetworkData { - profile_id, - param: PacketSwitchedNetworkDataParam::PsdProfileStatus, - }, - true, - ) - .map_err(Error::from)?; - - if param_tag == 0 { - self.network.context_state = ContextState::Activating; - - // SARA-U2 pattern: everything is done through AT+UPSD - // Set up the APN - if let Apn::Given(apn) = apn_info.clone().apn { - self.network - .send_internal( - &SetPacketSwitchedConfig { - profile_id, - param: PacketSwitchedParam::APN(apn), - }, - true, - ) - .map_err(Error::from)?; - } - - // Set up the user name - if let Some(user_name) = apn_info.clone().user_name { - self.network - .send_internal( - &SetPacketSwitchedConfig { - profile_id, - param: PacketSwitchedParam::Username(user_name), - }, - true, - ) - .map_err(Error::from)?; - } - - // Set up the password - if let Some(password) = apn_info.clone().password { - self.network - .send_internal( - &SetPacketSwitchedConfig { - profile_id, - param: PacketSwitchedParam::Password(password), - }, - true, - ) - .map_err(Error::from)?; - } - - // Set up the dynamic IP address assignment. - #[cfg(not(feature = "sara-r5"))] - self.network - .send_internal( - &SetPacketSwitchedConfig { - profile_id, - param: PacketSwitchedParam::IPAddress(Ipv4Addr::unspecified().into()), - }, - true, - ) - .map_err(Error::from)?; - - // Automatic authentication protocol selection - #[cfg(not(feature = "sara-r5"))] - self.network - .send_internal( - &SetPacketSwitchedConfig { - profile_id, - param: PacketSwitchedParam::Authentication(AuthenticationType::Auto), - }, - true, - ) - .map_err(Error::from)?; - - #[cfg(not(feature = "sara-r5"))] - self.network - .send_internal( - &SetPacketSwitchedConfig { - profile_id, - param: PacketSwitchedParam::IPAddress(Ipv4Addr::unspecified().into()), - }, - true, - ) - .map_err(Error::from)?; - - #[cfg(feature = "sara-r5")] - self.network - .send_internal( - &SetPacketSwitchedConfig { - profile_id, - param: PacketSwitchedParam::ProtocolType(ProtocolType::IPv4), - }, - true, - ) - .map_err(Error::from)?; - - #[cfg(feature = "sara-r5")] - self.network - .send_internal( - &SetPacketSwitchedConfig { - profile_id, - param: PacketSwitchedParam::MapProfile(ContextId(1)), - }, - true, - ) - .map_err(Error::from)?; - - self.network - .send_internal( - &SetPacketSwitchedAction { - profile_id, - action: PacketSwitchedAction::Activate, - }, - true, - ) - .map_err(Error::from)?; - } - - self.network.context_state = ContextState::Active; - Ok(()) - } - - /// Activate context using 3GPP commands - /// Required for SARA-R4 and TOBY modules. - #[cfg(not(feature = "upsd-context-activation"))] - fn activate_context(&mut self, cid: ContextId, profile_id: ProfileId) -> nb::Result<(), Error> { - if self.network.context_state == ContextState::Active { - return Ok(()); - } - - let context_states = self - .network - .send_internal(&GetPDPContextState, true) - .map_err(Error::from)?; - - let activated = context_states - .iter() - .find_map(|state| { - if state.cid == cid { - Some(state.status == PDPContextStatus::Activated) - } else { - None - } - }) - .unwrap_or(false); - - if activated { - // Note: SARA-R4 only supports a single context at any one time and - // so doesn't require/support AT+UPSD. - #[cfg(not(any(feature = "sara-r4", feature = "lara-r6")))] - { - if let psn::responses::PacketSwitchedConfig { - param: psn::types::PacketSwitchedParam::MapProfile(context), - .. - } = self - .network - .send_internal( - &psn::GetPacketSwitchedConfig { - profile_id, - param: psn::types::PacketSwitchedParamReq::MapProfile, - }, - true, - ) - .map_err(Error::from)? - { - if context != cid { - self.network - .send_internal( - &psn::SetPacketSwitchedConfig { - profile_id, - param: psn::types::PacketSwitchedParam::MapProfile(cid), - }, - true, - ) - .map_err(Error::from)?; - - self.network - .send_internal( - &psn::GetPacketSwitchedNetworkData { - profile_id, - param: psn::types::PacketSwitchedNetworkDataParam::PsdProfileStatus, - }, - true, - ) - .map_err(Error::from)?; - } - } - - let psn::responses::PacketSwitchedNetworkData { param_tag, .. } = self - .network - .send_internal( - &psn::GetPacketSwitchedNetworkData { - profile_id, - param: psn::types::PacketSwitchedNetworkDataParam::PsdProfileStatus, - }, - true, - ) - .map_err(Error::from)?; - - if param_tag == 0 { - self.network - .send_internal( - &psn::SetPacketSwitchedAction { - profile_id, - action: psn::types::PacketSwitchedAction::Activate, - }, - true, - ) - .map_err(Error::from)?; - } - } - - self.network.context_state = ContextState::Active; - Ok(()) - } else { - self.network - .send_internal( - &SetPDPContextState { - status: PDPContextStatus::Activated, - cid: Some(cid), - }, - true, - ) - .map_err(Error::from)?; - - Err(nb::Error::WouldBlock) - } - } - - pub fn send_at(&mut self, cmd: &A) -> Result - where - A: atat::AtatCmd, - { - Ok(self.network.send_internal(cmd, true)?) - } - - pub fn handle_urc bool>(&mut self, f: F) -> Result<(), Error> { - self.network.at_tx.handle_urc(f).map_err(Error::Network) - } - - fn socket_ingress_all(&mut self) -> Result<(), Error> { - if let Some(ref mut sockets) = self.sockets { - let network = &mut self.network; - sockets - .iter_mut() - .map(|(handle, mut socket)| { - let available_data = socket.available_data(); - - if available_data == 0 { - // Check for new socket data available at regular - // intervals, just in case a URC is missed - if socket.should_update_available_data() { - match network.send_internal( - &ReadSocketData { - socket: handle, - length: 0, - }, - false, - ) { - Ok(SocketData { length, .. }) => socket.set_available_data(length), - Err(_) => socket.closed_by_remote(), - } - } - - return Ok(()); - } - - if !socket.can_recv() { - return Err(Error::BufferFull); - } - - // Request [`IngressChunkSize`] if it is available, - // otherwise request maximum available data - let wanted_len = core::cmp::min(available_data, INGRESS_CHUNK_SIZE); - // Check if socket.buffer has room for wanted_len, and - // ingress the smallest of the two - let requested_len = core::cmp::min(wanted_len, socket.rx_window()); - - let (socket_handle, mut data, len) = match socket.get_type() { - SocketType::Tcp => { - // Allow room for 2x length (Hex), and command - // overhead - let SocketData { - socket, - data, - length, - } = network.send_internal( - &ReadSocketData { - socket: handle, - length: requested_len, - }, - false, - )?; - - (socket, data, length) - } - SocketType::Udp => { - // Allow room for 2x length (Hex), and command - // overhead - let UDPSocketData { - socket, - data, - length, - .. - } = network.send_internal( - &ReadUDPSocketData { - socket: handle, - length: requested_len, - }, - false, - )?; - - (socket, data, length) - } - _ => return Err(Error::WrongSocketType), - }; - - if socket_handle != handle { - error!("WrongSocketType {:?} != {:?}", socket_handle, handle); - return Err(Error::WrongSocketType); - } - - if len == 0 { - socket.set_available_data(0); - } - - if let Some(ref mut data) = data { - let hex_mode = true; - // let hex_mode = self.config.hex_mode; - let data_len = if hex_mode { data.len() / 2 } else { data.len() }; - if len > 0 && data_len != len { - error!("BadLength {} != {}, {}", len, data_len, data.as_str()); - return Err(Error::BadLength); - } - - let demangled = if hex_mode { - hex::from_hex(unsafe { data.as_bytes_mut() }) - .map_err(|_| Error::InvalidHex)? - } else { - data.as_bytes() - }; - - let enqueued = socket.rx_enqueue_slice(demangled); - if enqueued != demangled.len() { - // This should never happen, due to the - // `requested_len` check above - error!( - "Failed to enqueue full slice of data! {} != {}", - enqueued, - demangled.len() - ); - } - } else { - return Err(Error::Socket(SocketError::Exhausted)); - } - - Ok(()) - }) - .filter_map(Result::err) - .for_each(|_e| { - // error!( "Failed to ingress data for socket! {:?}", - // Debug2Format(&e) ) - }); - Ok(()) - } else { - Err(Error::SocketMemory) - } - } -} diff --git a/ublox-cellular/src/services/data/ssl.rs b/ublox-cellular/src/services/data/ssl.rs deleted file mode 100644 index 7ac13e8..0000000 --- a/ublox-cellular/src/services/data/ssl.rs +++ /dev/null @@ -1,218 +0,0 @@ -use super::{DataService, Error}; -use crate::command::device_data_security::{ - types::{CertificateValidationLevel, SecurityDataType, SecurityProfileOperation}, - PrepareSecurityDataImport, SecurityProfileManager, SendSecurityDataImport, -}; -use atat::{atat_derive::AtatLen, blocking::AtatClient}; -use heapless::String; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, AtatLen)] -pub struct SecurityProfileId(pub u8); - -pub trait SSL { - fn import_certificate( - &mut self, - profile_id: SecurityProfileId, - name: &str, - certificate: &[u8], - ) -> Result<(), Error>; - fn import_root_ca( - &mut self, - profile_id: SecurityProfileId, - name: &str, - root_ca: &[u8], - ) -> Result<(), Error>; - fn import_private_key( - &mut self, - profile_id: SecurityProfileId, - name: &str, - private_key: &[u8], - password: Option<&str>, - ) -> Result<(), Error>; - fn enable_ssl( - &mut self, - profile_id: SecurityProfileId, - server_hostname: &str, - use_sni: bool, - ) -> Result<(), Error>; -} - -impl<'a, 'sub, AtCl, const N: usize, const L: usize> SSL for DataService<'a, 'sub, AtCl, N, L> -where - AtCl: AtatClient, -{ - fn import_certificate( - &mut self, - profile_id: SecurityProfileId, - name: &str, - certificate: &[u8], - ) -> Result<(), Error> { - assert!(name.len() < 200); - - self.network.send_internal( - &PrepareSecurityDataImport { - data_type: SecurityDataType::ClientCertificate, - data_size: certificate.len(), - internal_name: name, - password: None, - }, - true, - )?; - - self.network.send_internal( - &SendSecurityDataImport { - data: atat::serde_bytes::Bytes::new(certificate), - }, - true, - )?; - - self.network.send_internal( - &SecurityProfileManager { - profile_id, - operation: Some(SecurityProfileOperation::ClientCertificateInternalName( - String::from(name), - )), - }, - true, - )?; - - Ok(()) - } - - fn import_root_ca( - &mut self, - profile_id: SecurityProfileId, - name: &str, - root_ca: &[u8], - ) -> Result<(), Error> { - assert!(name.len() < 200); - - self.network.send_internal( - &PrepareSecurityDataImport { - data_type: SecurityDataType::TrustedRootCA, - data_size: root_ca.len(), - internal_name: name, - password: None, - }, - true, - )?; - - self.network.send_internal( - &SendSecurityDataImport { - data: atat::serde_bytes::Bytes::new(root_ca), - }, - true, - )?; - - self.network.send_internal( - &SecurityProfileManager { - profile_id, - operation: Some( - SecurityProfileOperation::TrustedRootCertificateInternalName(String::from( - name, - )), - ), - }, - true, - )?; - - Ok(()) - } - - fn import_private_key( - &mut self, - profile_id: SecurityProfileId, - name: &str, - private_key: &[u8], - password: Option<&str>, - ) -> Result<(), Error> { - assert!(name.len() < 200); - - self.network.send_internal( - &PrepareSecurityDataImport { - data_type: SecurityDataType::ClientPrivateKey, - data_size: private_key.len(), - internal_name: name, - password, - }, - true, - )?; - - self.network.send_internal( - &SendSecurityDataImport { - data: atat::serde_bytes::Bytes::new(private_key), - }, - true, - )?; - - self.network.send_internal( - &SecurityProfileManager { - profile_id, - operation: Some(SecurityProfileOperation::ClientPrivateKeyInternalName( - String::from(name), - )), - }, - true, - )?; - - Ok(()) - } - - fn enable_ssl( - &mut self, - profile_id: SecurityProfileId, - server_hostname: &str, - use_sni: bool, - ) -> Result<(), Error> { - self.network.send_internal( - &SecurityProfileManager { - profile_id, - operation: Some(SecurityProfileOperation::CertificateValidationLevel( - CertificateValidationLevel::RootCertValidationWithValidityDate, - )), - }, - true, - )?; - - self.network.send_internal( - &SecurityProfileManager { - profile_id, - operation: Some(SecurityProfileOperation::CipherSuite(0)), - }, - true, - )?; - - self.network.send_internal( - &SecurityProfileManager { - profile_id, - operation: Some(SecurityProfileOperation::ExpectedServerHostname( - String::from(server_hostname), - )), - }, - true, - )?; - - if use_sni { - self.network.send_internal( - &SecurityProfileManager { - profile_id, - operation: Some(SecurityProfileOperation::ServerNameIndication( - String::from(server_hostname), - )), - }, - true, - )?; - } else { - self.network.send_internal( - &SecurityProfileManager { - profile_id, - operation: Some(SecurityProfileOperation::ServerNameIndication(String::new())), - }, - true, - )?; - } - - Ok(()) - } -} diff --git a/ublox-cellular/src/services/data/tcp_stack.rs b/ublox-cellular/src/services/data/tcp_stack.rs deleted file mode 100644 index 476c8b8..0000000 --- a/ublox-cellular/src/services/data/tcp_stack.rs +++ /dev/null @@ -1,185 +0,0 @@ -use super::ssl::SecurityProfileId; -use super::DataService; -use super::EGRESS_CHUNK_SIZE; -use crate::command::ip_transport_layer::{ - types::{SocketProtocol, SslTlsStatus}, - CloseSocket, ConnectSocket, CreateSocket, PrepareWriteSocketDataBinary, SetSocketSslState, - WriteSocketDataBinary, -}; -use atat::blocking::AtatClient; -use embedded_nal::{SocketAddr, TcpClientStack}; -use ublox_sockets::{Error, SocketHandle, TcpSocket, TcpState}; - -impl<'a, 'sub, AtCl, const N: usize, const L: usize> TcpClientStack - for DataService<'a, 'sub, AtCl, N, L> -where - AtCl: AtatClient, -{ - type Error = Error; - - // Only return a SocketHandle to reference into the SocketSet owned by the GsmClient, - // as the Socket object itself provides no value without accessing it though the client. - type TcpSocket = SocketHandle; - - /// Open a new TCP socket to the given address and port. The socket starts in the unconnected state. - fn socket(&mut self) -> Result { - if let Some(ref mut sockets) = self.sockets { - // Check if there are any unused sockets available - if sockets.len() >= sockets.capacity() { - // Check if there are any sockets closed by remote, and close it - // if it has exceeded its timeout, in order to recycle it. - if !sockets.recycle() { - return Err(Error::SocketSetFull); - } - } - - let socket_resp = self - .network - .send_internal( - &CreateSocket { - protocol: SocketProtocol::TCP, - local_port: None, - }, - true, - ) - .map_err(|_| Error::Unaddressable)?; - - Ok(sockets.add(TcpSocket::new(socket_resp.socket.0))?) - } else { - Err(Error::Illegal) - } - } - - /// Connect to the given remote host and port. - fn connect( - &mut self, - socket: &mut Self::TcpSocket, - remote: SocketAddr, - ) -> nb::Result<(), Self::Error> { - if let Some(ref mut sockets) = self.sockets { - let mut tcp = sockets - .get::>(*socket) - .map_err(Self::Error::from)?; - - if matches!(tcp.state(), TcpState::Created) { - self.network - .send_internal( - &SetSocketSslState { - socket: *socket, - ssl_tls_status: SslTlsStatus::Enabled(SecurityProfileId(0)), - }, - true, - ) - .map_err(|_| nb::Error::Other(Error::Unaddressable))?; - - self.network - .send_internal( - &ConnectSocket { - socket: *socket, - remote_addr: remote.ip(), - remote_port: remote.port(), - }, - false, - ) - .map_err(|_| nb::Error::Other(Error::Unaddressable))?; - - tcp.set_state(TcpState::Connected(remote)); - Ok(()) - } else { - error!( - "Cannot connect socket! Socket: {:?} is in state: {:?}", - socket, - tcp.state() - ); - Err(Error::Illegal.into()) - } - } else { - Err(Error::Illegal.into()) - } - } - - /// Check if this socket is still connected - fn is_connected(&mut self, socket: &Self::TcpSocket) -> Result { - if let Some(ref mut sockets) = self.sockets { - Ok(sockets.get::>(*socket)?.is_connected()) - } else { - Err(Error::Illegal) - } - } - - /// Write to the stream. Returns the number of bytes written is returned - /// (which may be less than `buffer.len()`), or an error. - fn send( - &mut self, - socket: &mut Self::TcpSocket, - buffer: &[u8], - ) -> nb::Result { - if !self.is_connected(socket)? { - return Err(Error::SocketClosed.into()); - } - - for chunk in buffer.chunks(EGRESS_CHUNK_SIZE) { - trace!("Sending: {} bytes", chunk.len()); - self.network - .send_internal( - &PrepareWriteSocketDataBinary { - socket: *socket, - length: chunk.len(), - }, - false, - ) - .map_err(|_| nb::Error::Other(Error::Unaddressable))?; - - let response = self - .network - .send_internal( - &WriteSocketDataBinary { - data: atat::serde_bytes::Bytes::new(chunk), - }, - false, - ) - .map_err(|_| nb::Error::Other(Error::Unaddressable))?; - - if response.length != chunk.len() { - return Err(Error::BadLength.into()); - } - if &response.socket != socket { - return Err(Error::InvalidSocket.into()); - } - } - - Ok(buffer.len()) - } - - /// Read from the stream. Returns `Ok(n)`, which means `n` bytes of - /// data have been received and they have been placed in - /// `&buffer[0..n]`, or an error. - fn receive( - &mut self, - socket: &mut Self::TcpSocket, - buffer: &mut [u8], - ) -> nb::Result { - if let Some(ref mut sockets) = self.sockets { - let mut tcp = sockets - .get::>(*socket) - .map_err(Self::Error::from)?; - - Ok(tcp.recv_slice(buffer).map_err(Self::Error::from)?) - } else { - Err(Error::Illegal.into()) - } - } - - /// Close an existing TCP socket. - fn close(&mut self, socket: Self::TcpSocket) -> Result<(), Self::Error> { - if let Some(ref mut sockets) = self.sockets { - self.network - .send_internal(&CloseSocket { socket }, false) - .ok(); - sockets.remove(socket)?; - Ok(()) - } else { - Err(Error::Illegal) - } - } -} diff --git a/ublox-cellular/src/services/data/udp_stack.rs b/ublox-cellular/src/services/data/udp_stack.rs deleted file mode 100644 index 82f0b46..0000000 --- a/ublox-cellular/src/services/data/udp_stack.rs +++ /dev/null @@ -1,151 +0,0 @@ -use super::DataService; -use super::EGRESS_CHUNK_SIZE; -use crate::command::ip_transport_layer::{ - types::SocketProtocol, CloseSocket, CreateSocket, PrepareUDPSendToDataBinary, - UDPSendToDataBinary, -}; -use atat::blocking::AtatClient; -use embedded_nal::{SocketAddr, UdpClientStack}; -use ublox_sockets::{Error, SocketHandle, UdpSocket}; - -impl<'a, 'sub, AtCl, const N: usize, const L: usize> UdpClientStack - for DataService<'a, 'sub, AtCl, N, L> -where - AtCl: AtatClient, -{ - type Error = Error; - - // Only return a SocketHandle to reference into the SocketSet owned by the GsmClient, - // as the Socket object itself provides no value without accessing it though the client. - type UdpSocket = SocketHandle; - - /// Open a new UDP socket to the given address and port. UDP is connectionless, - /// so unlike `TcpStack` no `connect()` is required. - fn socket(&mut self) -> Result { - if let Some(ref mut sockets) = self.sockets { - if sockets.len() >= sockets.capacity() { - // Check if there are any sockets closed by remote, and close it - // if it has exceeded its timeout, in order to recycle it. - if !sockets.recycle() { - return Err(Error::SocketSetFull); - } - } - - let socket_resp = self - .network - .send_internal( - &CreateSocket { - protocol: SocketProtocol::UDP, - local_port: None, - }, - false, - ) - .map_err(|_| Error::Unaddressable)?; - - Ok(sockets.add(UdpSocket::new(socket_resp.socket.0))?) - } else { - Err(Error::Illegal) - } - } - - fn connect( - &mut self, - socket: &mut Self::UdpSocket, - remote: SocketAddr, - ) -> Result<(), Self::Error> { - if let Some(ref mut sockets) = self.sockets { - let mut udp = sockets - .get::>(*socket) - .map_err(Self::Error::from)?; - udp.bind(remote).map_err(Self::Error::from)?; - Ok(()) - } else { - Err(Error::Illegal) - } - } - - /// Send a datagram to the remote host. - fn send(&mut self, socket: &mut Self::UdpSocket, buffer: &[u8]) -> nb::Result<(), Self::Error> { - if let Some(ref mut sockets) = self.sockets { - let udp = sockets - .get::>(*socket) - .map_err(Self::Error::from)?; - - if !udp.is_open() { - return Err(Error::SocketClosed.into()); - } - - for chunk in buffer.chunks(EGRESS_CHUNK_SIZE) { - trace!("Sending: {} bytes", chunk.len()); - let endpoint = udp.endpoint().ok_or(Error::SocketClosed)?; - self.network - .send_internal( - &PrepareUDPSendToDataBinary { - socket: *socket, - remote_addr: endpoint.ip(), - remote_port: endpoint.port(), - length: chunk.len(), - }, - false, - ) - .map_err(|_| nb::Error::Other(Error::Unaddressable))?; - - let response = self - .network - .send_internal( - &UDPSendToDataBinary { - data: atat::serde_bytes::Bytes::new(chunk), - }, - false, - ) - .map_err(|_| nb::Error::Other(Error::Unaddressable))?; - - if response.length != chunk.len() { - return Err(Error::BadLength.into()); - } - if &response.socket != socket { - return Err(Error::InvalidSocket.into()); - } - } - - Ok(()) - } else { - Err(Error::Illegal.into()) - } - } - - /// Read a datagram the remote host has sent to us. Returns `Ok(n)`, which - /// means a datagram of size `n` has been received and it has been placed - /// in `&buffer[0..n]`, or an error. - fn receive( - &mut self, - socket: &mut Self::UdpSocket, - buffer: &mut [u8], - ) -> nb::Result<(usize, SocketAddr), Self::Error> { - if let Some(ref mut sockets) = self.sockets { - let mut udp = sockets - .get::>(*socket) - .map_err(Self::Error::from)?; - - let bytes = udp.recv_slice(buffer).map_err(Self::Error::from)?; - - let endpoint = udp.endpoint().ok_or(Error::SocketClosed)?; - Ok((bytes, endpoint)) - } else { - Err(Error::Illegal.into()) - } - } - - /// Close an existing UDP socket. - fn close(&mut self, socket: Self::UdpSocket) -> Result<(), Self::Error> { - if let Some(ref mut sockets) = self.sockets { - self.network - .send_internal(&CloseSocket { socket }, false) - .ok(); - sockets.remove(socket)?; - Ok(()) - } else { - Err(Error::Illegal) - } - } -} diff --git a/ublox-cellular/src/services/mod.rs b/ublox-cellular/src/services/mod.rs deleted file mode 100644 index 7a345e4..0000000 --- a/ublox-cellular/src/services/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod data;