From 005bcd89cee62edce16f1e9a16b303579193b318 Mon Sep 17 00:00:00 2001 From: Mathias Koch Date: Thu, 18 Jul 2024 09:14:06 +0200 Subject: [PATCH] feature(*): Full async implementation (#82) * initial commit - tests not working * Initial work on async client * Working async network management & TCP socket stack * Attempt to rid channel * Working dns + tcp at 11/6kbps * Add performance testing example * Comment out UartExt trait * Update to latest embassy and embedded-io 0.5 * Update dependencies and fix tests * Add TlsSocket * Fix clippy warnings * Fix TlsSocket and module restart with EDM * Reduce stack usage from holding large resources across await points * Correctly handle closing a dropped socket in FinWait1 state * Add support for PPP mode (#81) * Simplify initialization of both ppp mode and ublox mode, by providing batteries included new functions that sets up ATAT and all related resources * Refactor async completely for a more intuitive API. URCs over PPP UDP socket is still not working properly * Bump embassy-sync to 0.6 * Fix internal-network-stack compiling * Rework runner, add Proxy client and add working Control handle * Working control handle for connect and disconnect, with ppp udp bridge * Add a large number of convenience functions to Control and cleanup runner patterns * Fix defmt feature gating --------- Co-authored-by: unizippro --- .github/workflows/ci.yml | 87 +- .gitignore | 1 - .vscode/extensions.json | 11 + .vscode/settings.json | 18 + Cargo.toml | 87 +- Design_diagram.drawio | 1 - Design_diagram.png | Bin 135242 -> 0 bytes README.md | 38 +- examples/linux.rs | 152 --- examples/rpi-pico/.cargo/config.toml | 8 + examples/rpi-pico/.vscode/settings.json | 16 + examples/rpi-pico/Cargo.toml | 113 +++ examples/rpi-pico/build.rs | 36 + examples/rpi-pico/memory.x | 5 + examples/rpi-pico/rust-toolchain.toml | 7 + examples/rpi-pico/src/bin/embassy-async.rs | 297 ++++++ examples/rpi-pico/src/bin/embassy-perf.rs | 316 ++++++ .../rpi-pico/src/bin/embassy-smoltcp-ppp.rs | 196 ++++ rust-toolchain.toml | 6 +- src/asynch/at_udp_socket.rs | 70 ++ src/asynch/control.rs | 709 ++++++++++++++ src/asynch/mod.rs | 20 + src/asynch/network.rs | 321 ++++++ src/asynch/resources.rs | 39 + src/asynch/runner.rs | 486 +++++++++ src/asynch/state.rs | 216 ++++ src/asynch/ublox_stack/device.rs | 11 + src/asynch/ublox_stack/dns.rs | 183 ++++ src/asynch/ublox_stack/mod.rs | 529 ++++++++++ .../asynch/ublox_stack}/peer_builder.rs | 106 +- src/asynch/ublox_stack/tcp.rs | 831 ++++++++++++++++ src/asynch/ublox_stack/tls.rs | 435 ++++++++ src/asynch/ublox_stack/udp.rs | 247 +++++ .../src => src}/command/custom_digest.rs | 56 +- .../src => src}/command/data_mode/mod.rs | 13 +- .../command/data_mode/responses.rs | 14 +- .../src => src}/command/data_mode/types.rs | 13 + .../src => src}/command/data_mode/urc.rs | 22 +- .../src => src}/command/edm/mod.rs | 189 ++-- .../src => src}/command/edm/types.rs | 31 +- .../src => src}/command/edm/urc.rs | 27 +- .../src => src}/command/ethernet/mod.rs | 0 .../src => src}/command/ethernet/responses.rs | 0 .../src => src}/command/ethernet/types.rs | 2 +- .../src => src}/command/ethernet/urc.rs | 0 .../src => src}/command/general/mod.rs | 12 +- .../src => src}/command/general/responses.rs | 6 +- .../src => src}/command/general/types.rs | 3 +- .../src => src}/command/gpio/mod.rs | 2 +- .../src => src}/command/gpio/responses.rs | 4 +- .../src => src}/command/gpio/types.rs | 0 {ublox-short-range/src => src}/command/mod.rs | 36 +- .../src => src}/command/network/mod.rs | 2 +- .../src => src}/command/network/responses.rs | 0 .../src => src}/command/network/types.rs | 2 +- .../src => src}/command/network/urc.rs | 0 .../src => src}/command/ping/mod.rs | 0 .../src => src}/command/ping/types.rs | 7 +- .../src => src}/command/ping/urc.rs | 6 +- .../src => src}/command/security/mod.rs | 2 +- .../src => src}/command/security/responses.rs | 0 .../src => src}/command/security/types.rs | 0 .../src => src}/command/system/mod.rs | 16 +- .../src => src}/command/system/responses.rs | 4 +- .../src => src}/command/system/types.rs | 13 +- .../src => src}/command/wifi/mod.rs | 33 +- .../src => src}/command/wifi/responses.rs | 0 .../src => src}/command/wifi/types.rs | 56 +- .../src => src}/command/wifi/urc.rs | 0 src/config.rs | 31 + src/connection.rs | 87 ++ {ublox-short-range/src => src}/error.rs | 30 +- src/fmt.rs | 274 ++++++ {ublox-short-range/src => src}/hex.rs | 0 src/lib.rs | 34 + .../src/wifi => src}/network.rs | 25 +- ublox-short-range/Cargo.toml | 56 -- ublox-short-range/src/blocking_timer.rs | 21 - ublox-short-range/src/client.rs | 925 ------------------ ublox-short-range/src/config.rs | 94 -- ublox-short-range/src/lib.rs | 32 - ublox-short-range/src/wifi/ap.rs | 228 ----- ublox-short-range/src/wifi/connection.rs | 61 -- ublox-short-range/src/wifi/dns.rs | 63 -- ublox-short-range/src/wifi/mod.rs | 122 --- ublox-short-range/src/wifi/options.rs | 136 --- ublox-short-range/src/wifi/sta.rs | 220 ----- ublox-short-range/src/wifi/tcp_stack.rs | 245 ----- ublox-short-range/src/wifi/tls.rs | 107 -- ublox-short-range/src/wifi/udp_stack.rs | 411 -------- 90 files changed, 6078 insertions(+), 3293 deletions(-) create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json delete mode 100644 Design_diagram.drawio delete mode 100644 Design_diagram.png delete mode 100644 examples/linux.rs create mode 100644 examples/rpi-pico/.cargo/config.toml create mode 100644 examples/rpi-pico/.vscode/settings.json create mode 100644 examples/rpi-pico/Cargo.toml create mode 100644 examples/rpi-pico/build.rs create mode 100644 examples/rpi-pico/memory.x create mode 100644 examples/rpi-pico/rust-toolchain.toml create mode 100644 examples/rpi-pico/src/bin/embassy-async.rs create mode 100644 examples/rpi-pico/src/bin/embassy-perf.rs create mode 100644 examples/rpi-pico/src/bin/embassy-smoltcp-ppp.rs create mode 100644 src/asynch/at_udp_socket.rs create mode 100644 src/asynch/control.rs create mode 100644 src/asynch/mod.rs create mode 100644 src/asynch/network.rs create mode 100644 src/asynch/resources.rs create mode 100644 src/asynch/runner.rs create mode 100644 src/asynch/state.rs create mode 100644 src/asynch/ublox_stack/device.rs create mode 100644 src/asynch/ublox_stack/dns.rs create mode 100644 src/asynch/ublox_stack/mod.rs rename {ublox-short-range/src/wifi => src/asynch/ublox_stack}/peer_builder.rs (53%) create mode 100644 src/asynch/ublox_stack/tcp.rs create mode 100644 src/asynch/ublox_stack/tls.rs create mode 100644 src/asynch/ublox_stack/udp.rs rename {ublox-short-range/src => src}/command/custom_digest.rs (89%) rename {ublox-short-range/src => src}/command/data_mode/mod.rs (93%) rename {ublox-short-range/src => src}/command/data_mode/responses.rs (64%) rename {ublox-short-range/src => src}/command/data_mode/types.rs (92%) rename {ublox-short-range/src => src}/command/data_mode/urc.rs (52%) rename {ublox-short-range/src => src}/command/edm/mod.rs (68%) rename {ublox-short-range/src => src}/command/edm/types.rs (90%) rename {ublox-short-range/src => src}/command/edm/urc.rs (93%) rename {ublox-short-range/src => src}/command/ethernet/mod.rs (100%) rename {ublox-short-range/src => src}/command/ethernet/responses.rs (100%) rename {ublox-short-range/src => src}/command/ethernet/types.rs (99%) rename {ublox-short-range/src => src}/command/ethernet/urc.rs (100%) rename {ublox-short-range/src => src}/command/general/mod.rs (87%) rename {ublox-short-range/src => src}/command/general/responses.rs (90%) rename {ublox-short-range/src => src}/command/general/types.rs (98%) rename {ublox-short-range/src => src}/command/gpio/mod.rs (95%) rename {ublox-short-range/src => src}/command/gpio/responses.rs (83%) rename {ublox-short-range/src => src}/command/gpio/types.rs (100%) rename {ublox-short-range/src => src}/command/mod.rs (84%) rename {ublox-short-range/src => src}/command/network/mod.rs (97%) rename {ublox-short-range/src => src}/command/network/responses.rs (100%) rename {ublox-short-range/src => src}/command/network/types.rs (99%) rename {ublox-short-range/src => src}/command/network/urc.rs (100%) rename {ublox-short-range/src => src}/command/ping/mod.rs (100%) rename {ublox-short-range/src => src}/command/ping/types.rs (94%) rename {ublox-short-range/src => src}/command/ping/urc.rs (93%) rename {ublox-short-range/src => src}/command/security/mod.rs (99%) rename {ublox-short-range/src => src}/command/security/responses.rs (100%) rename {ublox-short-range/src => src}/command/security/types.rs (100%) rename {ublox-short-range/src => src}/command/system/mod.rs (95%) rename {ublox-short-range/src => src}/command/system/responses.rs (92%) rename {ublox-short-range/src => src}/command/system/types.rs (96%) rename {ublox-short-range/src => src}/command/wifi/mod.rs (97%) rename {ublox-short-range/src => src}/command/wifi/responses.rs (100%) rename {ublox-short-range/src => src}/command/wifi/types.rs (97%) rename {ublox-short-range/src => src}/command/wifi/urc.rs (100%) create mode 100644 src/config.rs create mode 100644 src/connection.rs rename {ublox-short-range/src => src}/error.rs (79%) create mode 100644 src/fmt.rs rename {ublox-short-range/src => src}/hex.rs (100%) create mode 100644 src/lib.rs rename {ublox-short-range/src/wifi => src}/network.rs (67%) delete mode 100644 ublox-short-range/Cargo.toml delete mode 100644 ublox-short-range/src/blocking_timer.rs delete mode 100644 ublox-short-range/src/client.rs delete mode 100644 ublox-short-range/src/config.rs delete mode 100644 ublox-short-range/src/lib.rs delete mode 100644 ublox-short-range/src/wifi/ap.rs delete mode 100644 ublox-short-range/src/wifi/connection.rs delete mode 100644 ublox-short-range/src/wifi/dns.rs delete mode 100644 ublox-short-range/src/wifi/mod.rs delete mode 100644 ublox-short-range/src/wifi/options.rs delete mode 100644 ublox-short-range/src/wifi/sta.rs delete mode 100644 ublox-short-range/src/wifi/tcp_stack.rs delete mode 100644 ublox-short-range/src/wifi/tls.rs delete mode 100644 ublox-short-range/src/wifi/udp_stack.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0b79afc..4ea3ed4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,56 +1,81 @@ +name: CI + on: push: branches: - master pull_request: -name: CI - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true +defaults: + run: + shell: bash jobs: - test: - name: Build & Test + rustfmt: + name: rustfmt runs-on: ubuntu-latest steps: - name: Checkout source code - uses: actions/checkout@v3 - - uses: dsherret/rust-toolchain-file@v1 - - name: Build - uses: actions-rs/cargo@v1 + uses: actions/checkout@v2 + + - name: Install Rust + uses: actions-rs/toolchain@v1 with: - command: build - args: --all --target thumbv7em-none-eabihf + profile: minimal + toolchain: nightly + override: true + components: rustfmt - - name: Test + - name: Run rustfmt uses: actions-rs/cargo@v1 with: - command: test - args: --lib - env: - DEFMT_LOG: off - - rustfmt: - name: rustfmt - runs-on: ubuntu-latest - steps: - - name: Checkout source code - uses: actions/checkout@v3 - - uses: dsherret/rust-toolchain-file@v1 - - name: Rustfmt - run: cargo fmt -- --check + command: fmt + args: --all -- --check --verbose clippy: name: clippy runs-on: ubuntu-latest steps: - name: Checkout source code - uses: actions/checkout@v3 - - uses: dsherret/rust-toolchain-file@v1 + uses: actions/checkout@v2 + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + components: clippy + - name: Run clippy uses: actions-rs/clippy-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} - args: -- ${{ env.CLIPPY_PARAMS }} + args: --features odin-w2xx,ppp + + test: + name: Test + runs-on: ubuntu-latest + steps: + - name: Checkout source code + uses: actions/checkout@v2 + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + target: thumbv7m-none-eabi + override: true + + - name: Build + uses: actions-rs/cargo@v1 + with: + command: build + args: --all --target thumbv7m-none-eabi --features odin-w2xx,ppp + + - name: Test + uses: actions-rs/cargo@v1 + with: + command: test + args: --lib --features odin-w2xx,ppp diff --git a/.gitignore b/.gitignore index a1c9e07..1d240a8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,4 @@ *.fifo target/ *.o -.vscode Cargo.lock \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..78f5c6d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,11 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. + // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp + // List of extensions which should be recommended for users of this workspace. + "recommendations": [ + "rust-lang.rust-analyzer", + "tamasfe.even-better-toml", + ], + // List of extensions recommended by VS Code that should not be recommended for users of this workspace. + "unwantedRecommendations": [] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..8fc7548 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,18 @@ +{ + "editor.formatOnSave": true, + "[toml]": { + "editor.formatOnSave": false + }, + "rust-analyzer.cargo.target": "thumbv6m-none-eabi", + "rust-analyzer.check.allTargets": false, + "rust-analyzer.linkedProjects": [], + "rust-analyzer.cargo.features": [ + "odin-w2xx", + // "internal-network-stack" + "ppp" + ], + "rust-analyzer.server.extraEnv": { + "WIFI_NETWORK": "foo", + "WIFI_PASSWORD": "foo", + } +} \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index c73ac94..650b27a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,88 @@ +[package] +name = "ublox-short-range-rs" +version = "0.1.1" +authors = ["Mads Andresen "] +description = "Driver crate for u-blox short range devices, implementation follows 'UBX-14044127 - R40'" +readme = "../README.md" +keywords = ["ublox", "wifi", "shortrange", "bluetooth"] +categories = ["embedded", "no-std"] +license = "MIT OR Apache-2.0" +repository = "https://github.com/BlackbirdHQ/ublox-short-range-rs" +edition = "2021" + +[lib] +name = "ublox_short_range" +doctest = false + +[dependencies] +atat = { version = "0.23", features = ["derive", "bytes"] } + +heapless = { version = "^0.8", features = ["serde"] } +no-std-net = { version = "0.6", 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", rev = "9f7fe54", optional = true } +portable-atomic = "1.6" + +log = { version = "^0.4", default-features = false, optional = true } +defmt = { version = "^0.3", optional = true } + +embedded-hal = "1.0" +embassy-time = "0.3" +embassy-sync = "0.6" +embassy-futures = "0.1" + +embedded-nal-async = { version = "0.7" } +futures-util = { version = "0.3.29", default-features = false } + +embedded-io-async = "0.6" + +embassy-net-ppp = { version = "0.1", optional = true } +embassy-net = { version = "0.4", features = [ + "proto-ipv4", + "medium-ip", +], optional = true } + + +[features] +default = ["socket-tcp", "socket-udp"] + +internal-network-stack = ["dep:ublox-sockets", "edm"] +edm = ["ublox-sockets?/edm"] + +ipv6 = ["embassy-net?/proto-ipv6"] + +# PPP mode requires UDP sockets enabled, to be able to do AT commands over UDP port 23 +ppp = ["dep:embassy-net-ppp", "dep:embassy-net", "socket-udp"] + +socket-tcp = ["ublox-sockets?/socket-tcp", "embassy-net?/tcp"] +socket-udp = ["ublox-sockets?/socket-udp", "embassy-net?/udp"] + +defmt = [ + "dep:defmt", + "heapless/defmt-03", + "atat/defmt", + "ublox-sockets?/defmt", + "embassy-net-ppp?/defmt", + "embassy-net?/defmt", +] +log = ["dep:log", "ublox-sockets?/log", "atat/log"] + +# Supported Ublox modules +odin-w2xx = [] +nina-w1xx = [] +nina-b1xx = [] +anna-b1xx = [] +nina-b2xx = [] +nina-b3xx = [] + [workspace] -resolver = "2" -members = [ "ublox-short-range" ] +members = [] +default-members = ["."] +exclude = ["examples"] [patch.crates-io] -atat = { git = "https://github.com/BlackbirdHQ/atat", rev = "c5caaf7" } +no-std-net = { git = "https://github.com/rushmorem/no-std-net", branch = "issue-15" } +atat = { git = "https://github.com/BlackbirdHQ/atat", rev = "a466836" } +# atat = { path = "../atat/atat" } \ No newline at end of file diff --git a/Design_diagram.drawio b/Design_diagram.drawio deleted file mode 100644 index 016b7ea..0000000 --- a/Design_diagram.drawio +++ /dev/null @@ -1 +0,0 @@ -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 \ No newline at end of file diff --git a/Design_diagram.png b/Design_diagram.png deleted file mode 100644 index e73bc905ee4f8cfc3c2d60fcd095a2ec59f0b7fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 135242 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

(_#$PkcfV+@iQa8 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 Z@|`iG@k(En + [no-std-badge]: https://img.shields.io/badge/no__std-yes-blue [test]: https://github.com/BlackbirdHQ/ublox-short-range-rs/workflows/Test/badge.svg [codecov-badge]: https://codecov.io/gh/BlackbirdHQ/ublox-short-range-rs/branch/master/graph/badge.svg diff --git a/examples/linux.rs b/examples/linux.rs deleted file mode 100644 index 52804e9..0000000 --- a/examples/linux.rs +++ /dev/null @@ -1,152 +0,0 @@ -// use std::sync::{Arc, Mutex}; -// use std::thread; -// use std::time::Duration; - -// use linux_embedded_hal::Serial; -// use serial::{self, core::SerialPort}; - -// extern crate at_rs as at; -// extern crate env_logger; -// extern crate nb; - -// // Note this useful idiom: importing names from outer (for mod tests) scope. -// use ublox_short_range::command::*; -// use ublox_short_range::prelude::*; -// use ublox_short_range::wifi; - -// use heapless::{consts::*, spsc::Queue, String}; -// #[allow(unused_imports)] -// use defmt::{error, info, warn}; - -// #[derive(Clone, Copy)] -// struct MilliSeconds(u32); - -// trait U32Ext { -// fn s(self) -> MilliSeconds; -// fn ms(self) -> MilliSeconds; -// } - -// impl U32Ext for u32 { -// fn s(self) -> MilliSeconds { -// MilliSeconds(self / 1000) -// } -// fn ms(self) -> MilliSeconds { -// MilliSeconds(self) -// } -// } - -// struct Timer; - -// impl embedded_hal::timer::CountDown for Timer { -// type Time = MilliSeconds; -// fn start(&mut self, _duration: T) -// where -// T: Into, -// { -// // let dur = duration.into(); -// // self.timeout_time = Instant::now().checked_add(Duration::from_millis(dur.0.into())).expect(""); -// } - -// fn wait(&mut self) -> ::nb::Result<(), void::Void> { -// // if self.timeout_time - Instant::now() < Duration::from_secs(0) { -// // Ok(()) -// // } else { -// Err(nb::Error::WouldBlock) -// // } -// } -// } - -// impl embedded_hal::timer::Cancel for Timer { -// type Error = (); -// fn cancel(&mut self) -> Result<(), Self::Error> { -// Ok(()) -// } -// } - -// type SerialRxBufferLen = U4096; -// type ATRequestQueueLen = U5; -// type ATResponseQueueLen = U5; - -// static mut WIFI_REQ_Q: Option> = None; -// static mut WIFI_RES_Q: Option, ATResponseQueueLen, u8>> = -// None; - -// fn main() { -// env_logger::builder() -// .filter_level(defmt::LevelFilter::Trace) -// .init(); - -// // Serial port settings -// let settings = serial::PortSettings { -// baud_rate: serial::Baud115200, -// char_size: serial::Bits8, -// parity: serial::ParityNone, -// stop_bits: serial::Stop1, -// flow_control: serial::FlowNone, -// }; - -// // Open serial port -// let mut port = serial::open("/dev/ttyACM0").expect("Could not open serial port"); -// port.configure(&settings) -// .expect("Could not configure serial port"); - -// port.set_timeout(Duration::from_millis(2)) -// .expect("Could not set serial port timeout"); - -// unsafe { WIFI_REQ_Q = Some(Queue::u8()) }; -// unsafe { WIFI_RES_Q = Some(Queue::u8()) }; - -// let (wifi_client, parser) = at::new::<_, _, _, SerialRxBufferLen, _, _>( -// unsafe { (WIFI_REQ_Q.as_mut().unwrap(), WIFI_RES_Q.as_mut().unwrap()) }, -// Serial(port), -// Timer, -// 1000.ms(), -// ); - -// let ublox = ublox_short_range::UbloxClient::new(wifi_client); - -// let at_parser_arc = Arc::new(Mutex::new(parser)); - -// let at_parser = at_parser_arc.clone(); -// let serial_irq = thread::Builder::new() -// .name("serial_irq".to_string()) -// .spawn(move || loop { -// thread::sleep(Duration::from_millis(1)); -// if let Ok(mut at) = at_parser.lock() { -// at.handle_irq() -// } -// }) -// .unwrap(); - -// let serial_loop = thread::Builder::new() -// .name("serial_loop".to_string()) -// .spawn(move || loop { -// thread::sleep(Duration::from_millis(100)); -// if let Ok(mut at) = at_parser_arc.lock() { -// at.spin() -// } -// }) -// .unwrap(); - -// let main_loop = thread::Builder::new() -// .name("main_loop".to_string()) -// .spawn(move || { -// // let networks = wifi_client.scan().unwrap(); -// // networks.iter().for_each(|n| info!("{:?}", n.ssid)); - -// let options = wifi::options::ConnectionOptions::new() -// .ssid(String::from("E-NET1")) -// .password(String::from("pakhus47")); - -// // Attempt to connect to a wifi -// let connection = ublox.connect(options).expect("Cannot connect!"); -// info!("Connected! {:?}", connection.network); -// }) -// .unwrap(); - -// // needed otherwise it does not block till -// // the threads actually have been run -// serial_irq.join().unwrap(); -// serial_loop.join().unwrap(); -// main_loop.join().unwrap(); -// } diff --git a/examples/rpi-pico/.cargo/config.toml b/examples/rpi-pico/.cargo/config.toml new file mode 100644 index 0000000..f7e22c1 --- /dev/null +++ b/examples/rpi-pico/.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 = "debug,atat=warn" \ No newline at end of file diff --git a/examples/rpi-pico/.vscode/settings.json b/examples/rpi-pico/.vscode/settings.json new file mode 100644 index 0000000..e786a02 --- /dev/null +++ b/examples/rpi-pico/.vscode/settings.json @@ -0,0 +1,16 @@ +{ + "editor.formatOnSave": true, + "[toml]": { + "editor.formatOnSave": false + }, + "rust-analyzer.cargo.features": [ + "ppp", + ], + "rust-analyzer.cargo.target": "thumbv6m-none-eabi", + "rust-analyzer.check.allTargets": false, + "rust-analyzer.linkedProjects": [], + "rust-analyzer.server.extraEnv": { + "WIFI_NETWORK": "foo", + "WIFI_PASSWORD": "foo", + } +} \ No newline at end of file diff --git a/examples/rpi-pico/Cargo.toml b/examples/rpi-pico/Cargo.toml new file mode 100644 index 0000000..3a26943 --- /dev/null +++ b/examples/rpi-pico/Cargo.toml @@ -0,0 +1,113 @@ +[package] +name = "ublox-short-range-examples-rpi-pico" +version = "0.1.0" +edition = "2021" + + +[dependencies] +ublox-short-range-rs = { path = "../../", features = ["odin-w2xx", "defmt"] } +embassy-executor = { version = "0.5", features = [ + "defmt", + "integrated-timers", + "nightly", + "arch-cortex-m", + "executor-thread", +] } +embassy-time = { version = "0.3", features = [ + "defmt", + "defmt-timestamp-uptime", +] } +embassy-sync = { version = "0.6" } +embassy-rp = { version = "0.1.0", features = [ + "defmt", + "unstable-pac", + "time-driver", +] } +embassy-futures = { version = "0.1.0" } +no-std-net = { version = "0.6", features = ["serde"] } + +static_cell = { version = "2", features = ["nightly"] } +defmt = "0.3.4" +defmt-rtt = "0.4" +panic-probe = { version = "0.3", features = ["print-defmt"] } + +cortex-m = { version = "0.7.6", features = ["critical-section-single-core"] } +cortex-m-rt = "0.7.0" +futures = { version = "0.3.17", default-features = false, features = [ + "async-await", + "cfg-target-has-atomic", + "unstable", +] } + +embedded-io-async = { version = "0.6" } +heapless = "0.8" +portable-atomic = { version = "*", features = ["unsafe-assume-single-core"] } + +embassy-net = { version = "0.4", optional = true, features = [ + "defmt", + "proto-ipv4", + "medium-ip", + "tcp", + "udp", + "dns" +] } +embassy-net-ppp = { version = "0.1", optional = true, features = ["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 } +embedded-tls = { path = "../../../embedded-tls", default-features = false, features = ["defmt"] } + + +[features] +internal-network-stack = ["ublox-short-range-rs/internal-network-stack"] +ppp = ["dep:embassy-net", "dep:embassy-net-ppp", "ublox-short-range-rs/ppp"] + +[patch.crates-io] +# embassy-executor = { git = "https://github.com/embassy-rs/embassy", rev = "03d6363d5af5dcaf21b52734994a466ca593d2b6" } +# embassy-time = { git = "https://github.com/embassy-rs/embassy", rev = "03d6363d5af5dcaf21b52734994a466ca593d2b6" } +# embassy-futures = { git = "https://github.com/embassy-rs/embassy", rev = "03d6363d5af5dcaf21b52734994a466ca593d2b6" } +# embassy-sync = { git = "https://github.com/embassy-rs/embassy", rev = "03d6363d5af5dcaf21b52734994a466ca593d2b6" } +# embassy-rp = { git = "https://github.com/embassy-rs/embassy", rev = "03d6363d5af5dcaf21b52734994a466ca593d2b6" } +# embassy-net-driver-channel = { git = "https://github.com/embassy-rs/embassy", rev = "03d6363d5af5dcaf21b52734994a466ca593d2b6" } + + +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" } +ublox-sockets = { path = "../../../ublox-sockets" } +no-std-net = { path = "../../../no-std-net" } +atat = { path = "../../../atat/atat" } + +[profile.dev] +debug = 2 +debug-assertions = true +opt-level = 1 +overflow-checks = true + +[profile.release] +codegen-units = 1 +debug = 2 +debug-assertions = false +incremental = false +lto = 'fat' +opt-level = 'z' +overflow-checks = false + +# do not optimize proc-macro crates = faster builds from scratch +[profile.dev.build-override] +codegen-units = 8 +debug = false +debug-assertions = false +opt-level = 0 +overflow-checks = false + +[profile.release.build-override] +codegen-units = 8 +debug = false +debug-assertions = false +opt-level = 0 +overflow-checks = false diff --git a/examples/rpi-pico/build.rs b/examples/rpi-pico/build.rs new file mode 100644 index 0000000..3f915f9 --- /dev/null +++ b/examples/rpi-pico/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/rpi-pico/memory.x b/examples/rpi-pico/memory.x new file mode 100644 index 0000000..eb8c173 --- /dev/null +++ b/examples/rpi-pico/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/rpi-pico/rust-toolchain.toml b/examples/rpi-pico/rust-toolchain.toml new file mode 100644 index 0000000..4e3b270 --- /dev/null +++ b/examples/rpi-pico/rust-toolchain.toml @@ -0,0 +1,7 @@ +[toolchain] +channel = "nightly-2024-01-17" +components = [ "rust-src", "rustfmt", "llvm-tools" ] +targets = [ + "thumbv6m-none-eabi", + "thumbv7em-none-eabihf" +] diff --git a/examples/rpi-pico/src/bin/embassy-async.rs b/examples/rpi-pico/src/bin/embassy-async.rs new file mode 100644 index 0000000..81090c4 --- /dev/null +++ b/examples/rpi-pico/src/bin/embassy-async.rs @@ -0,0 +1,297 @@ +#![cfg(feature = "internal-network-stack")] +#![no_std] +#![no_main] +#![feature(type_alias_impl_trait)] +#![feature(async_fn_in_trait)] +#![allow(incomplete_features)] + +use core::fmt::Write as _; +use embassy_executor::Spawner; +use embassy_futures::select::{select, Either}; +use embassy_rp::gpio::{AnyPin, Input, Level, Output, Pull}; +use embassy_rp::peripherals::{PIN_26, UART1}; +use embassy_rp::uart::{BufferedInterruptHandler, BufferedUartTx}; +use embassy_rp::{bind_interrupts, uart}; +use embassy_time::{Duration, Timer}; +use embedded_io_async::Write; +use no_std_net::{Ipv4Addr, SocketAddr}; +use static_cell::make_static; +use ublox_short_range::asynch::runner::Runner; +use ublox_short_range::asynch::ublox_stack::dns::DnsSocket; +use ublox_short_range::asynch::ublox_stack::tcp::TcpSocket; +use ublox_short_range::asynch::ublox_stack::{StackResources, UbloxStack}; +use ublox_short_range::asynch::{new, Resources, State}; +use ublox_short_range::atat::{self, AtatIngress}; +use ublox_short_range::command::custom_digest::EdmDigester; +use ublox_short_range::command::edm::urc::EdmEvent; +use ublox_short_range::embedded_nal_async::AddrType; +use {defmt_rtt as _, panic_probe as _}; + +const CMD_BUF_SIZE: usize = 128; +const INGRESS_BUF_SIZE: usize = 1024; +const URC_CAPACITY: usize = 2; + +type AtClient = ublox_short_range::atat::asynch::Client< + 'static, + uart::BufferedUartTx<'static, UART1>, + INGRESS_BUF_SIZE, +>; + +#[embassy_executor::task] +async fn wifi_task( + runner: InternalRunner< + 'a, + BufferedUartRx<'static, UART1>, + BufferedUartTx<'static, UART1>, + Output<'static, AnyPin>, + INGRESS_BUF_SIZE, + URC_CAPACITY, + >, +) -> ! { + runner.run().await +} + +#[embassy_executor::task] +async fn net_task(stack: &'static UbloxStack) -> ! { + stack.run().await +} + +#[embassy_executor::task(pool_size = 2)] +async fn echo_task( + stack: &'static UbloxStack, + hostname: &'static str, + port: u16, + write_interval: Duration, +) { + let mut rx_buffer = [0; 128]; + let mut tx_buffer = [0; 128]; + let mut buf = [0; 128]; + let mut cnt = 0u32; + let mut msg = heapless::String::<64>::new(); + Timer::after(Duration::from_secs(1)).await; + + let ip_addr = match DnsSocket::new(stack).query(hostname, AddrType::IPv4).await { + Ok(ip) => ip, + Err(_) => { + error!("[{}] Failed to resolve IP addr", hostname); + return; + } + }; + + let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); + + info!("[{}] Connecting... {}", hostname, debug2Format(&ip_addr)); + if let Err(e) = socket.connect((ip_addr, port)).await { + warn!("[{}] connect error: {:?}", hostname, e); + return; + } + info!( + "[{}] Connected to {:?}", + hostname, + debug2Format(&socket.remote_endpoint()) + ); + + loop { + match select(Timer::after(write_interval), socket.read(&mut buf)).await { + Either::First(_) => { + msg.clear(); + write!(msg, "Hello {}! {}\n", ip_addr, cnt).unwrap(); + cnt = cnt.wrapping_add(1); + if let Err(e) = socket.write_all(msg.as_bytes()).await { + warn!("[{}] write error: {:?}", hostname, e); + break; + } + info!("[{}] txd: {}", hostname, msg); + Timer::after(Duration::from_millis(400)).await; + } + Either::Second(res) => { + let n = match res { + Ok(0) => { + warn!("[{}] read EOF", hostname); + break; + } + Ok(n) => n, + Err(e) => { + warn!("[{}] {:?}", hostname, e); + break; + } + }; + info!( + "[{}] rxd {}", + hostname, + core::str::from_utf8(&buf[..n]).unwrap() + ); + } + } + } +} + +bind_interrupts!(struct Irqs { + UART1_IRQ => BufferedInterruptHandler; +}); + +#[embassy_executor::main] +async fn main(spawner: Spawner) { + info!("Hello World!"); + + let p = embassy_rp::init(Default::default()); + + let rst = Output::new(p.PIN_26, Level::High); + let mut btn = Input::new(p.PIN_27, Pull::Up); + + static TX_BUF: StaticCell<[u8; 16]> = StaticCell::new(); + static RX_BUF: StaticCell<[u8; 16]> = StaticCell::new(); + + let uart = uart::BufferedUart::new_with_rtscts( + p.UART1, + Irqs, + p.PIN_24, + p.PIN_25, + p.PIN_23, + p.PIN_22, + TX_BUF.init([0; 16]), + RX_BUF.init([0; 16]), + uart::Config::default(), + ); + let (uart_rx, uart_tx) = uart.split(); + + static RESOURCES: StaticCell< + Resources, CMD_BUF_SIZE, INGRESS_BUF_SIZE, URC_CAPACITY>, + > = StaticCell::new(); + + let (net_device, mut control, runner) = ublox_short_range::asynch::new_internal( + uart_rx, + uart_tx, + RESOURCES.init(Resources::new()), + rst, + ); + + // Init network stack + static STACK: StaticCell>> = StaticCell::new(); + static STACK_RESOURCES: StaticCell> = StaticCell::new(); + + let stack = &*STACK.init(UbloxStack::new( + net_device, + STACK_RESOURCES.init(StackResources::new()), + )); + + spawner.spawn(net_task(stack)).unwrap(); + spawner.spawn(wifi_task(runner)).unwrap(); + + control + .set_hostname("Factbird-duo-wifi-test") + .await + .unwrap(); + + // And now we can use it! + info!("Device initialized!"); + + let mut rx_buffer = [0; 256]; + let mut tx_buffer = [0; 256]; + let mut buf = [0; 256]; + let mut cnt = 0u32; + let mut msg = heapless::String::<64>::new(); + + loop { + loop { + match control.join_wpa2("test", "1234abcd").await { + Ok(_) => { + info!("Network connected!"); + spawner + .spawn(echo_task( + &stack, + // "echo.u-blox.com", + // 7, + "tcpbin.com", + 4242, + Duration::from_secs(1), + )) + .unwrap(); + + spawner + .spawn(echo_task( + &stack, + "tcpbin.com", + 4242, + Duration::from_millis(500), + )) + .unwrap(); + break; + } + Err(err) => { + info!("join failed with error={:?}. Retrying in 1 second", err); + Timer::after(Duration::from_secs(1)).await; + } + } + } + 'outer: loop { + Timer::after(Duration::from_secs(1)).await; + + let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); + // socket.set_timeout(Some(Duration::from_secs(10))); + + let remote: SocketAddr = (Ipv4Addr::new(192, 168, 1, 183), 4444).into(); + info!("Connecting... {}", debug2Format(&remote)); + if let Err(e) = socket.connect(remote).await { + warn!("connect error: {:?}", e); + continue; + } + info!("Connected to {:?}", debug2Format(&socket.remote_endpoint())); + + 'inner: loop { + match select(Timer::after(Duration::from_secs(3)), socket.read(&mut buf)).await { + Either::First(_) => { + msg.clear(); + write!(msg, "Hello world! {}\n", cnt).unwrap(); + cnt = cnt.wrapping_add(1); + if let Err(e) = socket.write_all(msg.as_bytes()).await { + warn!("write error: {:?}", e); + break; + } + info!("txd: {}", msg); + Timer::after(Duration::from_millis(400)).await; + } + Either::Second(res) => { + let n = match res { + Ok(0) => { + warn!("read EOF"); + break; + } + Ok(n) => n, + Err(e) => { + warn!("{:?}", e); + break; + } + }; + info!("rxd [{}] {}", n, core::str::from_utf8(&buf[..n]).unwrap()); + + match &buf[..n] { + b"c\n" => { + socket.close(); + break 'inner; + } + b"a\n" => { + socket.abort(); + break 'inner; + } + b"d\n" => { + drop(socket); + break 'inner; + } + b"f\n" => { + control.disconnect().await.unwrap(); + break 'outer; + } + _ => {} + } + } + } + } + info!("Press USER button to reconnect socket!"); + btn.wait_for_any_edge().await; + continue; + } + info!("Press USER button to reconnect to WiFi!"); + btn.wait_for_any_edge().await; + } +} diff --git a/examples/rpi-pico/src/bin/embassy-perf.rs b/examples/rpi-pico/src/bin/embassy-perf.rs new file mode 100644 index 0000000..630415a --- /dev/null +++ b/examples/rpi-pico/src/bin/embassy-perf.rs @@ -0,0 +1,316 @@ +#![no_std] +#![no_main] +#![feature(type_alias_impl_trait)] +#![feature(async_fn_in_trait)] +#![allow(incomplete_features)] + +use embassy_executor::Spawner; +use embassy_futures::join::join; +use embassy_rp::gpio::{Level, Output}; +use embassy_rp::peripherals::{PIN_26, UART1}; +use embassy_rp::uart::BufferedInterruptHandler; +use embassy_rp::{bind_interrupts, uart}; +use embassy_time::{with_timeout, Duration, Timer}; +use no_std_net::Ipv4Addr; +use static_cell::make_static; +use ublox_short_range::asynch::runner::Runner; +use ublox_short_range::asynch::ublox_stack::tcp::TcpSocket; +use ublox_short_range::asynch::ublox_stack::{StackResources, UbloxStack}; +use ublox_short_range::asynch::{new, State}; +use ublox_short_range::atat::{self, AtatIngress}; +use ublox_short_range::command::custom_digest::EdmDigester; +use ublox_short_range::command::edm::urc::EdmEvent; +use {defmt_rtt as _, panic_probe as _}; + +const RX_BUF_LEN: usize = 1024; +const URC_CAPACITY: usize = 3; + +type AtClient = ublox_short_range::atat::asynch::Client< + 'static, + common::TxWrap>, + RX_BUF_LEN, +>; + +#[embassy_executor::task] +async fn wifi_task( + runner: Runner<'static, AtClient, Output<'static, PIN_26>, 8, URC_CAPACITY>, +) -> ! { + runner.run().await +} + +#[embassy_executor::task] +async fn net_task(stack: &'static UbloxStack) -> ! { + stack.run().await +} + +#[embassy_executor::task] +async fn ingress_task( + mut ingress: atat::Ingress<'static, EdmDigester, EdmEvent, RX_BUF_LEN, URC_CAPACITY, 2>, + mut rx: uart::BufferedUartRx<'static, UART1>, +) -> ! { + ingress.read_from(&mut rx).await +} + +bind_interrupts!(struct Irqs { + UART1_IRQ => BufferedInterruptHandler; +}); + +#[embassy_executor::main] +async fn main(spawner: Spawner) { + info!("Hello World!"); + + let p = embassy_rp::init(Default::default()); + + let rst_pin = OutputOpenDrain::new(p.PIN_26.degrade(), Level::High); + + static TX_BUF: StaticCell<[u8; 32]> = StaticCell::new(); + static RX_BUF: StaticCell<[u8; 32]> = StaticCell::new(); + let wifi_uart = uart::BufferedUart::new_with_rtscts( + p.UART1, + Irqs, + p.PIN_24, + p.PIN_25, + p.PIN_23, + p.PIN_22, + TX_BUF.init([0; 32]), + RX_BUF.init([0; 32]), + uart::Config::default(), + ); + + static RESOURCES: StaticCell> = + StaticCell::new(); + + let mut runner = Runner::new( + wifi_uart.split(), + RESOURCES.init(Resources::new()), + WifiConfig { rst_pin }, + ); + + 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. guaranteed 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, + )); + + static CONTROL_RESOURCES: StaticCell = StaticCell::new(); + let mut control = runner.control(CONTROL_RESOURCES.init(ControlResources::new()), &stack); + + spawner.spawn(net_task(stack)).unwrap(); + spawner.spawn(ppp_task(runner, &stack)).unwrap(); + + stack.wait_config_up().await; + + Timer::after(Duration::from_secs(1)).await; + + loop { + match control.join_wpa2(WIFI_NETWORK, WIFI_PASSWORD).await { + Ok(_) => break, + Err(err) => { + defmt::panic!("join failed with status={}", err); + } + } + } + + // And now we can use it! + info!("Device initialized!"); + + let down = test_download(stack).await; + Timer::after(Duration::from_secs(SETTLE_TIME as _)).await; + let up = test_upload(stack).await; + Timer::after(Duration::from_secs(SETTLE_TIME as _)).await; + let updown = test_upload_download(stack).await; + Timer::after(Duration::from_secs(SETTLE_TIME as _)).await; + + // assert!(down > TEST_EXPECTED_DOWNLOAD_KBPS); + // assert!(up > TEST_EXPECTED_UPLOAD_KBPS); + // assert!(updown > TEST_EXPECTED_UPLOAD_DOWNLOAD_KBPS); + + info!("Test OK"); + cortex_m::asm::bkpt(); +} + +// Test-only wifi network, no internet access! +const WIFI_NETWORK: &str = "WiFimodem-7A76"; +const WIFI_PASSWORD: &str = "ndzwqzyhhd"; + +const TEST_DURATION: usize = 10; +const SETTLE_TIME: usize = 5; +const TEST_EXPECTED_DOWNLOAD_KBPS: usize = 300; +const TEST_EXPECTED_UPLOAD_KBPS: usize = 300; +const TEST_EXPECTED_UPLOAD_DOWNLOAD_KBPS: usize = 300; +const RX_BUFFER_SIZE: usize = 4096; +const TX_BUFFER_SIZE: usize = 4096; +const SERVER_ADDRESS: Ipv4Addr = Ipv4Addr::new(192, 168, 0, 8); +const DOWNLOAD_PORT: u16 = 4321; +const UPLOAD_PORT: u16 = 4322; +const UPLOAD_DOWNLOAD_PORT: u16 = 4323; + +async fn test_download(stack: &'static UbloxStack) -> usize { + info!("Testing download..."); + + let mut rx_buffer = [0; RX_BUFFER_SIZE]; + let mut tx_buffer = [0; TX_BUFFER_SIZE]; + let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); + // socket.set_timeout(Some(Duration::from_secs(10))); + + info!( + "connecting to {:?}:{}...", + debug2Format(&SERVER_ADDRESS), + DOWNLOAD_PORT + ); + if let Err(e) = socket.connect((SERVER_ADDRESS, DOWNLOAD_PORT)).await { + error!("connect error: {:?}", e); + return 0; + } + info!("connected, testing..."); + + let mut rx_buf = [0; 4096]; + let mut total: usize = 0; + with_timeout(Duration::from_secs(TEST_DURATION as _), async { + loop { + match socket.read(&mut rx_buf).await { + Ok(0) => { + error!("read EOF"); + return 0; + } + Ok(n) => total += n, + Err(e) => { + error!("read error: {:?}", e); + return 0; + } + } + } + }) + .await + .ok(); + + let kbps = (total + 512) / 1024 / TEST_DURATION; + info!("download: {} kB/s", kbps); + kbps +} + +async fn test_upload(stack: &'static UbloxStack) -> usize { + info!("Testing upload..."); + + let mut rx_buffer = [0; RX_BUFFER_SIZE]; + let mut tx_buffer = [0; TX_BUFFER_SIZE]; + let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); + // socket.set_timeout(Some(Duration::from_secs(10))); + + info!( + "connecting to {:?}:{}...", + debug2Format(&SERVER_ADDRESS), + UPLOAD_PORT + ); + if let Err(e) = socket.connect((SERVER_ADDRESS, UPLOAD_PORT)).await { + error!("connect error: {:?}", e); + return 0; + } + info!("connected, testing..."); + + let buf = [0; 4096]; + let mut total: usize = 0; + with_timeout(Duration::from_secs(TEST_DURATION as _), async { + loop { + match socket.write(&buf).await { + Ok(0) => { + error!("write zero?!??!?!"); + return 0; + } + Ok(n) => total += n, + Err(e) => { + error!("write error: {:?}", e); + return 0; + } + } + } + }) + .await + .ok(); + + let kbps = (total + 512) / 1024 / TEST_DURATION; + info!("upload: {} kB/s", kbps); + kbps +} + +async fn test_upload_download(stack: &'static UbloxStack) -> usize { + info!("Testing upload+download..."); + + let mut rx_buffer = [0; RX_BUFFER_SIZE]; + let mut tx_buffer = [0; TX_BUFFER_SIZE]; + let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); + // socket.set_timeout(Some(Duration::from_secs(10))); + + info!( + "connecting to {:?}:{}...", + debug2Format(&SERVER_ADDRESS), + UPLOAD_DOWNLOAD_PORT + ); + if let Err(e) = socket.connect((SERVER_ADDRESS, UPLOAD_DOWNLOAD_PORT)).await { + error!("connect error: {:?}", e); + return 0; + } + info!("connected, testing..."); + + let (mut reader, mut writer) = socket.split(); + + let tx_buf = [0; 4096]; + let mut rx_buf = [0; 4096]; + let mut total: usize = 0; + let tx_fut = async { + loop { + match writer.write(&tx_buf).await { + Ok(0) => { + error!("write zero?!??!?!"); + return 0; + } + Ok(_) => {} + Err(e) => { + error!("write error: {:?}", e); + return 0; + } + } + } + }; + + let rx_fut = async { + loop { + match reader.read(&mut rx_buf).await { + Ok(0) => { + error!("read EOF"); + return 0; + } + Ok(n) => total += n, + Err(e) => { + error!("read error: {:?}", e); + return 0; + } + } + } + }; + + if with_timeout( + Duration::from_secs(TEST_DURATION as _), + join(tx_fut, rx_fut), + ) + .await + .is_err() + { + error!("Test timed out"); + } + + let kbps = (total + 512) / 1024 / TEST_DURATION; + info!("upload+download: {} kB/s", kbps); + kbps +} diff --git a/examples/rpi-pico/src/bin/embassy-smoltcp-ppp.rs b/examples/rpi-pico/src/bin/embassy-smoltcp-ppp.rs new file mode 100644 index 0000000..e25d51e --- /dev/null +++ b/examples/rpi-pico/src/bin/embassy-smoltcp-ppp.rs @@ -0,0 +1,196 @@ +#![no_std] +#![no_main] +#![feature(type_alias_impl_trait)] +#![feature(impl_trait_in_assoc_type)] + +#[cfg(not(feature = "ppp"))] +compile_error!("You must enable the `ppp` feature flag to build this example"); + +use defmt::*; +use embassy_executor::Spawner; +use embassy_net::tcp::TcpSocket; +use embassy_net::{Ipv4Address, Stack, StackResources}; +use embassy_rp::gpio::{AnyPin, Level, Output, OutputOpenDrain, Pin}; +use embassy_rp::peripherals::UART1; +use embassy_rp::uart::{BufferedInterruptHandler, BufferedUart, BufferedUartRx, BufferedUartTx}; +use embassy_rp::{bind_interrupts, uart}; +use embassy_time::{Duration, Timer}; +use embedded_tls::TlsConfig; +use embedded_tls::TlsConnection; +use embedded_tls::TlsContext; +use embedded_tls::UnsecureProvider; +use embedded_tls::{Aes128GcmSha256, MaxFragmentLength}; +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_short_range::asynch::control::ControlResources; +use ublox_short_range::asynch::{Resources, Runner}; +use {defmt_rtt as _, panic_probe as _}; + +const CMD_BUF_SIZE: usize = 128; +const INGRESS_BUF_SIZE: usize = 512; +const URC_CAPACITY: usize = 2; + +pub struct WifiConfig { + pub rst_pin: OutputOpenDrain<'static>, +} + +impl<'a> ublox_short_range::WifiConfig<'a> for WifiConfig { + type ResetPin = OutputOpenDrain<'static>; + + 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> { + Some(&mut self.rst_pin) + } +} + +#[embassy_executor::task] +async fn net_task(stack: &'static Stack>) -> ! { + stack.run().await +} + +#[embassy_executor::task] +async fn ppp_task( + mut runner: Runner< + 'static, + BufferedUartRx<'static, UART1>, + BufferedUartTx<'static, UART1>, + WifiConfig, + INGRESS_BUF_SIZE, + URC_CAPACITY, + >, + stack: &'static embassy_net::Stack>, +) -> ! { + runner.run(stack).await +} + +bind_interrupts!(struct Irqs { + UART1_IRQ => BufferedInterruptHandler; +}); + +#[embassy_executor::main] +async fn main(spawner: Spawner) { + let p = embassy_rp::init(Default::default()); + + let rst_pin = OutputOpenDrain::new(p.PIN_26.degrade(), Level::High); + + static TX_BUF: StaticCell<[u8; 32]> = StaticCell::new(); + static RX_BUF: StaticCell<[u8; 32]> = StaticCell::new(); + let wifi_uart = uart::BufferedUart::new_with_rtscts( + p.UART1, + Irqs, + p.PIN_24, + p.PIN_25, + p.PIN_23, + p.PIN_22, + TX_BUF.init([0; 32]), + RX_BUF.init([0; 32]), + uart::Config::default(), + ); + + static RESOURCES: StaticCell> = + StaticCell::new(); + + let mut runner = Runner::new( + wifi_uart.split(), + RESOURCES.init(Resources::new()), + WifiConfig { rst_pin }, + ); + + 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. guaranteed 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, + )); + + static CONTROL_RESOURCES: StaticCell = StaticCell::new(); + let mut control = runner.control(CONTROL_RESOURCES.init(ControlResources::new()), &stack); + + spawner.spawn(net_task(stack)).unwrap(); + spawner.spawn(ppp_task(runner, &stack)).unwrap(); + + stack.wait_config_up().await; + + Timer::after(Duration::from_secs(1)).await; + + control.set_hostname("Ublox-wifi-test").await.ok(); + + control.join_wpa2("MyAccessPoint", "12345678").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_max_fragment_length(MaxFragmentLength::Bits11) + .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; 1024]; + let mut body_buf = [0; 8192]; + let response = Response::read(&mut tls, reqwless::request::Method::GET, &mut rx_buf) + .await + .unwrap(); + let len = response + .body() + .reader() + .read_to_end(&mut body_buf) + .await + .unwrap(); + + info!("{=[u8]:a}", &body_buf[..len]); +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 3cd5460..1dca89f 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,7 +1,7 @@ [toolchain] -channel = "nightly-2023-06-28" -components = [ "rust-src", "rustfmt", "llvm-tools-preview", "clippy" ] +channel = "1.79" +components = [ "rust-src", "rustfmt", "llvm-tools" ] targets = [ - "x86_64-unknown-linux-gnu", + "thumbv6m-none-eabi", "thumbv7em-none-eabihf" ] diff --git a/src/asynch/at_udp_socket.rs b/src/asynch/at_udp_socket.rs new file mode 100644 index 0000000..4428181 --- /dev/null +++ b/src/asynch/at_udp_socket.rs @@ -0,0 +1,70 @@ +use embassy_net::{udp::UdpSocket, Ipv4Address}; +use embedded_io_async::{Read, Write}; + +use crate::config::Transport; + +pub struct AtUdpSocket<'a>(pub(crate) UdpSocket<'a>); + +impl<'a> AtUdpSocket<'a> { + pub(crate) const PPP_AT_PORT: u16 = 23; +} + +impl<'a> embedded_io_async::ErrorType for &AtUdpSocket<'a> { + type Error = core::convert::Infallible; +} + +impl<'a> Read for &AtUdpSocket<'a> { + async fn read(&mut self, buf: &mut [u8]) -> Result { + let (len, _) = self.0.recv_from(buf).await.unwrap(); + Ok(len) + } +} + +impl<'a> Write for &AtUdpSocket<'a> { + async fn write(&mut self, buf: &[u8]) -> Result { + self.0 + .send_to( + buf, + (Ipv4Address::new(172, 30, 0, 251), AtUdpSocket::PPP_AT_PORT), + ) + .await + .unwrap(); + + Ok(buf.len()) + } +} + +impl<'a> Transport for AtUdpSocket<'a> { + fn set_baudrate(&mut self, _baudrate: u32) { + // Nothing to do here + } + + fn split_ref(&mut self) -> (impl Write, impl Read) { + (&*self, &*self) + } +} + +impl<'a> embedded_io_async::ErrorType for AtUdpSocket<'a> { + type Error = core::convert::Infallible; +} + +impl<'a> Read for AtUdpSocket<'a> { + async fn read(&mut self, buf: &mut [u8]) -> Result { + let (len, _) = self.0.recv_from(buf).await.unwrap(); + Ok(len) + } +} + +impl<'a> Write for AtUdpSocket<'a> { + async fn write(&mut self, buf: &[u8]) -> Result { + self.0 + .send_to( + buf, + (Ipv4Address::new(172, 30, 0, 251), AtUdpSocket::PPP_AT_PORT), + ) + .await + .unwrap(); + + Ok(buf.len()) + } +} diff --git a/src/asynch/control.rs b/src/asynch/control.rs new file mode 100644 index 0000000..67d648b --- /dev/null +++ b/src/asynch/control.rs @@ -0,0 +1,709 @@ +use core::cell::Cell; +use core::str::FromStr as _; + +use atat::AtatCmd; +use atat::{asynch::AtatClient, response_slot::ResponseSlotGuard, UrcChannel}; +use embassy_sync::{blocking_mutex::raw::NoopRawMutex, channel::Sender}; +use embassy_time::{with_timeout, Duration, Timer}; +use heapless::Vec; +use no_std_net::Ipv4Addr; + +use crate::command::general::responses::SoftwareVersionResponse; +use crate::command::general::types::FirmwareVersion; +use crate::command::general::SoftwareVersion; +use crate::command::gpio::responses::ReadGPIOResponse; +use crate::command::gpio::types::GPIOMode; +use crate::command::gpio::ConfigureGPIO; +use crate::command::network::responses::NetworkStatusResponse; +use crate::command::network::types::{NetworkStatus, NetworkStatusParameter}; +use crate::command::network::GetNetworkStatus; +use crate::command::ping::Ping; +use crate::command::system::responses::LocalAddressResponse; +use crate::command::system::types::InterfaceID; +use crate::command::system::GetLocalAddress; +use crate::command::wifi::{ExecWifiStationAction, GetWifiStatus, SetWifiStationConfig}; +use crate::command::OnOff; +use crate::command::{ + gpio::ReadGPIO, + wifi::{ + types::{ + AccessPointAction, Authentication, SecurityMode, SecurityModePSK, StatusId, + WifiStationAction, WifiStationConfig, WifiStatus, WifiStatusVal, + }, + WifiAPAction, + }, +}; +use crate::command::{ + gpio::{ + types::{GPIOId, GPIOValue}, + WriteGPIO, + }, + wifi::SetWifiAPConfig, +}; +use crate::command::{network::SetNetworkHostName, wifi::types::AccessPointConfig}; +use crate::command::{ + system::{RebootDCE, ResetToFactoryDefaults}, + wifi::types::AccessPointId, +}; +use crate::connection::{DnsServers, StaticConfigV4, WiFiState}; +use crate::error::Error; + +use super::runner::{MAX_CMD_LEN, URC_SUBSCRIBERS}; +use super::state::LinkState; +use super::{state, UbloxUrc}; + +enum WifiAuthentication<'a> { + None, + Wpa2Passphrase(&'a str), + Wpa2Psk(&'a [u8; 32]), +} + +const CONFIG_ID: u8 = 0; + +pub(crate) struct ProxyClient<'a, const INGRESS_BUF_SIZE: usize> { + pub(crate) req_sender: Sender<'a, NoopRawMutex, 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, 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 { + debug!( + "Sending command: {:?}", + atat::helpers::LossyStr(&buf[..len]) + ); + } else { + debug!("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(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, const URC_CAPACITY: usize> { + state_ch: state::Runner<'a>, + at_client: ProxyClient<'a, INGRESS_BUF_SIZE>, + urc_channel: &'a UrcChannel, +} + +impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> + Control<'a, INGRESS_BUF_SIZE, URC_CAPACITY> +{ + pub(crate) fn new( + state_ch: state::Runner<'a>, + urc_channel: &'a UrcChannel, + req_sender: Sender<'a, NoopRawMutex, Vec, 1>, + res_slot: &'a atat::ResponseSlot, + ) -> Self { + Self { + state_ch, + at_client: ProxyClient::new(req_sender, res_slot), + urc_channel: urc_channel, + } + } + + /// Set the hostname of the device + pub async fn set_hostname(&self, hostname: &str) -> Result<(), Error> { + self.state_ch.wait_for_initialized().await; + + (&self.at_client) + .send_retry(&SetNetworkHostName { + host_name: hostname, + }) + .await?; + Ok(()) + } + + /// Gets the firmware version of the device + pub async fn get_version(&self) -> Result { + self.state_ch.wait_for_initialized().await; + + let SoftwareVersionResponse { version } = + (&self.at_client).send_retry(&SoftwareVersion).await?; + Ok(version) + } + + /// Gets the MAC address of the device + pub async fn hardware_address(&mut self) -> Result<[u8; 6], Error> { + self.state_ch.wait_for_initialized().await; + + let LocalAddressResponse { mac } = (&self.at_client) + .send_retry(&GetLocalAddress { + interface_id: InterfaceID::WiFi, + }) + .await?; + + Ok(mac.to_be_bytes()[2..].try_into().unwrap()) + } + + async fn get_wifi_status(&self) -> Result { + match (&self.at_client) + .send_retry(&GetWifiStatus { + status_id: StatusId::Status, + }) + .await? + .status_id + { + WifiStatus::Status(s) => Ok(s), + _ => Err(Error::AT(atat::Error::InvalidResponse)), + } + } + + pub async fn wait_for_link_state(&self, link_state: LinkState) { + self.state_ch.wait_for_link_state(link_state).await + } + + pub async fn config_v4(&self) -> Result, Error> { + let NetworkStatusResponse { + status: NetworkStatus::IPv4Address(ipv4), + .. + } = (&self.at_client) + .send_retry(&GetNetworkStatus { + interface_id: 0, + status: NetworkStatusParameter::IPv4Address, + }) + .await? + else { + return Err(Error::Network); + }; + + let ipv4_addr = core::str::from_utf8(ipv4.as_slice()) + .ok() + .and_then(|s| Ipv4Addr::from_str(s).ok()) + .and_then(|ip| (!ip.is_unspecified()).then_some(ip)); + + let NetworkStatusResponse { + status: NetworkStatus::Gateway(gateway), + .. + } = (&self.at_client) + .send_retry(&GetNetworkStatus { + interface_id: 0, + status: NetworkStatusParameter::Gateway, + }) + .await? + else { + return Err(Error::Network); + }; + + let gateway_addr = core::str::from_utf8(gateway.as_slice()) + .ok() + .and_then(|s| Ipv4Addr::from_str(s).ok()) + .and_then(|ip| (!ip.is_unspecified()).then_some(ip)); + + let NetworkStatusResponse { + status: NetworkStatus::PrimaryDNS(primary), + .. + } = (&self.at_client) + .send_retry(&GetNetworkStatus { + interface_id: 0, + status: NetworkStatusParameter::PrimaryDNS, + }) + .await? + else { + return Err(Error::Network); + }; + + let primary = core::str::from_utf8(primary.as_slice()) + .ok() + .and_then(|s| Ipv4Addr::from_str(s).ok()) + .and_then(|ip| (!ip.is_unspecified()).then_some(ip)); + + let NetworkStatusResponse { + status: NetworkStatus::SecondaryDNS(secondary), + .. + } = (&self.at_client) + .send_retry(&GetNetworkStatus { + interface_id: 0, + status: NetworkStatusParameter::SecondaryDNS, + }) + .await? + else { + return Err(Error::Network); + }; + + let secondary = core::str::from_utf8(secondary.as_slice()) + .ok() + .and_then(|s| Ipv4Addr::from_str(s).ok()) + .and_then(|ip| (!ip.is_unspecified()).then_some(ip)); + + Ok(ipv4_addr.map(|address| StaticConfigV4 { + address, + gateway: gateway_addr, + dns_servers: DnsServers { primary, secondary }, + })) + } + + pub async fn get_connected_ssid(&self) -> Result, Error> { + match (&self.at_client) + .send_retry(&GetWifiStatus { + status_id: StatusId::SSID, + }) + .await? + .status_id + { + WifiStatus::SSID(s) => Ok(s), + _ => Err(Error::AT(atat::Error::InvalidResponse)), + } + } + + pub async fn factory_reset(&self) -> Result<(), Error> { + self.state_ch.wait_for_initialized().await; + + (&self.at_client) + .send_retry(&ResetToFactoryDefaults) + .await?; + (&self.at_client).send_retry(&RebootDCE).await?; + + Ok(()) + } + + async fn start_ap(&self, ssid: &str, _channel: u8) -> Result<(), Error> { + self.state_ch.wait_for_initialized().await; + + // Deactivate network id 0 + (&self.at_client) + .send_retry(&WifiAPAction { + ap_config_id: AccessPointId::Id0, + ap_action: AccessPointAction::Deactivate, + }) + .await?; + + (&self.at_client) + .send_retry(&WifiAPAction { + ap_config_id: AccessPointId::Id0, + ap_action: AccessPointAction::Reset, + }) + .await?; + + // // Disable DHCP Server (static IP address will be used) + // if options.ip.is_some() || options.subnet.is_some() || options.gateway.is_some() { + // (&self.at_client) + // .send_retry(&SetWifiAPConfig { + // ap_config_id: AccessPointId::Id0, + // ap_config_param: AccessPointConfig::IPv4Mode(IPv4Mode::Static), + // }) + // .await?; + // } + + // // Network IP address + // if let Some(ip) = options.ip { + // (&self.at_client) + // .send_retry(&SetWifiAPConfig { + // ap_config_id: AccessPointId::Id0, + // ap_config_param: AccessPointConfig::IPv4Address(ip), + // }) + // .await?; + // } + // // Network Subnet mask + // if let Some(subnet) = options.subnet { + // (&self.at_client) + // .send_retry(&SetWifiAPConfig { + // ap_config_id: AccessPointId::Id0, + // ap_config_param: AccessPointConfig::SubnetMask(subnet), + // }) + // .await?; + // } + // // Network Default gateway + // if let Some(gateway) = options.gateway { + // (&self.at_client) + // .send_retry(&SetWifiAPConfig { + // ap_config_id: AccessPointId::Id0, + // ap_config_param: AccessPointConfig::DefaultGateway(gateway), + // }) + // .await?; + // } + + // (&self.at_client) + // .send_retry(&SetWifiAPConfig { + // ap_config_id: AccessPointId::Id0, + // ap_config_param: AccessPointConfig::DHCPServer(true.into()), + // }) + // .await?; + + // Wifi part + // Set the Network SSID to connect to + (&self.at_client) + .send_retry(&SetWifiAPConfig { + ap_config_id: AccessPointId::Id0, + ap_config_param: AccessPointConfig::SSID( + heapless::String::try_from(ssid).map_err(|_| Error::Overflow)?, + ), + }) + .await?; + + // if let Some(pass) = options.password.clone() { + // // Use WPA2 as authentication type + // (&self.at_client) + // .send_retry(&SetWifiAPConfig { + // ap_config_id: AccessPointId::Id0, + // ap_config_param: AccessPointConfig::SecurityMode( + // SecurityMode::Wpa2AesCcmp, + // SecurityModePSK::PSK, + // ), + // }) + // .await?; + + // // Input passphrase + // (&self.at_client) + // .send_retry(&SetWifiAPConfig { + // ap_config_id: AccessPointId::Id0, + // ap_config_param: AccessPointConfig::PSKPassphrase(PasskeyR::Passphrase(pass)), + // }) + // .await?; + // } else { + (&self.at_client) + .send_retry(&SetWifiAPConfig { + ap_config_id: AccessPointId::Id0, + ap_config_param: AccessPointConfig::SecurityMode( + SecurityMode::Open, + SecurityModePSK::Open, + ), + }) + .await?; + // } + + // if let Some(channel) = configuration.channel { + // (&self.at_client) + // .send_retry(&SetWifiAPConfig { + // ap_config_id: AccessPointId::Id0, + // ap_config_param: AccessPointConfig::Channel(channel as u8), + // }) + // .await?; + // } + + (&self.at_client) + .send_retry(&WifiAPAction { + ap_config_id: AccessPointId::Id0, + ap_action: AccessPointAction::Activate, + }) + .await?; + + Ok(()) + } + + /// Start open access point. + pub async fn start_ap_open(&mut self, ssid: &str, channel: u8) -> Result<(), Error> { + self.start_ap(ssid, channel).await + } + + /// Start WPA2 protected access point. + pub async fn start_ap_wpa2( + &mut self, + ssid: &str, + _passphrase: &str, + channel: u8, + ) -> Result<(), Error> { + self.start_ap(ssid, channel).await + } + + /// Closes access point. + pub async fn close_ap(&self) -> Result<(), Error> { + todo!() + } + + async fn join_sta(&self, ssid: &str, auth: WifiAuthentication<'_>) -> Result<(), Error> { + self.state_ch.wait_for_initialized().await; + + if matches!(self.get_wifi_status().await?, WifiStatusVal::Connected) { + // Wifi already connected. Check if the SSID is the same + let current_ssid = self.get_connected_ssid().await?; + if current_ssid.as_str() == ssid { + self.state_ch.set_should_connect(true); + return Ok(()); + } else { + self.leave().await?; + }; + } + + (&self.at_client) + .send_retry(&ExecWifiStationAction { + config_id: CONFIG_ID, + action: WifiStationAction::Reset, + }) + .await?; + + (&self.at_client) + .send_retry(&SetWifiStationConfig { + config_id: CONFIG_ID, + config_param: WifiStationConfig::ActiveOnStartup(OnOff::Off), + }) + .await?; + + (&self.at_client) + .send_retry(&SetWifiStationConfig { + config_id: CONFIG_ID, + config_param: WifiStationConfig::SSID(ssid), + }) + .await?; + + match auth { + WifiAuthentication::None => { + (&self.at_client) + .send_retry(&SetWifiStationConfig { + config_id: CONFIG_ID, + config_param: WifiStationConfig::Authentication(Authentication::Open), + }) + .await?; + } + WifiAuthentication::Wpa2Passphrase(passphrase) => { + (&self.at_client) + .send_retry(&SetWifiStationConfig { + config_id: CONFIG_ID, + config_param: WifiStationConfig::Authentication(Authentication::WpaWpa2Psk), + }) + .await?; + + (&self.at_client) + .send_retry(&SetWifiStationConfig { + config_id: CONFIG_ID, + config_param: WifiStationConfig::WpaPskOrPassphrase(passphrase), + }) + .await?; + } + WifiAuthentication::Wpa2Psk(_psk) => { + unimplemented!() + // (&self.at_client) + // .send_retry(&SetWifiStationConfig { + // config_id: CONFIG_ID, + // config_param: WifiStationConfig::Authentication(Authentication::WpaWpa2Psk), + // }) + // .await?; + + // (&self.at_client) + // .send_retry(&SetWifiStationConfig { + // config_id: CONFIG_ID, + // config_param: WifiStationConfig::WpaPskOrPassphrase(todo!("hex values?!")), + // }) + // .await?; + } + } + + (&self.at_client) + .send_retry(&ExecWifiStationAction { + config_id: CONFIG_ID, + action: WifiStationAction::Activate, + }) + .await?; + + self.wait_for_join(ssid, Duration::from_secs(20)).await?; + self.state_ch.set_should_connect(true); + + Ok(()) + } + + /// Join an unprotected network with the provided ssid. + pub async fn join_open(&self, ssid: &str) -> Result<(), Error> { + self.join_sta(ssid, WifiAuthentication::None).await + } + + /// Join a protected network with the provided ssid and passphrase. + pub async fn join_wpa2(&self, ssid: &str, passphrase: &str) -> Result<(), Error> { + self.join_sta(ssid, WifiAuthentication::Wpa2Passphrase(passphrase)) + .await + } + + /// Join a protected network with the provided ssid and precomputed PSK. + pub async fn join_wpa2_psk(&mut self, ssid: &str, psk: &[u8; 32]) -> Result<(), Error> { + self.join_sta(ssid, WifiAuthentication::Wpa2Psk(psk)).await + } + + /// Leave the wifi, with which we are currently associated. + pub async fn leave(&self) -> Result<(), Error> { + self.state_ch.wait_for_initialized().await; + self.state_ch.set_should_connect(false); + + match self.get_wifi_status().await? { + WifiStatusVal::Disabled => {} + WifiStatusVal::Disconnected | WifiStatusVal::Connected => { + (&self.at_client) + .send_retry(&ExecWifiStationAction { + config_id: CONFIG_ID, + action: WifiStationAction::Deactivate, + }) + .await?; + } + } + + with_timeout( + Duration::from_secs(10), + self.state_ch.wait_connection_down(), + ) + .await + .map_err(|_| Error::Timeout)?; + + Ok(()) + } + + pub async fn wait_for_join(&self, ssid: &str, timeout: Duration) -> Result<(), Error> { + match with_timeout(timeout, self.state_ch.wait_for_link_state(LinkState::Up)).await { + Ok(_) => { + // Check that SSID matches + let current_ssid = self.get_connected_ssid().await?; + if ssid != current_ssid.as_str() { + return Err(Error::Network); + } + + Ok(()) + } + Err(_) if self.state_ch.wifi_state(None) == WiFiState::SecurityProblems => { + let _ = (&self.at_client) + .send_retry(&ExecWifiStationAction { + config_id: CONFIG_ID, + action: WifiStationAction::Deactivate, + }) + .await; + Err(Error::SecurityProblems) + } + Err(_) => Err(Error::Timeout), + } + } + + // /// Start a wifi scan + // /// + // /// Returns a `Stream` of networks found by the device + // /// + // /// # Note + // /// Device events are currently implemented using a bounded queue. + // /// To not miss any events, you should make sure to always await the stream. + // pub async fn scan(&mut self, scan_opts: ScanOptions) -> Scanner<'_> { + // todo!() + // } + + pub async fn send_at(&self, cmd: &Cmd) -> Result { + self.state_ch.wait_for_initialized().await; + Ok((&self.at_client).send_retry(cmd).await?) + } + + pub async fn gpio_configure(&self, id: GPIOId, mode: GPIOMode) -> Result<(), Error> { + self.send_at(&ConfigureGPIO { id, mode }).await?; + Ok(()) + } + + pub async fn gpio_set(&self, id: GPIOId, value: bool) -> Result<(), Error> { + let value = if value { + GPIOValue::High + } else { + GPIOValue::Low + }; + + self.send_at(&WriteGPIO { id, value }).await?; + Ok(()) + } + + pub async fn gpio_get(&self, id: GPIOId) -> Result { + let ReadGPIOResponse { value, .. } = self.send_at(&ReadGPIO { id }).await?; + Ok(value as u8 != 0) + } + + #[cfg(feature = "ppp")] + pub async fn ping( + &self, + hostname: &str, + ) -> Result { + let mut urc_sub = self.urc_channel.subscribe().map_err(|_| Error::Overflow)?; + + self.send_at(&Ping { + hostname, + retry_num: 1, + }) + .await?; + + let result_fut = async { + loop { + match urc_sub.next_message_pure().await { + crate::command::Urc::PingResponse(r) => return Ok(r), + crate::command::Urc::PingErrorResponse(e) => return Err(Error::Dns(e.error)), + _ => {} + } + } + }; + + with_timeout(Duration::from_secs(15), result_fut).await? + } + + // FIXME: This could probably be improved + // #[cfg(feature = "internal-network-stack")] + // pub async fn import_credentials( + // &mut self, + // data_type: SecurityDataType, + // name: &str, + // data: &[u8], + // md5_sum: Option<&str>, + // ) -> Result<(), atat::Error> { + // assert!(name.len() < 16); + + // info!("Importing {:?} bytes as {:?}", data.len(), name); + + // (&self.at_client) + // .send_retry(&PrepareSecurityDataImport { + // data_type, + // data_size: data.len(), + // internal_name: name, + // password: None, + // }) + // .await?; + + // let import_data = self + // .at_client + // .send_retry(&SendSecurityDataImport { + // data: atat::serde_bytes::Bytes::new(data), + // }) + // .await?; + + // if let Some(hash) = md5_sum { + // assert_eq!(import_data.md5_string.as_str(), hash); + // } + + // Ok(()) + // } +} diff --git a/src/asynch/mod.rs b/src/asynch/mod.rs new file mode 100644 index 0000000..afb3f0f --- /dev/null +++ b/src/asynch/mod.rs @@ -0,0 +1,20 @@ +#[cfg(feature = "ppp")] +mod at_udp_socket; +pub mod control; +pub mod network; +mod resources; +pub mod runner; +#[cfg(feature = "internal-network-stack")] +pub mod ublox_stack; + +pub(crate) mod state; + +pub use resources::Resources; +pub use runner::Runner; +pub use state::LinkState; + +#[cfg(feature = "edm")] +pub type UbloxUrc = crate::command::edm::urc::EdmEvent; + +#[cfg(not(feature = "edm"))] +pub type UbloxUrc = crate::command::Urc; diff --git a/src/asynch/network.rs b/src/asynch/network.rs new file mode 100644 index 0000000..79532b0 --- /dev/null +++ b/src/asynch/network.rs @@ -0,0 +1,321 @@ +use core::str::FromStr as _; + +use atat::{asynch::AtatClient, UrcChannel, UrcSubscription}; +use embassy_time::{with_timeout, Duration, Timer}; +use embedded_hal::digital::OutputPin as _; +use no_std_net::{Ipv4Addr, Ipv6Addr}; + +use crate::{ + command::{ + network::{ + responses::NetworkStatusResponse, + types::{InterfaceType, NetworkStatus, NetworkStatusParameter}, + urc::{NetworkDown, NetworkUp}, + GetNetworkStatus, + }, + system::{RebootDCE, StoreCurrentConfig}, + wifi::{ + types::DisconnectReason, + urc::{WifiLinkConnected, WifiLinkDisconnected}, + }, + Urc, + }, + connection::WiFiState, + error::Error, + network::WifiNetwork, + WifiConfig, +}; + +use super::{runner::URC_SUBSCRIBERS, state, UbloxUrc}; + +pub(crate) struct NetDevice<'a, 'b, C, A, const URC_CAPACITY: usize> { + ch: &'b state::Runner<'a>, + config: &'b mut C, + at_client: A, + urc_subscription: UrcSubscription<'a, UbloxUrc, URC_CAPACITY, { URC_SUBSCRIBERS }>, +} + +impl<'a, 'b, C, A, const URC_CAPACITY: usize> NetDevice<'a, 'b, C, A, URC_CAPACITY> +where + C: WifiConfig<'a>, + A: AtatClient, +{ + pub fn new( + ch: &'b state::Runner<'a>, + config: &'b mut C, + at_client: A, + urc_channel: &'a UrcChannel, + ) -> Self { + Self { + ch, + config, + at_client, + urc_subscription: urc_channel.subscribe().unwrap(), + } + } + + pub async fn run(&mut self) -> Result<(), Error> { + loop { + match embassy_futures::select::select( + self.urc_subscription.next_message_pure(), + self.ch.wait_for_wifi_state_change(), + ) + .await + { + embassy_futures::select::Either::First(event) => { + #[cfg(feature = "edm")] + let Some(event) = event.extract_urc() else { + continue; + }; + + self.handle_urc(event).await?; + } + _ => {} + } + + if self.ch.wifi_state(None) == WiFiState::Inactive && self.ch.connection_down(None) { + return Ok(()); + } + } + } + + async fn handle_urc(&mut self, event: Urc) -> Result<(), Error> { + match event { + Urc::StartUp => { + error!("AT startup event?! Device restarted unintentionally!"); + } + Urc::WifiLinkConnected(WifiLinkConnected { + connection_id: _, + bssid, + channel, + }) => self.ch.update_connection_with(|con| { + con.wifi_state = WiFiState::Connected; + con.network + .replace(WifiNetwork::new_station(bssid, channel)); + }), + Urc::WifiLinkDisconnected(WifiLinkDisconnected { reason, .. }) => { + self.ch.update_connection_with(|con| { + con.wifi_state = match reason { + DisconnectReason::NetworkDisabled => { + con.network.take(); + warn!("Wifi network disabled!"); + WiFiState::Inactive + } + DisconnectReason::SecurityProblems => { + error!("Wifi Security Problems"); + WiFiState::SecurityProblems + } + _ => WiFiState::NotConnected, + } + }) + } + Urc::WifiAPUp(_) => warn!("Not yet implemented [WifiAPUp]"), + Urc::WifiAPDown(_) => warn!("Not yet implemented [WifiAPDown]"), + Urc::WifiAPStationConnected(_) => warn!("Not yet implemented [WifiAPStationConnected]"), + Urc::WifiAPStationDisconnected(_) => { + warn!("Not yet implemented [WifiAPStationDisconnected]") + } + Urc::EthernetLinkUp(_) => warn!("Not yet implemented [EthernetLinkUp]"), + Urc::EthernetLinkDown(_) => warn!("Not yet implemented [EthernetLinkDown]"), + Urc::NetworkUp(NetworkUp { interface_id }) => { + self.network_status_callback(interface_id).await?; + } + Urc::NetworkDown(NetworkDown { interface_id }) => { + self.network_status_callback(interface_id).await?; + } + Urc::NetworkError(_) => warn!("Not yet implemented [NetworkError]"), + _ => {} + } + + Ok(()) + } + + async fn network_status_callback(&mut self, interface_id: u8) -> Result<(), Error> { + // Normally a check for this interface type being + // `InterfaceType::WifiStation`` should be made but there is a bug in + // uConnect which gives the type `InterfaceType::Unknown` when the + // credentials have been restored from persistent memory. This although + // the wifi station has been started. So we assume that this type is + // also ok. + let NetworkStatusResponse { + status: + NetworkStatus::InterfaceType(InterfaceType::WifiStation | InterfaceType::Unknown), + .. + } = self + .at_client + .send_retry(&GetNetworkStatus { + interface_id, + status: NetworkStatusParameter::InterfaceType, + }) + .await? + else { + return Err(Error::Network); + }; + + let NetworkStatusResponse { + status: NetworkStatus::IPv4Address(ipv4), + .. + } = self + .at_client + .send_retry(&GetNetworkStatus { + interface_id, + status: NetworkStatusParameter::IPv4Address, + }) + .await? + else { + return Err(Error::Network); + }; + + let ipv4_up = core::str::from_utf8(ipv4.as_slice()) + .ok() + .and_then(|s| Ipv4Addr::from_str(s).ok()) + .map(|ip| !ip.is_unspecified()) + .unwrap_or_default(); + + #[cfg(feature = "ipv6")] + let ipv6_up = { + let NetworkStatusResponse { + status: NetworkStatus::IPv6Address1(ipv6), + .. + } = self + .at_client + .send_retry(&GetNetworkStatus { + interface_id, + status: NetworkStatusParameter::IPv6Address1, + }) + .await? + else { + return Err(Error::Network); + }; + + core::str::from_utf8(ipv6.as_slice()) + .ok() + .and_then(|s| Ipv6Addr::from_str(s).ok()) + .map(|ip| !ip.is_unspecified()) + .unwrap_or_default() + }; + + let NetworkStatusResponse { + status: NetworkStatus::IPv6LinkLocalAddress(ipv6_link_local), + .. + } = self + .at_client + .send_retry(&GetNetworkStatus { + interface_id, + status: NetworkStatusParameter::IPv6LinkLocalAddress, + }) + .await? + else { + return Err(Error::Network); + }; + + let ipv6_link_local_up = core::str::from_utf8(ipv6_link_local.as_slice()) + .ok() + .and_then(|s| Ipv6Addr::from_str(s).ok()) + .map(|ip| !ip.is_unspecified()) + .unwrap_or_default(); + + // Use `ipv4_addr` & `ipv6_addr` to determine link state + self.ch.update_connection_with(|con| { + con.ipv6_link_local_up = ipv6_link_local_up; + con.ipv4_up = ipv4_up; + + #[cfg(feature = "ipv6")] + { + con.ipv6_up = ipv6_up + } + }); + + Ok(()) + } + + async fn wait_startup(&mut self, timeout: Duration) -> Result<(), Error> { + let fut = async { + loop { + let event = self.urc_subscription.next_message_pure().await; + + #[cfg(feature = "edm")] + let Some(event) = event.extract_urc() else { + continue; + }; + + if let Urc::StartUp = event { + return; + } + } + }; + + with_timeout(timeout, fut).await.map_err(|_| Error::Timeout) + } + + pub async fn reset(&mut self) -> Result<(), Error> { + if let Some(reset_pin) = self.config.reset_pin() { + warn!("Reset pin found! Hard resetting Ublox Short Range"); + reset_pin.set_low().ok(); + Timer::after(Duration::from_millis(100)).await; + reset_pin.set_high().ok(); + } else { + warn!("No reset pin found! Soft resetting Ublox Short Range"); + self.at_client.send_retry(&RebootDCE).await?; + } + + self.ch.mark_uninitialized(); + + self.wait_startup(Duration::from_secs(5)).await?; + + #[cfg(feature = "edm")] + self.enter_edm(Duration::from_secs(4)).await?; + + Ok(()) + } + + #[allow(dead_code)] + pub async fn restart(&mut self, store: bool) -> Result<(), Error> { + warn!("Soft resetting Ublox Short Range"); + if store { + self.at_client.send_retry(&StoreCurrentConfig).await?; + } + + self.at_client.send_retry(&RebootDCE).await?; + + self.ch.mark_uninitialized(); + + self.wait_startup(Duration::from_secs(5)).await?; + + info!("Module started again"); + #[cfg(feature = "edm")] + self.enter_edm(Duration::from_secs(4)).await?; + + Ok(()) + } + + #[cfg(feature = "edm")] + pub async fn enter_edm(&mut self, timeout: Duration) -> Result<(), Error> { + info!("Entering EDM mode"); + + // Switch to EDM on Init. If in EDM, fail and check with autosense + let fut = async { + loop { + // Ignore AT results until we are successful in EDM mode + if let Ok(_) = self + .at_client + .send_retry(&crate::command::edm::SwitchToEdmCommand) + .await + { + // After executing the data mode command or the extended data + // mode command, a delay of 50 ms is required before start of + // data transmission. + Timer::after(Duration::from_millis(50)).await; + break; + } + Timer::after(Duration::from_millis(10)).await; + } + }; + + with_timeout(timeout, fut) + .await + .map_err(|_| Error::Timeout)?; + + Ok(()) + } +} diff --git a/src/asynch/resources.rs b/src/asynch/resources.rs new file mode 100644 index 0000000..20db742 --- /dev/null +++ b/src/asynch/resources.rs @@ -0,0 +1,39 @@ +use atat::{ResponseSlot, UrcChannel}; +use embassy_sync::{blocking_mutex::raw::NoopRawMutex, channel::Channel}; + +use super::{ + runner::{MAX_CMD_LEN, URC_SUBSCRIBERS}, + state, UbloxUrc, +}; + +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], +} + +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], + } + } +} diff --git a/src/asynch/runner.rs b/src/asynch/runner.rs new file mode 100644 index 0000000..5153b77 --- /dev/null +++ b/src/asynch/runner.rs @@ -0,0 +1,486 @@ +use super::{control::Control, network::NetDevice, state, Resources, UbloxUrc}; +use crate::{ + asynch::control::ProxyClient, + command::{ + data_mode::{self, ChangeMode}, + general::SoftwareVersion, + system::{ + types::{BaudRate, ChangeAfterConfirm, EchoOn, FlowControl, Parity, StopBits}, + SetEcho, SetRS232Settings, + }, + wifi::{ + types::{PowerSaveMode, WifiConfig as WifiConfigParam}, + SetWifiConfig, + }, + OnOff, AT, + }, + config::Transport, + error::Error, + WifiConfig, DEFAULT_BAUD_RATE, +}; +use atat::{ + asynch::{AtatClient as _, SimpleClient}, + AtatIngress as _, UrcChannel, +}; +use embassy_futures::select::Either; +use embassy_sync::{blocking_mutex::raw::NoopRawMutex, channel::Channel}; +use embassy_time::{Duration, Timer}; +use embedded_io_async::{BufRead, Write}; + +#[cfg(feature = "ppp")] +pub(crate) const URC_SUBSCRIBERS: usize = 2; +#[cfg(feature = "ppp")] +type Digester = atat::AtDigester; + +#[cfg(feature = "internal-network-stack")] +pub(crate) const URC_SUBSCRIBERS: usize = 3; +#[cfg(feature = "internal-network-stack")] +type Digester = crate::command::custom_digest::EdmDigester; + +pub(crate) const MAX_CMD_LEN: usize = 256; + +async fn at_bridge<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize>( + transport: &mut impl Transport, + req_slot: &Channel, 1>, + ingress: &mut atat::Ingress< + 'a, + Digester, + UbloxUrc, + INGRESS_BUF_SIZE, + URC_CAPACITY, + { URC_SUBSCRIBERS }, + >, +) -> ! { + ingress.clear(); + + let (mut tx, rx) = transport.split_ref(); + + 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: Transport, C, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> { + transport: T, + + ch: state::Runner<'a>, + config: C, + + pub urc_channel: &'a UrcChannel, + + pub ingress: + atat::Ingress<'a, Digester, UbloxUrc, INGRESS_BUF_SIZE, URC_CAPACITY, { URC_SUBSCRIBERS }>, + pub res_slot: &'a atat::ResponseSlot, + pub req_slot: &'a Channel, 1>, + + #[cfg(feature = "ppp")] + 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 + BufRead, + C: WifiConfig<'a> + 'a, +{ + pub fn new( + transport: T, + resources: &'a mut Resources, + config: C, + ) -> (Self, Control<'a, INGRESS_BUF_SIZE, URC_CAPACITY>) { + let ch_runner = state::Runner::new(&mut resources.ch); + + let ingress = atat::Ingress::new( + Digester::new(), + &mut resources.ingress_buf, + &resources.res_slot, + &resources.urc_channel, + ); + + let control = Control::new( + ch_runner.clone(), + &resources.urc_channel, + 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, + + #[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, + ) -> super::ublox_stack::Device<'a, INGRESS_BUF_SIZE, URC_CAPACITY> { + super::ublox_stack::Device { + state_ch: self.ch.clone(), + at_client: core::cell::RefCell::new(ProxyClient::new( + self.req_slot.sender(), + &self.res_slot, + )), + urc_channel: &self.urc_channel, + } + } + + /// 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 wifi module using baud rate: {}", baudrate as u32); + self.transport.set_baudrate(baudrate as u32); + + let baud_fut = async { + let at_client = ProxyClient::new(self.req_slot.sender(), self.res_slot); + + // Hard reset module + NetDevice::new(&self.ch, &mut self.config, &at_client, self.urc_channel) + .reset() + .await?; + + (&at_client).send_retry(&AT).await?; + + // Lets take a shortcut if we are probing for the desired baudrate + if baudrate == C::BAUD_RATE { + info!("Successfully shortcut the baud probing!"); + return Ok(None); + } + + let flow_control = if C::FLOW_CONTROL { + FlowControl::On + } else { + FlowControl::Off + }; + + (&at_client) + .send_retry(&SetRS232Settings { + baud_rate: C::BAUD_RATE, + flow_control, + data_bits: 8, + stop_bits: StopBits::One, + parity: Parity::None, + change_after_confirm: ChangeAfterConfirm::ChangeAfterOK, + }) + .await?; + + Ok::<_, Error>(Some(C::BAUD_RATE)) + }; + + match embassy_futures::select::select( + baud_fut, + at_bridge(&mut self.transport, self.req_slot, &mut self.ingress), + ) + .await + { + Either::First(Ok(Some(baud))) => { + self.transport.set_baudrate(baud as u32); + Timer::after_millis(40).await; + Ok(()) + } + Either::First(r) => r.map(drop), + Either::Second(_) => unreachable!(), + } + } + + async fn init(&mut self) -> Result<(), Error> { + // Initialize a new ublox device to a known state + debug!("Initializing WiFi module"); + + // 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::B14400, + BaudRate::B19200, + BaudRate::B28800, + BaudRate::B38400, + BaudRate::B57600, + BaudRate::B76800, + BaudRate::B115200, + BaudRate::B230400, + BaudRate::B250000, + BaudRate::B460800, + BaudRate::B921600, + BaudRate::B3000000, + BaudRate::B5250000, + ] { + if self.probe_baud(baudrate).await.is_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. + let _ = embassy_futures::select::select( + NetDevice::new( + &self.ch, + &mut self.config, + &ProxyClient::new(self.req_slot.sender(), self.res_slot), + self.urc_channel, + ) + .restart(true), + at_bridge(&mut self.transport, self.req_slot, &mut self.ingress), + ) + .await; + } + found_baudrate = true; + break; + } + } + + if !found_baudrate { + return Err(Error::BaudDetection); + } + + let at_client = ProxyClient::new(self.req_slot.sender(), self.res_slot); + + let setup_fut = async { + (&at_client).send_retry(&SoftwareVersion).await?; + + (&at_client) + .send_retry(&SetEcho { on: EchoOn::Off }) + .await?; + (&at_client) + .send_retry(&SetWifiConfig { + config_param: WifiConfigParam::DropNetworkOnLinkLoss(OnOff::On), + }) + .await?; + + // Disable all power savings for now + (&at_client) + .send_retry(&SetWifiConfig { + config_param: WifiConfigParam::PowerSaveMode(PowerSaveMode::ActiveMode), + }) + .await?; + + #[cfg(feature = "internal-network-stack")] + if let Some(size) = C::TLS_IN_BUFFER_SIZE { + (&at_client) + .send_retry(&crate::command::data_mode::SetPeerConfiguration { + parameter: crate::command::data_mode::types::PeerConfigParameter::TlsInBuffer( + size, + ), + }) + .await?; + } + + #[cfg(feature = "internal-network-stack")] + if let Some(size) = C::TLS_OUT_BUFFER_SIZE { + (&at_client) + .send_retry(&crate::command::data_mode::SetPeerConfiguration { + parameter: + crate::command::data_mode::types::PeerConfigParameter::TlsOutBuffer( + size, + ), + }) + .await?; + } + + Ok::<(), Error>(()) + }; + + match embassy_futures::select::select( + setup_fut, + at_bridge(&mut self.transport, self.req_slot, &mut self.ingress), + ) + .await + { + Either::First(r) => r?, + Either::Second(_) => unreachable!(), + } + + self.ch.mark_initialized(); + + Ok(()) + } + + #[cfg(feature = "internal-network-stack")] + pub async fn run(&mut self) -> ! { + loop { + if self.init().await.is_err() { + continue; + } + + embassy_futures::select::select( + NetDevice::new( + &self.ch, + &mut self.config, + &ProxyClient::new(self.req_slot.sender(), &self.res_slot), + self.urc_channel, + ) + .run(), + at_bridge(&mut self.transport, &self.req_slot, &mut self.ingress), + ) + .await; + } + } + + #[cfg(feature = "ppp")] + pub async fn run( + &mut self, + stack: &embassy_net::Stack, + ) -> ! { + loop { + if self.init().await.is_err() { + continue; + } + + debug!("Done initializing WiFi module"); + + let network_fut = async { + // Allow control to send/receive AT commands directly on the + // UART, until we are ready to establish connection using PPP + let _ = embassy_futures::select::select( + at_bridge(&mut self.transport, self.req_slot, &mut self.ingress), + self.ch.wait_connected(), + ) + .await; + + #[cfg(feature = "ppp")] + let ppp_fut = async { + self.ch.wait_for_link_state(state::LinkState::Up).await; + + { + let mut buf = [0u8; 8]; + let mut at_client = SimpleClient::new( + &mut self.transport, + atat::AtDigester::::new(), + &mut buf, + C::AT_CONFIG, + ); + + // Send AT command `ATO3` to enter PPP mode + let res = at_client + .send_retry(&ChangeMode { + mode: data_mode::types::Mode::PPPMode, + }) + .await; + + if let Err(e) = res { + warn!("ppp dial failed {:?}", e); + return; + } + + // Drain the UART + let _ = embassy_time::with_timeout(Duration::from_millis(500), async { + loop { + self.transport.read(&mut buf).await.ok(); + } + }) + .await; + } + + info!("RUNNING PPP"); + let _ = self + .ppp_runner + .as_mut() + .unwrap() + .run(&mut self.transport, C::PPP_CONFIG, |ipv4| { + debug!("Running on_ipv4_up for wifi!"); + 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); + }) + .await; + + info!("ppp failed"); + }; + + let at_fut = async { + use crate::asynch::at_udp_socket::AtUdpSocket; + use embassy_net::udp::{PacketMetadata, UdpSocket}; + + let mut rx_meta = [PacketMetadata::EMPTY; 1]; + let mut tx_meta = [PacketMetadata::EMPTY; 1]; + let mut socket_rx_buf = [0u8; 64]; + let mut socket_tx_buf = [0u8; 64]; + let mut socket = UdpSocket::new( + stack, + &mut rx_meta, + &mut socket_rx_buf, + &mut tx_meta, + &mut socket_tx_buf, + ); + + socket.bind(AtUdpSocket::PPP_AT_PORT).unwrap(); + let mut at_socket = AtUdpSocket(socket); + + at_bridge(&mut at_socket, self.req_slot, &mut self.ingress).await; + }; + + embassy_futures::select::select(ppp_fut, at_fut).await; + }; + + let device_fut = async { + let _ = NetDevice::new( + &self.ch, + &mut self.config, + &ProxyClient::new(self.req_slot.sender(), self.res_slot), + self.urc_channel, + ) + .run() + .await; + + warn!("Breaking to reboot device"); + }; + + embassy_futures::select::select(device_fut, network_fut).await; + } + } +} diff --git a/src/asynch/state.rs b/src/asynch/state.rs new file mode 100644 index 0000000..4dc3784 --- /dev/null +++ b/src/asynch/state.rs @@ -0,0 +1,216 @@ +#![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; + +use crate::connection::{WiFiState, WifiConnection}; + +/// The link state of a network device. +#[derive(PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum LinkState { + /// Device is not yet initialized. + Uninitialized, + /// The link is down. + Down, + /// The link is up. + Up, +} + +pub(crate) struct State { + shared: Mutex>, +} + +impl State { + pub(crate) const fn new() -> Self { + Self { + shared: Mutex::new(RefCell::new(Shared { + should_connect: false, + link_state: LinkState::Uninitialized, + wifi_connection: WifiConnection::new(), + state_waker: WakerRegistration::new(), + connection_waker: WakerRegistration::new(), + })), + } + } +} + +/// State of the LinkState +pub(crate) struct Shared { + link_state: LinkState, + should_connect: bool, + wifi_connection: WifiConnection, + state_waker: WakerRegistration, + connection_waker: WakerRegistration, +} + +#[derive(Clone)] +pub(crate) struct Runner<'d> { + shared: &'d Mutex>, +} + +impl<'d> Runner<'d> { + pub(crate) fn new(state: &'d mut State) -> Self { + Self { + shared: &state.shared, + } + } + + pub(crate) fn mark_initialized(&self) { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + s.link_state = LinkState::Down; + s.state_waker.wake(); + }) + } + + pub(crate) fn mark_uninitialized(&self) { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + s.link_state = LinkState::Uninitialized; + s.state_waker.wake(); + }) + } + + pub(crate) fn set_should_connect(&self, should_connect: bool) { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + s.connection_waker.wake(); + s.should_connect = should_connect; + }) + } + + pub(crate) async fn wait_for_initialized(&self) { + if self.link_state(None) != LinkState::Uninitialized { + return; + } + + poll_fn(|cx| { + if self.link_state(Some(cx)) != LinkState::Uninitialized { + return Poll::Ready(()); + } + Poll::Pending + }) + .await + } + + pub(crate) 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(crate) async fn wait_for_link_state(&self, ls: LinkState) { + if self.link_state(None) == ls { + return; + } + + poll_fn(|cx| { + if self.link_state(Some(cx)) == ls { + return Poll::Ready(()); + } + Poll::Pending + }) + .await + } + + pub(crate) fn update_connection_with(&self, f: impl FnOnce(&mut WifiConnection)) { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + f(&mut s.wifi_connection); + info!( + "Connection status changed! Connected: {:?}", + s.wifi_connection.is_connected() + ); + + s.link_state = if s.wifi_connection.is_connected() { + LinkState::Up + } else { + LinkState::Down + }; + + s.state_waker.wake(); + s.connection_waker.wake(); + }) + } + + pub(crate) fn connection_down(&self, cx: Option<&mut Context>) -> bool { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + if let Some(cx) = cx { + s.connection_waker.register(cx.waker()); + } + !s.wifi_connection.ipv4_up && !s.wifi_connection.ipv6_link_local_up + }) + } + + pub(crate) async fn wait_connection_down(&self) { + if self.connection_down(None) { + return; + } + + poll_fn(|cx| { + if self.connection_down(Some(cx)) { + return Poll::Ready(()); + } + Poll::Pending + }) + .await + } + + pub(crate) fn is_connected(&self, cx: Option<&mut Context>) -> bool { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + if let Some(cx) = cx { + s.connection_waker.register(cx.waker()); + } + s.wifi_connection.is_connected() && s.should_connect + }) + } + + pub(crate) async fn wait_connected(&self) { + if self.is_connected(None) { + return; + } + + poll_fn(|cx| { + if self.is_connected(Some(cx)) { + return Poll::Ready(()); + } + Poll::Pending + }) + .await + } + + pub(crate) fn wifi_state(&self, cx: Option<&mut Context>) -> WiFiState { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + if let Some(cx) = cx { + s.connection_waker.register(cx.waker()); + } + s.wifi_connection.wifi_state + }) + } + + pub(crate) async fn wait_for_wifi_state_change(&self) -> WiFiState { + let old_state = self.wifi_state(None); + + poll_fn(|cx| { + let new_state = self.wifi_state(Some(cx)); + if old_state != new_state { + return Poll::Ready(new_state); + } + Poll::Pending + }) + .await + } +} diff --git a/src/asynch/ublox_stack/device.rs b/src/asynch/ublox_stack/device.rs new file mode 100644 index 0000000..bf728c5 --- /dev/null +++ b/src/asynch/ublox_stack/device.rs @@ -0,0 +1,11 @@ +use core::cell::RefCell; + +use atat::UrcChannel; + +use crate::asynch::{control::ProxyClient, runner::URC_SUBSCRIBERS, state, UbloxUrc}; + +pub struct Device<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> { + pub(crate) state_ch: state::Runner<'a>, + pub(crate) at_client: RefCell>, + pub(crate) urc_channel: &'a UrcChannel, +} diff --git a/src/asynch/ublox_stack/dns.rs b/src/asynch/ublox_stack/dns.rs new file mode 100644 index 0000000..00550a2 --- /dev/null +++ b/src/asynch/ublox_stack/dns.rs @@ -0,0 +1,183 @@ +use core::{cell::RefCell, future::poll_fn, task::Poll}; + +use embassy_sync::waitqueue::WakerRegistration; +use embedded_nal_async::AddrType; +use no_std_net::IpAddr; + +use crate::command::ping::types::PingError; + +use super::{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, +} + +/// From u-connectXpress AT commands manual: +/// depends on the . For internet domain names, the maximum +/// length is 64 characters. +/// Domain name length is 128 for NINA-W13 and NINA-W15 software version 4.0 +/// .0 or later. +#[cfg(not(feature = "nina-w1xx"))] +pub const MAX_DOMAIN_NAME_LENGTH: usize = 64; + +#[cfg(feature = "nina-w1xx")] +pub const MAX_DOMAIN_NAME_LENGTH: usize = 128; + +pub struct DnsTableEntry { + pub domain_name: heapless::String, + pub state: DnsState, + pub waker: WakerRegistration, +} + +#[derive(PartialEq, Clone)] +pub enum DnsState { + New, + Pending, + Resolved(IpAddr), + Error(PingError), +} + +impl DnsTableEntry { + pub const fn new(domain_name: heapless::String) -> Self { + Self { + domain_name, + state: DnsState::New, + waker: WakerRegistration::new(), + } + } +} + +pub struct DnsTable { + pub table: heapless::Deque, +} + +impl DnsTable { + pub const fn new() -> Self { + Self { + table: heapless::Deque::new(), + } + } + pub fn upsert(&mut self, new_entry: DnsTableEntry) { + if let Some(entry) = self + .table + .iter_mut() + .find(|e| e.domain_name == new_entry.domain_name) + { + entry.state = new_entry.state; + return; + } + + if self.table.is_full() { + self.table.pop_front(); + } + unsafe { + self.table.push_back_unchecked(new_entry); + } + } + + pub fn get(&self, domain_name: &str) -> Option<&DnsTableEntry> { + self.table + .iter() + .find(|e| e.domain_name.as_str() == domain_name) + } + + pub fn get_mut(&mut self, domain_name: &str) -> Option<&mut DnsTableEntry> { + self.table + .iter_mut() + .find(|e| e.domain_name.as_str() == domain_name) + } + + pub fn reverse_lookup(&self, ip: IpAddr) -> Option<&str> { + self.table + .iter() + .find(|e| e.state == DnsState::Resolved(ip)) + .map(|e| e.domain_name.as_str()) + } +} + +/// 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 name_string = heapless::String::try_from(name).map_err(|_| Error::NameTooLong)?; + + { + let mut s = self.stack.borrow_mut(); + s.dns_table.upsert(DnsTableEntry::new(name_string.clone())); + s.waker.wake(); + } + + poll_fn(|cx| { + let mut s = self.stack.borrow_mut(); + let query = s.dns_table.get_mut(&name_string).unwrap(); + match query.state { + DnsState::Resolved(ip) => Poll::Ready(Ok(ip)), + DnsState::Error(_e) => Poll::Ready(Err(Error::Failed)), + _ => { + query.waker.register(cx.waker()); + Poll::Pending + } + } + }) + .await + } +} + +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: IpAddr, + _result: &mut [u8], + ) -> Result { + unimplemented!() + } +} diff --git a/src/asynch/ublox_stack/mod.rs b/src/asynch/ublox_stack/mod.rs new file mode 100644 index 0000000..07247b6 --- /dev/null +++ b/src/asynch/ublox_stack/mod.rs @@ -0,0 +1,529 @@ +#[cfg(feature = "socket-tcp")] +pub mod tcp; +#[cfg(feature = "socket-tcp")] +pub mod tls; +#[cfg(feature = "socket-udp")] +pub mod udp; + +mod device; +pub mod dns; +mod peer_builder; + +pub use device::Device; + +use core::cell::RefCell; +use core::future::poll_fn; +use core::ops::{DerefMut, Rem}; +use core::task::Poll; + +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}; +use crate::command::edm::urc::EdmEvent; +use crate::command::edm::{EdmAtCmdWrapper, EdmDataCommand}; +use crate::command::ping::types::PingError; +use crate::command::ping::urc::{PingErrorResponse, PingResponse}; +use crate::command::ping::Ping; +use crate::command::Urc; +use peer_builder::{PeerUrlBuilder, SecurityCredentials}; + +use self::dns::{DnsSocket, DnsState, DnsTable}; + +use super::control::ProxyClient; + +use embassy_futures::select; +use embassy_sync::waitqueue::WakerRegistration; +use embassy_time::{Duration, Ticker}; +use embedded_nal_async::SocketAddr; +use no_std_net::IpAddr; +use portable_atomic::{AtomicBool, AtomicU8, Ordering}; +use ublox_sockets::{ + AnySocket, ChannelId, PeerHandle, Socket, SocketHandle, SocketSet, SocketStorage, +}; + +#[cfg(feature = "socket-tcp")] +use ublox_sockets::TcpState; + +#[cfg(feature = "socket-udp")] +use ublox_sockets::UdpState; + +const MAX_EGRESS_SIZE: usize = 2048; + +pub struct StackResources { + sockets: [SocketStorage<'static>; SOCK], +} + +impl Default for StackResources { + fn default() -> Self { + Self::new() + } +} + +impl StackResources { + pub fn new() -> Self { + Self { + sockets: [SocketStorage::EMPTY; SOCK], + } + } +} + +pub struct UbloxStack { + socket: RefCell, + device: Device<'static, INGRESS_BUF_SIZE, URC_CAPACITY>, + last_tx_socket: AtomicU8, + should_tx: AtomicBool, +} + +pub(crate) struct SocketStack { + sockets: SocketSet<'static>, + waker: WakerRegistration, + dns_table: DnsTable, + dropped_sockets: heapless::Vec, + credential_map: heapless::FnvIndexMap, +} + +impl + UbloxStack +{ + pub fn new( + device: Device<'static, INGRESS_BUF_SIZE, URC_CAPACITY>, + resources: &'static mut StackResources, + ) -> Self { + let sockets = SocketSet::new(&mut resources.sockets[..]); + + let socket = SocketStack { + sockets, + dns_table: DnsTable::new(), + waker: WakerRegistration::new(), + dropped_sockets: heapless::Vec::new(), + credential_map: heapless::IndexMap::new(), + }; + + Self { + socket: RefCell::new(socket), + device, + last_tx_socket: AtomicU8::new(0), + should_tx: AtomicBool::new(false), + } + } + + pub async fn run(&self) -> ! { + let mut tx_buf = [0u8; MAX_EGRESS_SIZE]; + + let Device { + urc_channel, + state_ch, + at_client, + } = &self.device; + + let mut urc_subscription = urc_channel.subscribe().unwrap(); + + 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)); + futures_util::pin_mut!(ticker); + + match select::select3( + urc_subscription.next_message_pure(), + should_tx, + ticker.next(), + ) + .await + { + select::Either3::First(event) => { + Self::socket_rx(event, &self.socket); + } + select::Either3::Second(_) | select::Either3::Third(_) => { + if let Some(ev) = self.tx_event(&mut tx_buf) { + Self::socket_tx(ev, &self.socket, &at_client).await; + } + } + } + } + } + + /// 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) => + // FIXME: + // 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; + // FIXME: + // 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_table.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::Error(PingError::Timeout); + query.waker.wake(); + } + DnsState::Pending => { + query.state = DnsState::Resolved(ip); + query.waker.wake(); + } + _ => {} + } + } + } + EdmEvent::ATEvent(Urc::PingErrorResponse(PingErrorResponse { error })) => { + let mut s = socket.borrow_mut(); + for query in s.dns_table.table.iter_mut() { + match query.state { + DnsState::Pending => { + query.state = DnsState::Error(error); + query.waker.wake(); + } + _ => {} + } + } + } + _ => {} + } + } + + fn tx_event<'data>(&self, buf: &'data mut [u8]) -> Option> { + let mut s = self.socket.borrow_mut(); + for query in s.dns_table.table.iter_mut() { + if let DnsState::New = query.state { + query.state = DnsState::Pending; + buf[..query.domain_name.len()].copy_from_slice(query.domain_name.as_bytes()); + return Some(TxEvent::Dns { + hostname: core::str::from_utf8(&buf[..query.domain_name.len()]).unwrap(), + }); + } + } + + // 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(); + let skip = 0; + + let SocketStack { + sockets, + dns_table, + credential_map, + .. + } = s.deref_mut(); + + for (handle, socket) in 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 mut builder = PeerUrlBuilder::new(); + + if let Some(hostname) = dns_table.reverse_lookup(addr.ip()) { + builder.hostname(hostname).port(addr.port()) + } else { + builder.address(&addr) + }; + + if let Some(creds) = credential_map.get(&handle) { + info!("Found credentials {} for {}", creds, handle); + builder.creds(creds); + } + + let url = + builder.set_local_port(tcp.local_port).tcp::<128>().unwrap(); + + // FIXME: Write directly into `buf` instead + buf[..url.len()].copy_from_slice(url.as_bytes()); + + return Some(TxEvent::Connect { + socket_handle: handle, + url: core::str::from_utf8(&buf[..url.len()]).unwrap(), + }); + } + } + // 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 { + return tcp.tx_dequeue(|payload| { + let len = core::cmp::min(payload.len(), MAX_EGRESS_SIZE); + let res = if len != 0 { + buf[..len].copy_from_slice(&payload[..len]); + Some(TxEvent::Send { + edm_channel, + data: &buf[..len], + }) + } 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<'data>( + ev: TxEvent<'data>, + socket: &RefCell, + at_client: &RefCell>, + ) { + use atat::asynch::AtatClient; + + let mut at = at_client.borrow_mut(); + match ev { + TxEvent::Connect { socket_handle, url } => { + match at + .send_retry(&EdmAtCmdWrapper(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_retry(&EdmDataCommand { + channel: edm_channel, + data, + }) + .await + .ok(); + } + TxEvent::Close { peer_handle } => { + at.send_retry(&EdmAtCmdWrapper(ClosePeerConnection { peer_handle })) + .await + .ok(); + } + TxEvent::Dns { hostname } => { + match at + .send_retry(&EdmAtCmdWrapper(Ping { + hostname: &hostname, + retry_num: 1, + })) + .await + { + Ok(_) => {} + Err(_) => { + let mut s = socket.borrow_mut(); + if let Some(query) = s.dns_table.get_mut(&hostname) { + match query.state { + DnsState::Pending => { + query.state = DnsState::Error(PingError::Other); + 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.endpoint == Some(endpoint) => { + udp.edm_channel = Some(channel_id); + udp.set_state(UdpState::Established); + break; + } + _ => {} + }, + _ => {} + } + } + } +} + +// TODO: This extra data clone step can probably be avoided by adding a +// waker/context based API to ATAT. +enum TxEvent<'data> { + Connect { + socket_handle: SocketHandle, + url: &'data str, + }, + Send { + edm_channel: ChannelId, + data: &'data [u8], + }, + Close { + peer_handle: PeerHandle, + }, + Dns { + hostname: &'data str, + }, +} + +#[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/ublox-short-range/src/wifi/peer_builder.rs b/src/asynch/ublox_stack/peer_builder.rs similarity index 53% rename from ublox-short-range/src/wifi/peer_builder.rs rename to src/asynch/ublox_stack/peer_builder.rs index 1ad56a0..2e65114 100644 --- a/ublox-short-range/src/wifi/peer_builder.rs +++ b/src/asynch/ublox_stack/peer_builder.rs @@ -1,16 +1,22 @@ -use crate::{client::SecurityCredentials, error::Error}; +use crate::error::Error; use core::fmt::Write; -/// Handles receiving data from sockets -/// implements TCP and UDP for WiFi client -use embedded_nal::{IpAddr, SocketAddr}; use heapless::String; +use no_std_net::{IpAddr, SocketAddr}; + +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct SecurityCredentials { + pub ca_cert_name: heapless::String<16>, + pub c_cert_name: heapless::String<16>, + pub c_key_name: heapless::String<16>, +} #[derive(Default)] pub(crate) struct PeerUrlBuilder<'a> { hostname: Option<&'a str>, ip_addr: Option, port: Option, - creds: Option, + creds: Option<&'a SecurityCredentials>, local_port: Option, } @@ -20,7 +26,7 @@ impl<'a> PeerUrlBuilder<'a> { Self::default() } - pub fn write_domain(&self, s: &mut String<128>) -> Result<(), Error> { + fn write_domain(&self, s: &mut String) -> Result<(), Error> { let port = self.port.ok_or(Error::Network)?; let addr = self .ip_addr @@ -32,45 +38,42 @@ impl<'a> PeerUrlBuilder<'a> { addr.xor(host).ok_or(Error::Network) } - pub fn udp(&self) -> Result, Error> { + pub fn udp(&self) -> Result, Error> { let mut s = String::new(); - write!(&mut s, "udp://").ok(); + write!(&mut s, "udp://").map_err(|_| Error::Overflow)?; self.write_domain(&mut s)?; // Start writing query parameters - write!(&mut s, "?").ok(); - self.local_port - .map(|v| write!(&mut s, "local_port={}&", v).ok()); + write!(&mut s, "?").map_err(|_| Error::Overflow)?; + + if let Some(v) = self.local_port { + write!(&mut s, "local_port={}&", v).map_err(|_| Error::Overflow)?; + } + // Remove trailing '&' or '?' if no query. s.pop(); Ok(s) } - pub fn tcp(&mut self) -> Result, Error> { + pub fn tcp(&mut self) -> Result, Error> { let mut s = String::new(); - write!(&mut s, "tcp://").ok(); + write!(&mut s, "tcp://").map_err(|_| Error::Overflow)?; self.write_domain(&mut s)?; // Start writing query parameters - write!(&mut s, "?").ok(); - self.local_port - .map(|v| write!(&mut s, "local_port={}&", v).ok()); + write!(&mut s, "?").map_err(|_| Error::Overflow)?; - if let Some(creds) = self.creds.as_ref() { - creds - .ca_cert_name - .as_ref() - .map(|v| write!(&mut s, "ca={}&", v).ok()); - creds - .c_cert_name - .as_ref() - .map(|v| write!(&mut s, "cert={}&", v).ok()); - creds - .c_key_name - .as_ref() - .map(|v| write!(&mut s, "privKey={}&", v).ok()); + if let Some(v) = self.local_port { + write!(&mut s, "local_port={}&", v).map_err(|_| Error::Overflow)?; } + + if let Some(creds) = self.creds.as_ref() { + write!(&mut s, "ca={}&", creds.ca_cert_name).map_err(|_| Error::Overflow)?; + write!(&mut s, "cert={}&", creds.c_cert_name).map_err(|_| Error::Overflow)?; + write!(&mut s, "privKey={}&", creds.c_key_name).map_err(|_| Error::Overflow)?; + }; + // Remove trailing '&' or '?' if no query. s.pop(); @@ -86,19 +89,34 @@ impl<'a> PeerUrlBuilder<'a> { self } + pub fn set_hostname(&mut self, hostname: Option<&'a str>) -> &mut Self { + self.hostname = hostname; + self + } + /// maximum length 64 pub fn ip_addr(&mut self, ip_addr: IpAddr) -> &mut Self { self.ip_addr.replace(ip_addr); self } + pub fn set_ip_addr(&mut self, ip_addr: Option) -> &mut Self { + self.ip_addr = ip_addr; + self + } + /// port number pub fn port(&mut self, port: u16) -> &mut Self { self.port.replace(port); self } - pub fn creds(&mut self, creds: SecurityCredentials) -> &mut Self { + pub fn set_port(&mut self, port: Option) -> &mut Self { + self.port = port; + self + } + + pub fn creds(&mut self, creds: &'a SecurityCredentials) -> &mut Self { self.creds.replace(creds); self } @@ -107,6 +125,11 @@ impl<'a> PeerUrlBuilder<'a> { self.local_port.replace(local_port); self } + + pub fn set_local_port(&mut self, local_port: Option) -> &mut Self { + self.local_port = local_port; + self + } } #[cfg(test)] @@ -116,7 +139,10 @@ mod test { #[test] fn udp_ipv4_url() { let address = "192.168.0.1:8080".parse().unwrap(); - let url = PeerUrlBuilder::new().address(&address).udp().unwrap(); + let url = PeerUrlBuilder::new() + .address(&address) + .udp::<128>() + .unwrap(); assert_eq!(url, "udp://192.168.0.1:8080/"); } @@ -125,7 +151,10 @@ mod test { let address = "[FE80:0000:0000:0000:0202:B3FF:FE1E:8329]:8080" .parse() .unwrap(); - let url = PeerUrlBuilder::new().address(&address).udp().unwrap(); + let url = PeerUrlBuilder::new() + .address(&address) + .udp::<128>() + .unwrap(); assert_eq!(url, "udp://[fe80::202:b3ff:fe1e:8329]:8080/"); } @@ -135,7 +164,7 @@ mod test { .hostname("example.org") .port(2000) .local_port(2001) - .udp() + .udp::<128>() .unwrap(); assert_eq!(url, "udp://example.org:2000/?local_port=2001"); } @@ -145,13 +174,14 @@ mod test { let url = PeerUrlBuilder::new() .hostname("example.org") .port(2000) - .creds(SecurityCredentials { - c_cert_name: Some(heapless::String::from("client.crt")), - ca_cert_name: Some(heapless::String::from("ca.crt")), - c_key_name: Some(heapless::String::from("client.key")), + .creds(&SecurityCredentials { + c_cert_name: heapless::String::try_from("client.crt").unwrap(), + ca_cert_name: heapless::String::try_from("ca.crt").unwrap(), + c_key_name: heapless::String::try_from("client.key").unwrap(), }) - .tcp() + .tcp::<128>() .unwrap(); + assert_eq!( url, "tcp://example.org:2000/?ca=ca.crt&cert=client.crt&privKey=client.key" diff --git a/src/asynch/ublox_stack/tcp.rs b/src/asynch/ublox_stack/tcp.rs new file mode 100644 index 0000000..8706313 --- /dev/null +++ b/src/asynch/ublox_stack/tcp.rs @@ -0,0 +1,831 @@ +use core::cell::RefCell; +use core::future::poll_fn; +use core::mem; +use core::task::Poll; + +use embassy_time::Duration; +use embedded_nal_async::SocketAddr; +use ublox_sockets::{tcp, SocketHandle, TcpState}; + +use super::{SocketStack, UbloxStack}; + +/// Error returned by TcpSocket read/write functions. +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Error { + /// The connection was reset. + /// + /// This can happen on receiving a RST packet, or on timeout. + ConnectionReset, +} + +/// Error returned by [`TcpSocket::connect`]. +#[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, +} + +/// Error returned by [`TcpSocket::accept`]. +#[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, +} + +/// A TCP socket. +pub struct TcpSocket<'a> { + pub(crate) io: TcpIo<'a>, +} + +/// The reader half of a TCP socket. +pub struct TcpReader<'a> { + pub(crate) io: TcpIo<'a>, +} + +/// The writer half of a TCP socket. +pub struct TcpWriter<'a> { + pub(crate) io: TcpIo<'a>, +} + +impl<'a> TcpReader<'a> { + /// Read data from the socket. + /// + /// Returns how many bytes were read, or an error. If no data is available, it waits + /// until there is at least one byte available. + pub async fn read(&mut self, buf: &mut [u8]) -> Result { + self.io.read(buf).await + } + + /// Call `f` with the largest contiguous slice of octets in the receive buffer, + /// and dequeue the amount of elements returned by `f`. + /// + /// If no data is available, it waits until there is at least one byte available. + pub async fn read_with(&mut self, f: F) -> Result + where + F: FnOnce(&mut [u8]) -> (usize, R), + { + self.io.read_with(f).await + } + + /// Return the maximum number of bytes inside the transmit buffer. + pub fn recv_capacity(&self) -> usize { + self.io.recv_capacity() + } +} + +impl<'a> TcpWriter<'a> { + /// Write data to the socket. + /// + /// Returns how many bytes were written, or an error. If the socket is not ready to + /// accept data, it waits until it is. + pub async fn write(&mut self, buf: &[u8]) -> Result { + self.io.write(buf).await + } + + /// Flushes the written data to the socket. + /// + /// This waits until all data has been sent, and ACKed by the remote host. For a connection + /// closed with [`abort()`](TcpSocket::abort) it will wait for the TCP RST packet to be sent. + pub async fn flush(&mut self) -> Result<(), Error> { + self.io.flush().await + } + + /// Call `f` with the largest contiguous slice of octets in the transmit buffer, + /// and enqueue the amount of elements returned by `f`. + /// + /// If the socket is not ready to accept data, it waits until it is. + pub async fn write_with(&mut self, f: F) -> Result + where + F: FnOnce(&mut [u8]) -> (usize, R), + { + self.io.write_with(f).await + } + + /// Return the maximum number of bytes inside the transmit buffer. + pub fn send_capacity(&self) -> usize { + self.io.send_capacity() + } +} + +impl<'a> TcpSocket<'a> { + /// Create a new TCP socket on the given stack, with the given buffers. + 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, + }, + } + } + + /// Return the maximum number of bytes inside the recv buffer. + pub fn recv_capacity(&self) -> usize { + self.io.recv_capacity() + } + + /// Return the maximum number of bytes inside the transmit buffer. + pub fn send_capacity(&self) -> usize { + self.io.send_capacity() + } + + /// Call `f` with the largest contiguous slice of octets in the transmit buffer, + /// and enqueue the amount of elements returned by `f`. + /// + /// If the socket is not ready to accept data, it waits until it is. + pub async fn write_with(&mut self, f: F) -> Result + where + F: FnOnce(&mut [u8]) -> (usize, R), + { + self.io.write_with(f).await + } + + /// Call `f` with the largest contiguous slice of octets in the receive buffer, + /// and dequeue the amount of elements returned by `f`. + /// + /// If no data is available, it waits until there is at least one byte available. + pub async fn read_with(&mut self, f: F) -> Result + where + F: FnOnce(&mut [u8]) -> (usize, R), + { + self.io.read_with(f).await + } + + /// Split the socket into reader and a writer halves. + pub fn split(&mut self) -> (TcpReader<'_>, TcpWriter<'_>) { + (TcpReader { io: self.io }, TcpWriter { io: self.io }) + } + + /// Connect to a remote host. + 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 + } + + // /// Accept a connection from a remote host. + // /// + // /// This function puts the socket in listening mode, and waits until a connection is received. + // pub async fn accept(&mut self, local_endpoint: T) -> Result<(), AcceptError> + // where + // T: Into, + // { + // todo!() + // // 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 + // } + + /// Read data from the socket. + /// + /// Returns how many bytes were read, or an error. If no data is available, it waits + /// until there is at least one byte available. + pub async fn read(&mut self, buf: &mut [u8]) -> Result { + self.io.read(buf).await + } + + /// Write data to the socket. + /// + /// Returns how many bytes were written, or an error. If the socket is not ready to + /// accept data, it waits until it is. + pub async fn write(&mut self, buf: &[u8]) -> Result { + self.io.write(buf).await + } + + /// Flushes the written data to the socket. + /// + /// This waits until all data has been sent, and ACKed by the remote host. For a connection + /// closed with [`abort()`](TcpSocket::abort) it will wait for the TCP RST packet to be sent. + pub async fn flush(&mut self) -> Result<(), Error> { + self.io.flush().await + } + + /// Set the timeout for the socket. + /// + /// If the timeout is set, the socket will be closed if no data is received for the + /// specified duration. + pub fn set_timeout(&mut self, _duration: Option) { + todo!() + // self.io.with_mut(|s| s.set_timeout(duration)) + } + + /// Set the keep-alive interval for the socket. + /// + /// If the keep-alive interval is set, the socket will send keep-alive packets after + /// the specified duration of inactivity. + /// + /// If not set, the socket will not send keep-alive packets. + pub fn set_keep_alive(&mut self, _interval: Option) { + todo!() + // self.io + // .with_mut(|s| s.set_keep_alive(interval.map(duration_to_smoltcp))) + } + + // /// Set the hop limit field in the IP header of sent packets. + // pub fn set_hop_limit(&mut self, hop_limit: Option) { + // self.io.with_mut(|s| s.set_hop_limit(hop_limit)) + // } + + /// Get the local endpoint of the socket. + /// + /// Returns `None` if the socket is not bound (listening) or not connected. + pub fn local_endpoint(&self) -> Option { + todo!() + // self.io.with(|s| s.local_endpoint()) + } + + /// Get the remote endpoint of the socket. + /// + /// Returns `None` if the socket is not connected. + pub fn remote_endpoint(&self) -> Option { + self.io.with(|s| s.remote_endpoint()) + } + + /// Get the state of the socket. + pub fn state(&self) -> TcpState { + self.io.with(|s| s.state()) + } + + /// Close the write half of the socket. + /// + /// This closes only the write half of the socket. The read half side remains open, the + /// socket can still receive data. + /// + /// Data that has been written to the socket and not yet sent (or not yet ACKed) will still + /// still sent. The last segment of the pending to send data is sent with the FIN flag set. + pub fn close(&mut self) { + self.io.with_mut(|s| s.close()) + } + + /// Forcibly close the socket. + /// + /// This instantly closes both the read and write halves of the socket. Any pending data + /// that has not been sent will be lost. + /// + /// Note that the TCP RST packet is not sent immediately - if the `TcpSocket` is dropped too soon + /// the remote host may not know the connection has been closed. + /// `abort()` callers should wait for a [`flush()`](TcpSocket::flush) call to complete before + /// dropping or reusing the socket. + pub fn abort(&mut self) { + self.io.with_mut(|s| s.abort()) + } + + /// Get whether the socket is ready to send data, i.e. whether there is space in the send buffer. + pub fn may_send(&self) -> bool { + self.io.with(|s| s.may_send()) + } + + /// return whether the receive half of the full-duplex connection is open. + /// This function returns true if it’s possible to receive data from the remote endpoint. + /// It will return true while there is data in the receive buffer, and if there isn’t, + /// as long as the remote endpoint has not closed the connection. + pub fn may_recv(&self) -> bool { + self.io.with(|s| s.may_recv()) + } + + /// Get whether the socket is ready to receive data, i.e. whether there is some pending data in the receive buffer. + pub fn can_recv(&self) -> bool { + self.io.with(|s| s.can_recv()) + } +} + +impl<'a> Drop for TcpSocket<'a> { + fn drop(&mut self) { + if matches!( + self.state(), + TcpState::Listen | TcpState::Established | TcpState::FinWait1 + ) { + 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)] +pub(crate) struct TcpIo<'a> { + pub(crate) stack: &'a RefCell, + pub(crate) 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) if buf.is_empty() => { + // embedded_io_async::Read's contract is to not block if buf is empty. While + // this function is not a direct implementor of the trait method, we still don't + // want our future to never resolve. + Poll::Ready(Ok(0)) + } + // 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 write_with(&mut self, f: F) -> Result + where + F: FnOnce(&mut [u8]) -> (usize, R), + { + let mut f = Some(f); + + poll_fn(move |cx| { + self.with_mut(|s| { + if !s.can_send() { + if s.may_send() { + // socket buffer is full wait until it has atleast one byte free + s.register_send_waker(cx.waker()); + Poll::Pending + } else { + // if we can't transmit because the transmit half of the duplex connection is closed then return an error + Poll::Ready(Err(Error::ConnectionReset)) + } + } else { + Poll::Ready(match s.send(f.take().unwrap()) { + // Connection reset. TODO: this can also be timeouts etc, investigate. + // Err(tcp::SendError::InvalidState) => Err(Error::ConnectionReset), + Err(_) => Err(Error::ConnectionReset), + Ok(r) => Ok(r), + }) + } + }) + }) + .await + } + + async fn read_with(&mut self, f: F) -> Result + where + F: FnOnce(&mut [u8]) -> (usize, R), + { + let mut f = Some(f); + poll_fn(move |cx| { + self.with_mut(|s| { + if !s.can_recv() { + if s.may_recv() { + // socket buffer is empty wait until it has atleast one byte has arrived + s.register_recv_waker(cx.waker()); + Poll::Pending + } else { + // if we can't receive because the receive half of the duplex connection is closed then return an error + Poll::Ready(Err(Error::ConnectionReset)) + } + } else { + Poll::Ready(match s.recv(f.take().unwrap()) { + // Connection reset. TODO: this can also be timeouts etc, investigate. + // Err(tcp::RecvError::Finished) | Err(tcp::RecvError::InvalidState) => { + // Err(Error::ConnectionReset) + // } + Err(_) => Err(Error::ConnectionReset), + Ok(r) => Ok(r), + }) + } + }) + }) + .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 + } + + fn recv_capacity(&self) -> usize { + self.with(|s| s.recv_capacity()) + } + + fn send_capacity(&self) -> usize { + self.with(|s| s.send_capacity()) + } +} + +mod embedded_io_impls { + use super::*; + + impl embedded_io_async::Error for ConnectError { + fn kind(&self) -> embedded_io_async::ErrorKind { + match self { + ConnectError::ConnectionReset => embedded_io_async::ErrorKind::ConnectionReset, + ConnectError::TimedOut => embedded_io_async::ErrorKind::TimedOut, + ConnectError::NoRoute => embedded_io_async::ErrorKind::NotConnected, + ConnectError::InvalidState => embedded_io_async::ErrorKind::Other, + } + } + } + + impl embedded_io_async::Error for Error { + fn kind(&self) -> embedded_io_async::ErrorKind { + match self { + Error::ConnectionReset => embedded_io_async::ErrorKind::ConnectionReset, + } + } + } + + 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::ReadReady for TcpSocket<'d> { + fn read_ready(&mut self) -> Result { + Ok(self.io.with(|s| s.may_recv())) + } + } + + 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::WriteReady for TcpSocket<'d> { + fn write_ready(&mut self) -> Result { + Ok(self.io.with(|s| s.may_send())) + } + } + + 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::ReadReady for TcpReader<'d> { + fn read_ready(&mut self) -> Result { + Ok(self.io.with(|s| s.may_recv())) + } + } + + 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 + } + } + + impl<'d> embedded_io_async::WriteReady for TcpWriter<'d> { + fn write_ready(&mut self) -> Result { + Ok(self.io.with(|s| s.may_send())) + } + } +} + +/// TCP client compatible with `embedded-nal-async` traits. +pub mod client { + use core::cell::{Cell, UnsafeCell}; + use core::mem::MaybeUninit; + use core::ptr::NonNull; + + use crate::asynch::ublox_stack::dns::DnsSocket; + + use super::*; + + /// TCP client connection pool compatible with `embedded-nal-async` traits. + /// + /// The pool is capable of managing up to N concurrent connections with tx and rx buffers according to TX_SZ and RX_SZ. + pub struct TcpClient< + 'd, + const INGRESS_BUF_SIZE: usize, + const URC_CAPACITY: usize, + const N: usize, + const TX_SZ: usize = 1024, + const RX_SZ: usize = 1024, + > { + pub(crate) stack: &'d UbloxStack, + pub(crate) state: &'d TcpClientState, + } + + impl< + 'd, + const INGRESS_BUF_SIZE: usize, + const URC_CAPACITY: usize, + const N: usize, + const TX_SZ: usize, + const RX_SZ: usize, + > embedded_nal_async::Dns + for TcpClient<'d, INGRESS_BUF_SIZE, URC_CAPACITY, N, TX_SZ, RX_SZ> + { + type Error = crate::asynch::ublox_stack::dns::Error; + + async fn get_host_by_name( + &self, + host: &str, + addr_type: embedded_nal_async::AddrType, + ) -> Result { + DnsSocket::new(self.stack).query(host, addr_type).await + } + + async fn get_host_by_address( + &self, + _addr: no_std_net::IpAddr, + _result: &mut [u8], + ) -> Result { + unimplemented!() + } + } + + impl< + 'd, + const INGRESS_BUF_SIZE: usize, + const URC_CAPACITY: usize, + const N: usize, + const TX_SZ: usize, + const RX_SZ: usize, + > TcpClient<'d, INGRESS_BUF_SIZE, URC_CAPACITY, N, TX_SZ, RX_SZ> + { + /// Create a new `TcpClient`. + pub fn new( + stack: &'d UbloxStack, + state: &'d TcpClientState, + ) -> Self { + Self { stack, state } + } + } + + impl< + 'd, + const INGRESS_BUF_SIZE: usize, + const URC_CAPACITY: usize, + const N: usize, + const TX_SZ: usize, + const RX_SZ: usize, + > embedded_nal_async::TcpConnect + for TcpClient<'d, INGRESS_BUF_SIZE, URC_CAPACITY, N, 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: SocketAddr, + ) -> Result, Self::Error> { + 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) + } + } + + /// Opened TCP connection in a [`TcpClient`]. + 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 { + pub(crate) pool: Pool<([u8; TX_SZ], [u8; RX_SZ]), N>, + } + + impl TcpClientState { + /// Create a new `TcpClientState`. + pub const fn new() -> Self { + Self { pool: Pool::new() } + } + } + + pub(crate) struct Pool { + used: [Cell; N], + data: [UnsafeCell>; N], + } + + impl Pool { + const VALUE: Cell = Cell::new(false); + const UNINIT: UnsafeCell> = UnsafeCell::new(MaybeUninit::uninit()); + + pub(crate) const fn new() -> Self { + Self { + used: [Self::VALUE; N], + data: [Self::UNINIT; N], + } + } + } + + impl Pool { + pub(crate) fn alloc(&self) -> Option> { + for n in 0..N { + // this can't race because Pool is not Sync. + if !self.used[n].get() { + self.used[n].set(true); + 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. + pub(crate) 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].set(false); + } + } +} diff --git a/src/asynch/ublox_stack/tls.rs b/src/asynch/ublox_stack/tls.rs new file mode 100644 index 0000000..327f811 --- /dev/null +++ b/src/asynch/ublox_stack/tls.rs @@ -0,0 +1,435 @@ +use embassy_time::Duration; +use no_std_net::SocketAddr; +use ublox_sockets::TcpState as State; + +use super::peer_builder::SecurityCredentials; + +use super::{ + tcp::{ConnectError, Error, TcpIo, TcpReader, TcpSocket, TcpWriter}, + UbloxStack, +}; + +pub struct TlsSocket<'a> { + inner: TcpSocket<'a>, +} + +impl<'a> TlsSocket<'a> { + /// Create a new TCP socket on the given stack, with the given buffers. + pub fn new( + stack: &'a UbloxStack, + rx_buffer: &'a mut [u8], + tx_buffer: &'a mut [u8], + credentials: SecurityCredentials, + ) -> Self { + let tcp_socket = TcpSocket::new(stack, rx_buffer, tx_buffer); + + let TcpIo { stack, handle } = tcp_socket.io; + + let s = &mut *stack.borrow_mut(); + info!("Associating credentials {} with {}", credentials, handle); + s.credential_map.insert(handle, credentials).unwrap(); + + Self { inner: tcp_socket } + } + + /// Return the maximum number of bytes inside the recv buffer. + pub fn recv_capacity(&self) -> usize { + self.inner.recv_capacity() + } + + /// Return the maximum number of bytes inside the transmit buffer. + pub fn send_capacity(&self) -> usize { + self.inner.send_capacity() + } + + /// Call `f` with the largest contiguous slice of octets in the transmit buffer, + /// and enqueue the amount of elements returned by `f`. + /// + /// If the socket is not ready to accept data, it waits until it is. + pub async fn write_with(&mut self, f: F) -> Result + where + F: FnOnce(&mut [u8]) -> (usize, R), + { + self.inner.write_with(f).await + } + + /// Call `f` with the largest contiguous slice of octets in the receive buffer, + /// and dequeue the amount of elements returned by `f`. + /// + /// If no data is available, it waits until there is at least one byte available. + pub async fn read_with(&mut self, f: F) -> Result + where + F: FnOnce(&mut [u8]) -> (usize, R), + { + self.inner.read_with(f).await + } + + /// Split the socket into reader and a writer halves. + pub fn split(&mut self) -> (TcpReader<'_>, TcpWriter<'_>) { + ( + TcpReader { io: self.inner.io }, + TcpWriter { io: self.inner.io }, + ) + } + + /// Connect to a remote host. + pub async fn connect(&mut self, remote_endpoint: T) -> Result<(), ConnectError> + where + T: Into, + { + self.inner.connect(remote_endpoint).await + } + + // /// Accept a connection from a remote host. + // /// + // /// This function puts the socket in listening mode, and waits until a connection is received. + // pub async fn accept(&mut self, local_endpoint: T) -> Result<(), AcceptError> + // where + // T: Into, + // { + // todo!() + // // 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 + // } + + /// Read data from the socket. + /// + /// Returns how many bytes were read, or an error. If no data is available, it waits + /// until there is at least one byte available. + pub async fn read(&mut self, buf: &mut [u8]) -> Result { + self.inner.read(buf).await + } + + /// Write data to the socket. + /// + /// Returns how many bytes were written, or an error. If the socket is not ready to + /// accept data, it waits until it is. + pub async fn write(&mut self, buf: &[u8]) -> Result { + self.inner.write(buf).await + } + + /// Flushes the written data to the socket. + /// + /// This waits until all data has been sent, and ACKed by the remote host. For a connection + /// closed with [`abort()`](TlsSocket::abort) it will wait for the TCP RST packet to be sent. + pub async fn flush(&mut self) -> Result<(), Error> { + self.inner.flush().await + } + + /// Set the timeout for the socket. + /// + /// If the timeout is set, the socket will be closed if no data is received for the + /// specified duration. + pub fn set_timeout(&mut self, _duration: Option) { + todo!() + // self.inner.set_timeout(duration) + } + + /// Set the keep-alive interval for the socket. + /// + /// If the keep-alive interval is set, the socket will send keep-alive packets after + /// the specified duration of inactivity. + /// + /// If not set, the socket will not send keep-alive packets. + pub fn set_keep_alive(&mut self, interval: Option) { + self.inner.set_keep_alive(interval) + } + + // /// Set the hop limit field in the IP header of sent packets. + // pub fn set_hop_limit(&mut self, hop_limit: Option) { + // self.inner.set_hop_limit() + // } + + /// Get the local endpoint of the socket. + /// + /// Returns `None` if the socket is not bound (listening) or not connected. + pub fn local_endpoint(&self) -> Option { + todo!() + // self.inner.local_endpoint() + } + + /// Get the remote endpoint of the socket. + /// + /// Returns `None` if the socket is not connected. + pub fn remote_endpoint(&self) -> Option { + self.inner.remote_endpoint() + } + + /// Get the state of the socket. + pub fn state(&self) -> State { + self.inner.state() + } + + /// Close the write half of the socket. + /// + /// This closes only the write half of the socket. The read half side remains open, the + /// socket can still receive data. + /// + /// Data that has been written to the socket and not yet sent (or not yet ACKed) will still + /// still sent. The last segment of the pending to send data is sent with the FIN flag set. + pub fn close(&mut self) { + self.inner.close() + } + + /// Forcibly close the socket. + /// + /// This instantly closes both the read and write halves of the socket. Any pending data + /// that has not been sent will be lost. + /// + /// Note that the TCP RST packet is not sent immediately - if the `TlsSocket` is dropped too soon + /// the remote host may not know the connection has been closed. + /// `abort()` callers should wait for a [`flush()`](TlsSocket::flush) call to complete before + /// dropping or reusing the socket. + pub fn abort(&mut self) { + self.inner.abort() + } + + /// Get whether the socket is ready to send data, i.e. whether there is space in the send buffer. + pub fn may_send(&self) -> bool { + self.inner.may_send() + } + + /// return whether the receive half of the full-duplex connection is open. + /// This function returns true if it’s possible to receive data from the remote endpoint. + /// It will return true while there is data in the receive buffer, and if there isn’t, + /// as long as the remote endpoint has not closed the connection. + pub fn may_recv(&self) -> bool { + self.inner.may_recv() + } + + /// Get whether the socket is ready to receive data, i.e. whether there is some pending data in the receive buffer. + pub fn can_recv(&self) -> bool { + self.inner.can_recv() + } +} + +impl<'a> Drop for TlsSocket<'a> { + fn drop(&mut self) { + let mut stack = self.inner.io.stack.borrow_mut(); + stack.credential_map.remove(&self.inner.io.handle); + } +} + +mod embedded_io_impls { + use super::*; + + impl<'d> embedded_io_async::ErrorType for TlsSocket<'d> { + type Error = Error; + } + + impl<'d> embedded_io_async::Read for TlsSocket<'d> { + async fn read(&mut self, buf: &mut [u8]) -> Result { + self.inner.read(buf).await + } + } + + impl<'d> embedded_io_async::ReadReady for TlsSocket<'d> { + fn read_ready(&mut self) -> Result { + self.inner.read_ready() + } + } + + impl<'d> embedded_io_async::Write for TlsSocket<'d> { + async fn write(&mut self, buf: &[u8]) -> Result { + self.inner.write(buf).await + } + + async fn flush(&mut self) -> Result<(), Self::Error> { + self.inner.flush().await + } + } + + impl<'d> embedded_io_async::WriteReady for TlsSocket<'d> { + fn write_ready(&mut self) -> Result { + self.inner.write_ready() + } + } +} + +/// TLS client compatible with `embedded-nal-async` traits. +pub mod client { + use core::ptr::NonNull; + + use crate::asynch::ublox_stack::dns::DnsSocket; + use crate::asynch::ublox_stack::tcp::client::TcpClientState; + + use super::*; + + /// TLS client connection pool compatible with `embedded-nal-async` traits. + /// + /// The pool is capable of managing up to N concurrent connections with tx and rx buffers according to TX_SZ and RX_SZ. + pub struct TlsClient< + 'd, + const INGRESS_BUF_SIZE: usize, + const URC_CAPACITY: usize, + const N: usize, + const TX_SZ: usize = 1024, + const RX_SZ: usize = 1024, + > { + pub(crate) stack: &'d UbloxStack, + pub(crate) state: &'d TcpClientState, + pub(crate) credentials: SecurityCredentials, + } + + impl< + 'd, + const INGRESS_BUF_SIZE: usize, + const URC_CAPACITY: usize, + const N: usize, + const TX_SZ: usize, + const RX_SZ: usize, + > embedded_nal_async::Dns + for TlsClient<'d, INGRESS_BUF_SIZE, URC_CAPACITY, N, TX_SZ, RX_SZ> + { + type Error = crate::asynch::ublox_stack::dns::Error; + + async fn get_host_by_name( + &self, + host: &str, + addr_type: embedded_nal_async::AddrType, + ) -> Result { + DnsSocket::new(self.stack).query(host, addr_type).await + } + + async fn get_host_by_address( + &self, + _addr: no_std_net::IpAddr, + _result: &mut [u8], + ) -> Result { + unimplemented!() + } + } + + impl< + 'd, + const INGRESS_BUF_SIZE: usize, + const URC_CAPACITY: usize, + const N: usize, + const TX_SZ: usize, + const RX_SZ: usize, + > TlsClient<'d, INGRESS_BUF_SIZE, URC_CAPACITY, N, TX_SZ, RX_SZ> + { + /// Create a new `TlsClient`. + pub fn new( + stack: &'d UbloxStack, + state: &'d TcpClientState, + credentials: SecurityCredentials, + ) -> Self { + Self { + stack, + state, + credentials, + } + } + } + + impl< + 'd, + const INGRESS_BUF_SIZE: usize, + const URC_CAPACITY: usize, + const N: usize, + const TX_SZ: usize, + const RX_SZ: usize, + > embedded_nal_async::TcpConnect + for TlsClient<'d, INGRESS_BUF_SIZE, URC_CAPACITY, N, TX_SZ, RX_SZ> + { + type Error = Error; + type Connection<'m> = TlsConnection<'m, N, TX_SZ, RX_SZ> where Self: 'm; + + async fn connect<'a>( + &'a self, + remote: SocketAddr, + ) -> Result, Self::Error> { + let remote_endpoint = (remote.ip(), remote.port()); + let mut socket = TlsConnection::new(self.stack, self.state, self.credentials.clone())?; + socket + .socket + .connect(remote_endpoint) + .await + .map_err(|_| Error::ConnectionReset)?; + Ok(socket) + } + } + + /// Opened TLS connection in a [`TlsClient`]. + pub struct TlsConnection<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> { + socket: TlsSocket<'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> + TlsConnection<'d, N, TX_SZ, RX_SZ> + { + fn new( + stack: &'d UbloxStack, + state: &'d TcpClientState, + credentials: SecurityCredentials, + ) -> Result { + let mut bufs = state.pool.alloc().ok_or(Error::ConnectionReset)?; + Ok(Self { + socket: unsafe { + TlsSocket::new( + stack, + &mut bufs.as_mut().1, + &mut bufs.as_mut().0, + credentials, + ) + }, + state, + bufs, + }) + } + } + + impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> Drop + for TlsConnection<'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 TlsConnection<'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 TlsConnection<'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 TlsConnection<'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 + } + } +} diff --git a/src/asynch/ublox_stack/udp.rs b/src/asynch/ublox_stack/udp.rs new file mode 100644 index 0000000..f561f69 --- /dev/null +++ b/src/asynch/ublox_stack/udp.rs @@ -0,0 +1,247 @@ +//! UDP sockets. +use core::cell::RefCell; + +use core::mem; + +use embedded_nal_async::SocketAddr; +use ublox_sockets::{udp, SocketHandle, UdpState}; + +use super::{SocketStack, UbloxStack}; + +/// Error returned by [`UdpSocket::bind`]. +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum BindError { + /// The socket was already open. + InvalidState, + /// No route to host. + NoRoute, +} + +/// Error returned by [`UdpSocket::recv_from`] and [`UdpSocket::send_to`]. +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum SendError { + /// No route to host. + NoRoute, + /// Socket not bound to an outgoing port. + SocketNotBound, +} + +/// Error returned by [`UdpSocket::recv_from`] and [`UdpSocket::send_to`]. +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum RecvError { + /// Provided buffer was smaller than the received packet. + Truncated, +} + +/// An UDP socket. +pub struct UdpSocket<'a> { + stack: &'a RefCell, + handle: SocketHandle, +} + +impl<'a> UdpSocket<'a> { + /// Create a new UDP socket using the provided stack and buffers. + 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(udp::Socket::new( + udp::SocketBuffer::new(rx_buffer), + udp::SocketBuffer::new(tx_buffer), + )); + + Self { + stack: &stack.socket, + handle, + } + } + + // /// Bind the socket to a local endpoint. + // pub fn bind(&mut self, endpoint: T) -> Result<(), BindError> + // where + // T: Into, + // { + // let mut endpoint = endpoint.into(); + + // if endpoint.port == 0 { + // // If user didn't specify port allocate a dynamic port. + // endpoint.port = self.stack.borrow_mut().get_local_port(); + // } + + // match self.with_mut(|s| s.bind(endpoint)) { + // Ok(()) => Ok(()), + // // Err(udp::BindError::InvalidState) => Err(BindError::InvalidState), + // // Err(udp::BindError::Unaddressable) => Err(BindError::NoRoute), + // Err(_) => Err(BindError::InvalidState), + // } + // } + + fn with(&self, f: impl FnOnce(&udp::Socket) -> R) -> R { + let s = &*self.stack.borrow(); + let socket = s.sockets.get::(self.handle); + f(socket) + } + + fn with_mut(&self, f: impl FnOnce(&mut udp::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 + } + + // /// Receive a datagram. + // /// + // /// This method will wait until a datagram is received. + // /// + // /// Returns the number of bytes received and the remote endpoint. + // pub async fn recv_from(&self, buf: &mut [u8]) -> Result<(usize, IpEndpoint), RecvError> { + // poll_fn(move |cx| self.poll_recv_from(buf, cx)).await + // } + + // /// Receive a datagram. + // /// + // /// When no datagram is available, this method will return `Poll::Pending` and + // /// register the current task to be notified when a datagram is received. + // /// + // /// When a datagram is received, this method will return `Poll::Ready` with the + // /// number of bytes received and the remote endpoint. + // pub fn poll_recv_from( + // &self, + // buf: &mut [u8], + // cx: &mut Context<'_>, + // ) -> Poll> { + // self.with_mut(|s| match s.recv_slice(buf) { + // Ok((n, meta)) => Poll::Ready(Ok((n, meta.endpoint))), + // // No data ready + // // Err(udp::RecvError::Truncated) => Poll::Ready(Err(RecvError::Truncated)), + // // Err(udp::RecvError::Exhausted) => { + // Err(_) => { + // s.register_recv_waker(cx.waker()); + // Poll::Pending + // } + // }) + // } + + // /// Send a datagram to the specified remote endpoint. + // /// + // /// This method will wait until the datagram has been sent. + // /// + // /// When the remote endpoint is not reachable, this method will return `Err(SendError::NoRoute)` + // pub async fn send_to(&self, buf: &[u8], remote_endpoint: T) -> Result<(), SendError> + // where + // T: Into, + // { + // let remote_endpoint: IpEndpoint = remote_endpoint.into(); + // poll_fn(move |cx| self.poll_send_to(buf, remote_endpoint, cx)).await + // } + + // /// Send a datagram to the specified remote endpoint. + // /// + // /// When the datagram has been sent, this method will return `Poll::Ready(Ok())`. + // /// + // /// When the socket's send buffer is full, this method will return `Poll::Pending` + // /// and register the current task to be notified when the buffer has space available. + // /// + // /// When the remote endpoint is not reachable, this method will return `Poll::Ready(Err(Error::NoRoute))`. + // pub fn poll_send_to( + // &self, + // buf: &[u8], + // remote_endpoint: T, + // cx: &mut Context<'_>, + // ) -> Poll> + // where + // T: Into, + // { + // self.with_mut(|s| match s.send_slice(buf, remote_endpoint) { + // // Entire datagram has been sent + // Ok(()) => Poll::Ready(Ok(())), + // Err(udp::SendError::BufferFull) => { + // s.register_send_waker(cx.waker()); + // Poll::Pending + // } + // Err(udp::SendError::Unaddressable) => { + // // If no sender/outgoing port is specified, there is not really "no route" + // if s.endpoint().port == 0 { + // Poll::Ready(Err(SendError::SocketNotBound)) + // } else { + // Poll::Ready(Err(SendError::NoRoute)) + // } + // } + // }) + // } + + /// Returns the local endpoint of the socket. + pub fn endpoint(&self) -> Option { + self.with(|s| s.endpoint()) + } + + /// Returns whether the socket is open. + pub fn is_open(&self) -> bool { + self.with(|s| s.is_open()) + } + + /// Close the socket. + pub fn close(&mut self) { + self.with_mut(|s| s.close()) + } + + // /// Returns whether the socket is ready to send data, i.e. it has enough buffer space to hold a packet. + // pub fn may_send(&self) -> bool { + // self.with(|s| s.can_send()) + // } + + /// Returns whether the socket is ready to receive data, i.e. it has received a packet that's now in the buffer. + pub fn may_recv(&self) -> bool { + self.with(|s| s.can_recv()) + } + + // /// Return the maximum number packets the socket can receive. + // pub fn packet_recv_capacity(&self) -> usize { + // self.with(|s| s.packet_recv_capacity()) + // } + + // /// Return the maximum number packets the socket can receive. + // pub fn packet_send_capacity(&self) -> usize { + // self.with(|s| s.packet_send_capacity()) + // } + + // /// Return the maximum number of bytes inside the recv buffer. + // pub fn payload_recv_capacity(&self) -> usize { + // self.with(|s| s.payload_recv_capacity()) + // } + + // /// Return the maximum number of bytes inside the transmit buffer. + // pub fn payload_send_capacity(&self) -> usize { + // self.with(|s| s.payload_send_capacity()) + // } + + // /// Set the hop limit field in the IP header of sent packets. + // pub fn set_hop_limit(&mut self, hop_limit: Option) { + // self.with_mut(|s| s.set_hop_limit(hop_limit)) + // } +} + +impl<'a> Drop for UdpSocket<'a> { + fn drop(&mut self) { + if matches!(self.with(|s| s.state()), UdpState::Established) { + if let Some(peer_handle) = self.with(|s| s.peer_handle) { + self.stack + .borrow_mut() + .dropped_sockets + .push(peer_handle) + .ok(); + } + } + let mut stack = self.stack.borrow_mut(); + stack.sockets.remove(self.handle); + stack.waker.wake(); + } +} diff --git a/ublox-short-range/src/command/custom_digest.rs b/src/command/custom_digest.rs similarity index 89% rename from ublox-short-range/src/command/custom_digest.rs rename to src/command/custom_digest.rs index 20c4dde..2b4b537 100644 --- a/ublox-short-range/src/command/custom_digest.rs +++ b/src/command/custom_digest.rs @@ -10,6 +10,12 @@ use super::edm::types::{AUTOCONNECTMESSAGE, STARTUPMESSAGE}; #[derive(Debug, Default)] pub struct EdmDigester; +impl EdmDigester { + pub fn new() -> Self { + Self + } +} + impl Digester for EdmDigester { fn digest<'a>(&mut self, buf: &'a [u8]) -> (DigestResult<'a>, usize) { // TODO: Handle module restart, tests and set default startupmessage in client, and optimize this! @@ -18,12 +24,12 @@ impl Digester for EdmDigester { return (DigestResult::None, 0); } - defmt::trace!("Digest {:?}", LossyStr(&buf)); + trace!("Digest {:?}", LossyStr(buf)); if buf.len() >= STARTUPMESSAGE.len() && buf[..2] == *b"\r\n" { if let Some(i) = buf[2..].windows(2).position(|x| x == *b"\r\n") { // Two for starting position, one for index -> len and one for the window size. let len = i + 4; - defmt::trace!("Digest common at {:?}; i: {:?}", LossyStr(&buf[..len]), i); + trace!("Digest common at {:?}; i: {:?}", LossyStr(&buf[..len]), i); if buf[..len] == *STARTUPMESSAGE { return ( DigestResult::Urc(&buf[..STARTUPMESSAGE.len()]), @@ -67,7 +73,7 @@ impl Digester for EdmDigester { // Debug statement for trace properly if !buf.is_empty() { - defmt::trace!("Digest {:?}", LossyStr(&buf)); + trace!("Digest {:?}", LossyStr(buf)); } // Filter message by payload @@ -84,8 +90,7 @@ impl Digester for EdmDigester { }; (return_val, edm_len) } - PayloadType::StartEvent => (DigestResult::Urc(&buf[..edm_len]), edm_len), - // PayloadType::StartEvent => (DigestResult::Response(Ok(&buf[..edm_len])), edm_len), + PayloadType::StartEvent => (DigestResult::Response(Ok(&buf[..edm_len])), edm_len), PayloadType::ATEvent | PayloadType::ConnectEvent | PayloadType::DataEvent @@ -103,49 +108,21 @@ impl Digester for EdmDigester { // #[cfg(test)] // mod test { - // use super::*; -// use atat::bbqueue::framed::FrameConsumer; -// use atat::{frame::Frame, AtatIngress, Buffers, Ingress, Response}; +// use atat::Config; +// use atat::{AtatIngress, Buffers, Response, blocking::AtatClient}; // const TEST_RX_BUF_LEN: usize = 256; // const TEST_RES_CAPACITY: usize = 3 * TEST_RX_BUF_LEN; // const TEST_URC_CAPACITY: usize = 3 * TEST_RX_BUF_LEN; -// // macro_rules! setup_ingressmanager { -// // () => {{ -// // static mut RES_Q: BBBuffer = BBBuffer::new(); -// // let (res_p, res_c) = unsafe { RES_Q.try_split_framed().unwrap() }; - -// // static mut URC_Q: BBBuffer = BBBuffer::new(); -// // let (urc_p, urc_c) = unsafe { URC_Q.try_split_framed().unwrap() }; - -// // ( -// // IngressManager::<_, TEST_RX_BUF_LEN, TEST_RES_CAPACITY, TEST_URC_CAPACITY>::new( -// // res_p, -// // urc_p, -// // EdmDigester::default(), -// // ), -// // res_c, -// // urc_c, -// // ) -// // }}; -// // } +// struct MockWriter; // /// Removed functionality used to change OK responses to empty responses. // #[test] // fn ok_response<'a>() { -// let mut at_pars: Ingress< -// 'a, -// EdmDigester, -// TEST_RX_BUF_LEN, -// TEST_RES_CAPACITY, -// TEST_URC_CAPACITY, -// >; -// let mut res_c: FrameConsumer<'a, TEST_RES_CAPACITY>; -// let mut urc_c: FrameConsumer<'a, TEST_URC_CAPACITY>; // let buf = Buffers::::new(); -// (at_pars, res_c, urc_c) = buf.to_ingress(EdmDigester::default()); +// (at_pars, client) = buf.split_blocking(MockWriter, EdmDigester::default(), Config::new()); // // Payload: "OK\r\n" // let data = &[0xAA, 0x00, 0x06, 0x00, 0x45, 0x4f, 0x4b, 0x0D, 0x0a, 0x55]; @@ -154,7 +131,7 @@ impl Digester for EdmDigester { // let ingress_buf = at_pars.write_buf(); // let len = usize::min(data.len(), ingress_buf.len()); // ingress_buf[..len].copy_from_slice(&data[..len]); -// at_pars.try_advance(len); +// at_pars.try_advance(len).unwrap(); // let mut grant = res_c.read().unwrap(); // grant.auto_release(true); @@ -167,6 +144,7 @@ impl Digester for EdmDigester { // assert_eq!(res, Ok(&empty_ok_response[..])); // assert_eq!(urc_c.read(), None); // } +// } // #[test] // fn error_response() { @@ -243,7 +221,7 @@ impl Digester for EdmDigester { // assert_eq!(urc_c.read(), None); // } -// /// Regular response with traling regular response.. +// /// Regular response with trailing regular response.. // #[test] // fn at_urc() { // let mut at_pars: Ingress< diff --git a/ublox-short-range/src/command/data_mode/mod.rs b/src/command/data_mode/mod.rs similarity index 93% rename from ublox-short-range/src/command/data_mode/mod.rs rename to src/command/data_mode/mod.rs index 18062e9..ca9cdfb 100644 --- a/ublox-short-range/src/command/data_mode/mod.rs +++ b/src/command/data_mode/mod.rs @@ -8,7 +8,7 @@ use heapless::String; use responses::*; use types::*; -use super::{NoResponse, PeerHandle}; +use super::NoResponse; /// 5.1 Enter data mode O /// @@ -27,8 +27,9 @@ pub struct ChangeMode { /// Connects to an enabled service on a remote device. When the host connects to a /// service on a remote device, it implicitly registers to receive the "Connection Closed" /// event. +#[cfg(feature = "internal-network-stack")] #[derive(Clone, AtatCmd)] -#[at_cmd("+UDCP", ConnectPeerResponse, timeout_ms = 1000)] +#[at_cmd("+UDCP", ConnectPeerResponse, timeout_ms = 5000)] pub struct ConnectPeer<'a> { #[at_arg(position = 0, len = 128)] pub url: &'a str, @@ -37,11 +38,12 @@ pub struct ConnectPeer<'a> { /// 5.3 Close peer connection +UDCPC /// /// Closes an existing peer connection. +#[cfg(feature = "internal-network-stack")] #[derive(Clone, AtatCmd)] #[at_cmd("+UDCPC", NoResponse, timeout_ms = 1000)] pub struct ClosePeerConnection { - #[at_arg(position = 0)] - pub peer_handle: PeerHandle, + #[at_arg(position = 0, len = 1)] + pub peer_handle: ublox_sockets::PeerHandle, } /// 5.4 Default remote peer +UDDRP @@ -64,6 +66,7 @@ pub struct SetDefaultRemotePeer<'a> { /// 5.5 Peer list +UDLP /// /// This command reads the connected peers (peer handle). +#[cfg(feature = "internal-network-stack")] #[derive(Clone, AtatCmd)] #[at_cmd("+UDLP?", PeerListResponse, timeout_ms = 1000)] pub struct PeerList; @@ -126,7 +129,7 @@ pub struct SetWatchdogSettings { /// /// Writes peer configuration. /// -/// Suported parameter tags | Software Version +/// Supported parameter tags | Software Version /// ------------------------|------------------ /// 0,1 | All versions /// 2 | >= 4.0.0 diff --git a/ublox-short-range/src/command/data_mode/responses.rs b/src/command/data_mode/responses.rs similarity index 64% rename from ublox-short-range/src/command/data_mode/responses.rs rename to src/command/data_mode/responses.rs index 3f64833..fddac87 100644 --- a/ublox-short-range/src/command/data_mode/responses.rs +++ b/src/command/data_mode/responses.rs @@ -1,26 +1,26 @@ //! Responses for Data Mode -use super::PeerHandle; use atat::atat_derive::AtatResp; -use heapless::String; /// 5.2 Connect peer +UDCP +#[cfg(feature = "internal-network-stack")] #[derive(Clone, AtatResp)] pub struct ConnectPeerResponse { #[at_arg(position = 0)] - pub peer_handle: PeerHandle, + pub peer_handle: ublox_sockets::PeerHandle, } /// 5.5 Peer list +UDLP +#[cfg(feature = "internal-network-stack")] #[derive(Clone, AtatResp)] pub struct PeerListResponse { #[at_arg(position = 0)] - pub peer_handle: PeerHandle, + pub peer_handle: ublox_sockets::PeerHandle, #[at_arg(position = 1)] - pub protocol: String<64>, + pub protocol: heapless::String<64>, #[at_arg(position = 2)] - pub local_address: String<64>, + pub local_address: heapless::String<64>, #[at_arg(position = 3)] - pub remote_address: String<64>, + pub remote_address: heapless::String<64>, } /// 5.12 Bind +UDBIND diff --git a/ublox-short-range/src/command/data_mode/types.rs b/src/command/data_mode/types.rs similarity index 92% rename from ublox-short-range/src/command/data_mode/types.rs rename to src/command/data_mode/types.rs index 9bbc5f6..6db7c29 100644 --- a/ublox-short-range/src/command/data_mode/types.rs +++ b/src/command/data_mode/types.rs @@ -5,6 +5,7 @@ use heapless::String; use crate::command::OnOff; #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum Mode { /// Command mode @@ -19,6 +20,7 @@ pub enum Mode { } #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum ConnectScheme { /// Always connected - Keep the peer connected when not in command mode. @@ -38,11 +40,13 @@ pub enum ConnectScheme { Both = 0b110, } #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum ServerConfig { Type(ServerType), Url(String<128>), } #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum ServerType { #[at_arg(value = 0)] Disabled, @@ -63,6 +67,7 @@ pub enum ServerType { } #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum Interface { TCP = 1, @@ -75,6 +80,7 @@ pub enum Interface { } #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum UDPBehaviour { /// No connect. This will trigger an +UUDPC URC immediately (with @@ -90,6 +96,7 @@ pub enum UDPBehaviour { } #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum ImmediateFlush { Disable = 0, @@ -97,6 +104,7 @@ pub enum ImmediateFlush { } #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum IPVersion { /// Default @@ -105,6 +113,7 @@ pub enum IPVersion { } #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum RemoteConfiguration { Disable = 0, @@ -112,6 +121,7 @@ pub enum RemoteConfiguration { } #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum WatchdogSetting { /// SPP (and all SPP based protocols like DUN) write timeout: is the time in @@ -158,6 +168,7 @@ pub enum WatchdogSetting { } #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum PeerConfigParameter { /// Keep remote peer in the command mode /// - Off: Disconnect peers when entering the command mode @@ -218,6 +229,7 @@ pub enum PeerConfigParameter { } #[derive(Debug, Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum ConnectionType { Bluetooth = 1, @@ -236,6 +248,7 @@ pub enum ConnectionType { // } #[derive(Debug, Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum IPProtocol { TCP = 0, diff --git a/ublox-short-range/src/command/data_mode/urc.rs b/src/command/data_mode/urc.rs similarity index 52% rename from ublox-short-range/src/command/data_mode/urc.rs rename to src/command/data_mode/urc.rs index cdda3af..b768654 100644 --- a/ublox-short-range/src/command/data_mode/urc.rs +++ b/src/command/data_mode/urc.rs @@ -1,15 +1,14 @@ //! Unsolicited responses for Data mode Commands -use crate::command::PeerHandle; - +#[allow(unused_imports)] use super::types::*; -use atat::atat_derive::AtatResp; -use atat::heapless_bytes::Bytes; /// 5.10 Peer connected +UUDPC -#[derive(Debug, PartialEq, Clone, AtatResp)] +#[cfg(feature = "internal-network-stack")] +#[derive(Debug, PartialEq, Clone, atat::atat_derive::AtatResp)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct PeerConnected { #[at_arg(position = 0)] - pub handle: PeerHandle, + pub handle: ublox_sockets::PeerHandle, #[at_arg(position = 1)] pub connection_type: ConnectionType, #[at_arg(position = 2)] @@ -17,20 +16,23 @@ pub struct PeerConnected { // #[at_arg(position = 3)] // pub local_address: IpAddr, #[at_arg(position = 3)] - pub local_address: Bytes<40>, + #[cfg_attr(feature = "defmt", defmt(Debug2Format))] + pub local_address: atat::heapless_bytes::Bytes<40>, #[at_arg(position = 4)] pub local_port: u16, // #[at_arg(position = 5)] // pub remote_address: IpAddr, #[at_arg(position = 5)] - pub remote_address: Bytes<40>, + #[cfg_attr(feature = "defmt", defmt(Debug2Format))] + pub remote_address: atat::heapless_bytes::Bytes<40>, #[at_arg(position = 6)] pub remote_port: u16, } /// 5.11 Peer disconnected +UUDPD -#[derive(Debug, PartialEq, Clone, AtatResp)] +#[cfg(feature = "internal-network-stack")] +#[derive(Debug, PartialEq, Clone, atat::atat_derive::AtatResp)] pub struct PeerDisconnected { #[at_arg(position = 0)] - pub handle: PeerHandle, + pub handle: ublox_sockets::PeerHandle, } diff --git a/ublox-short-range/src/command/edm/mod.rs b/src/command/edm/mod.rs similarity index 68% rename from ublox-short-range/src/command/edm/mod.rs rename to src/command/edm/mod.rs index 324b196..61694fb 100644 --- a/ublox-short-range/src/command/edm/mod.rs +++ b/src/command/edm/mod.rs @@ -6,11 +6,12 @@ use core::convert::TryInto; use crate::command::{data_mode, data_mode::ChangeMode}; use crate::command::{NoResponse, Urc}; -use crate::wifi::EGRESS_CHUNK_SIZE; +// use crate::wifi::EGRESS_CHUNK_SIZE; /// Containing EDM structs with custom serialaization and deserilaisation. use atat::AtatCmd; -use heapless::Vec; + use types::*; +use ublox_sockets::ChannelId; pub(crate) fn calc_payload_len(resp: &[u8]) -> usize { (u16::from_be_bytes(resp[1..3].try_into().unwrap()) & EDM_FULL_SIZE_FILTER) as usize @@ -22,97 +23,30 @@ pub(crate) fn calc_payload_len(resp: &[u8]) -> usize { // using the parameter. Instead the parameter must // be set to 0 and the serial settings will take effect when the module is reset. #[derive(Debug, Clone)] -pub(crate) struct EdmAtCmdWrapper, const LEN: usize>(pub T); +pub(crate) struct EdmAtCmdWrapper(pub T); -impl atat::AtatCmd<1024> for EdmAtCmdWrapper -where - T: AtatCmd, -{ +impl atat::AtatCmd for EdmAtCmdWrapper { type Response = T::Response; + const MAX_LEN: usize = T::MAX_LEN + 6; + const MAX_TIMEOUT_MS: u32 = T::MAX_TIMEOUT_MS; - fn as_bytes(&self) -> Vec { - let at_vec = self.0.as_bytes(); - let payload_len = (at_vec.len() + 2) as u16; - [ + fn write(&self, buf: &mut [u8]) -> usize { + let at_len = self.0.write(&mut buf[5..]); + let payload_len = (at_len + 2) as u16; + + buf[0..5].copy_from_slice(&[ STARTBYTE, (payload_len >> 8) as u8 & EDM_SIZE_FILTER, (payload_len & 0xffu16) as u8, 0x00, PayloadType::ATRequest as u8, - ] - .iter() - .cloned() - .chain(at_vec) - .chain(core::iter::once(ENDBYTE)) - .collect() - } - - fn parse( - &self, - resp: Result<&[u8], atat::InternalError>, - ) -> core::result::Result { - let resp = resp.and_then(|resp| { - if resp.len() < PAYLOAD_OVERHEAD - || !resp.starts_with(&[STARTBYTE]) - || !resp.ends_with(&[ENDBYTE]) - { - return Err(atat::InternalError::InvalidResponse); - }; - - let payload_len = calc_payload_len(resp); - - if resp.len() != payload_len + EDM_OVERHEAD - || resp[4] != PayloadType::ATConfirmation as u8 - { - return Err(atat::InternalError::InvalidResponse); - } - - // Recieved OK response code in EDM response? - match resp - .windows(b"\r\nOK".len()) - .position(|window| window == b"\r\nOK") - { - // Cutting OK out leaves an empth string for NoResponse, for - // other responses just removes "\r\nOK\r\n" - Some(pos) => Ok(&resp[AT_COMMAND_POSITION..pos]), - // Isolate the AT_response - None => Ok(&resp[AT_COMMAND_POSITION..PAYLOAD_POSITION + payload_len]), - } - }); - - self.0.parse(resp) - } -} - -/////////////////////// Temp Solution for fixed size /////////////////////// -#[derive(Debug, Clone)] -pub(crate) struct BigEdmAtCmdWrapper, const LEN: usize>(pub T); - -impl atat::AtatCmd<2054> for BigEdmAtCmdWrapper -where - T: AtatCmd, -{ - type Response = T::Response; + ]); - const MAX_TIMEOUT_MS: u32 = T::MAX_TIMEOUT_MS; + buf[5 + at_len] = ENDBYTE; - fn as_bytes(&self) -> Vec { - let at_vec = self.0.as_bytes(); - let payload_len = (at_vec.len() + 2) as u16; - [ - STARTBYTE, - (payload_len >> 8) as u8 & EDM_SIZE_FILTER, - (payload_len & 0xffu16) as u8, - 0x00, - PayloadType::ATRequest as u8, - ] - .iter() - .cloned() - .chain(at_vec) - .chain(core::iter::once(ENDBYTE)) - .collect() + 5 + at_len + 1 } fn parse( @@ -135,7 +69,7 @@ where return Err(atat::InternalError::InvalidResponse); } - // Recieved OK response code in EDM response? + // Received OK response code in EDM response? match resp .windows(b"\r\nOK".len()) .position(|window| window == b"\r\nOK") @@ -151,7 +85,6 @@ where self.0.parse(resp) } } -////////////////////////////////////////////////////////////////////////////////////////////// #[derive(Debug, Clone)] pub struct EdmDataCommand<'a> { @@ -159,54 +92,57 @@ pub struct EdmDataCommand<'a> { pub data: &'a [u8], } // wifi::socket::EGRESS_CHUNK_SIZE + PAYLOAD_OVERHEAD = 512 + 6 + 1 = 519 -impl<'a> atat::AtatCmd<{ EGRESS_CHUNK_SIZE + 7 }> for EdmDataCommand<'a> { +impl<'a> atat::AtatCmd for EdmDataCommand<'a> { type Response = NoResponse; + const MAX_LEN: usize = DATA_PACKAGE_SIZE + 7; + const EXPECTS_RESPONSE_CODE: bool = false; - fn as_bytes(&self) -> Vec { + fn parse( + &self, + _resp: Result<&[u8], atat::InternalError>, + ) -> core::result::Result { + Ok(NoResponse) + } + + fn write(&self, buf: &mut [u8]) -> usize { let payload_len = (self.data.len() + 3) as u16; - [ + buf[0..6].copy_from_slice(&[ STARTBYTE, (payload_len >> 8) as u8 & EDM_SIZE_FILTER, (payload_len & 0xffu16) as u8, 0x00, PayloadType::DataCommand as u8, self.channel.0, - ] - .iter() - .cloned() - .chain(self.data.iter().cloned()) - .chain(core::iter::once(ENDBYTE)) - .collect() - } + ]); - fn parse( - &self, - _resp: Result<&[u8], atat::InternalError>, - ) -> core::result::Result { - Ok(NoResponse) + buf[6..6 + self.data.len()].copy_from_slice(self.data); + buf[6 + self.data.len()] = ENDBYTE; + + 6 + self.data.len() + 1 } } #[derive(Debug, Clone)] pub struct EdmResendConnectEventsCommand; -impl atat::AtatCmd<6> for EdmResendConnectEventsCommand { +impl atat::AtatCmd for EdmResendConnectEventsCommand { type Response = NoResponse; - fn as_bytes(&self) -> Vec { - [ + const MAX_LEN: usize = 6; + + fn write(&self, buf: &mut [u8]) -> usize { + buf[0..6].copy_from_slice(&[ STARTBYTE, 0x00, 0x02, 0x00, PayloadType::ResendConnectEventsCommand as u8, ENDBYTE, - ] - .iter() - .cloned() - .collect() + ]); + + 6 } fn parse( @@ -220,32 +156,30 @@ impl atat::AtatCmd<6> for EdmResendConnectEventsCommand { #[derive(Debug, Clone)] pub struct SwitchToEdmCommand; -impl atat::AtatCmd<6> for SwitchToEdmCommand { +impl atat::AtatCmd for SwitchToEdmCommand { type Response = NoResponse; + const MAX_LEN: usize = 6; + const MAX_TIMEOUT_MS: u32 = 2000; - fn as_bytes(&self) -> Vec { + fn write(&self, buf: &mut [u8]) -> usize { ChangeMode { mode: data_mode::types::Mode::ExtendedDataMode, } - .as_bytes() - .into_iter() - .collect() + .write(buf) } fn parse( &self, - _resp: Result<&[u8], atat::InternalError>, + resp: Result<&[u8], atat::InternalError>, ) -> core::result::Result { - // let resp = resp?; - // // Parse EDM startup command - // let correct = &[0xAA, 0x00, 0x02, 0x00, 0x71, 0x55]; // &[0xAA,0x00,0x06,0x00,0x45,0x4f,0x4b,0x0D,0x0a,0x55]; // AA 00 06 00 44 41 54 0D 0A 0D 0A 4F 4B 0D 0A 55 ? - // if resp.len() != correct.len() - // || resp[.. correct.len()] != *correct { - // // TODO: check this - // return Err(atat::Error::InvalidResponse); - // } + let resp = resp?; + // Parse EDM startup command + let correct = &[0xAA, 0x00, 0x02, 0x00, 0x71, 0x55]; + if resp.len() != correct.len() || resp[..correct.len()] != *correct { + return Err(atat::Error::InvalidResponse); + } Ok(NoResponse) } } @@ -278,7 +212,11 @@ mod test { PayloadType::ATConfirmation as u8, 0x55, ]; - assert_eq!(parse.as_bytes(), correct_cmd); + + let mut buf = [0u8; as AtatCmd>::MAX_LEN]; + let len = parse.write(&mut buf); + + assert_eq!(buf[..len], correct_cmd); assert_eq!(parse.parse(Ok(response)), Ok(correct_response)); let parse = EdmAtCmdWrapper(SystemStatus { @@ -318,7 +256,10 @@ mod test { 0x0A, 0x55, ]; - assert_eq!(parse.as_bytes(), correct); + let mut buf = [0u8; as AtatCmd>::MAX_LEN]; + let len = parse.write(&mut buf); + + assert_eq!(buf[..len], correct); assert_eq!(parse.parse(Ok(response)), Ok(correct_response)); } @@ -450,7 +391,11 @@ mod test { fn change_to_edm_cmd() { let resp = &[0xAA, 0x00, 0x02, 0x00, 0x71, 0x55]; let correct = Vec::<_, 6>::from_slice(b"ATO2\r\n").unwrap(); - assert_eq!(SwitchToEdmCommand.as_bytes(), correct); + + let mut buf = [0u8; SwitchToEdmCommand::MAX_LEN]; + let len = SwitchToEdmCommand.write(&mut buf); + + assert_eq!(buf[..len], correct); assert_eq!(SwitchToEdmCommand.parse(Ok(resp)).unwrap(), NoResponse); } } diff --git a/ublox-short-range/src/command/edm/types.rs b/src/command/edm/types.rs similarity index 90% rename from ublox-short-range/src/command/edm/types.rs rename to src/command/edm/types.rs index 2b3e5b6..08690cf 100644 --- a/ublox-short-range/src/command/edm/types.rs +++ b/src/command/edm/types.rs @@ -1,29 +1,12 @@ -use atat::atat_derive::AtatLen; -use embedded_nal::{Ipv4Addr, Ipv6Addr}; use heapless::Vec; +use no_std_net::{Ipv4Addr, Ipv6Addr}; use serde::{Deserialize, Serialize}; +use ublox_sockets::ChannelId; /// Start byte, Length: u16, Id+Type: u16, Endbyte // type EdmAtCmdOverhead = (u8, u16, u16, u8); -#[derive( - Debug, - Clone, - Copy, - PartialEq, - Eq, - PartialOrd, - AtatLen, - Ord, - Default, - Serialize, - Deserialize, - defmt::Format, - hash32_derive::Hash32, -)] -pub struct ChannelId(pub u8); - -pub const DATA_PACKAGE_SIZE: usize = 2304; +pub const DATA_PACKAGE_SIZE: usize = 4096; pub const STARTBYTE: u8 = 0xAA; pub const ENDBYTE: u8 = 0x55; pub const EDM_SIZE_FILTER: u8 = 0x0F; @@ -98,7 +81,7 @@ pub struct BluetoothConnectEvent { pub frame_size: u16, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct IPv4ConnectEvent { pub channel_id: ChannelId, pub protocol: Protocol, @@ -107,7 +90,7 @@ pub struct IPv4ConnectEvent { pub local_ip: Ipv4Addr, pub local_port: u16, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct IPv6ConnectEvent { pub channel_id: ChannelId, pub protocol: Protocol, @@ -117,7 +100,7 @@ pub struct IPv6ConnectEvent { pub local_port: u16, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct DataEvent { pub channel_id: ChannelId, pub data: Vec, @@ -151,7 +134,7 @@ impl From for ConnectType { } } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[repr(u8)] pub enum Protocol { TCP = 0x00, diff --git a/ublox-short-range/src/command/edm/urc.rs b/src/command/edm/urc.rs similarity index 93% rename from ublox-short-range/src/command/edm/urc.rs rename to src/command/edm/urc.rs index 34ee3ad..1f78606 100644 --- a/ublox-short-range/src/command/edm/urc.rs +++ b/src/command/edm/urc.rs @@ -3,8 +3,9 @@ use super::types::*; use super::Urc; use atat::helpers::LossyStr; use atat::AtatUrc; -use embedded_nal::{Ipv4Addr, Ipv6Addr}; use heapless::Vec; +use no_std_net::{Ipv4Addr, Ipv6Addr}; +use ublox_sockets::ChannelId; #[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq)] @@ -20,13 +21,22 @@ pub enum EdmEvent { StartUp, } +impl EdmEvent { + pub fn extract_urc(self) -> Option { + match self { + EdmEvent::ATEvent(urc) => Some(urc), + _ => None, + } + } +} + impl AtatUrc for EdmEvent { /// The type of the response. Usually the enum this trait is implemented on. - type Response = EdmEvent; + type Response = Self; /// Parse the response into a `Self::Response` instance. fn parse(resp: &[u8]) -> Option { - defmt::trace!("[Parse URC] {:?}", LossyStr(resp)); + trace!("[Parse URC] {:?}", LossyStr(resp)); // Startup message? // TODO: simplify mayby no packet check. if resp.len() >= STARTUPMESSAGE.len() @@ -59,12 +69,12 @@ impl AtatUrc for EdmEvent { || !resp.starts_with(&[STARTBYTE]) || !resp.ends_with(&[ENDBYTE]) { - defmt::error!("[Parse URC Start/End byte Error] {:?}", LossyStr(&resp)); + error!("[Parse URC Start/End byte Error] {:?}", LossyStr(resp)); return None; }; let payload_len = calc_payload_len(resp); if resp.len() != payload_len + EDM_OVERHEAD { - defmt::error!("[Parse URC lenght Error] {:?}", LossyStr(resp)); + error!("[Parse URC length Error] {:?}", LossyStr(resp)); return None; } @@ -156,7 +166,7 @@ impl AtatUrc for EdmEvent { PayloadType::StartEvent => EdmEvent::StartUp.into(), _ => { - defmt::error!("[Parse URC Error] {:?}", LossyStr(resp)); + error!("[Parse URC Error] {:?}", LossyStr(resp)); None } } @@ -166,10 +176,9 @@ impl AtatUrc for EdmEvent { #[cfg(test)] mod test { use super::*; - use crate::command::{ - data_mode::urc::PeerConnected, edm::types::DATA_PACKAGE_SIZE, PeerHandle, Urc, - }; + use crate::command::{data_mode::urc::PeerConnected, edm::types::DATA_PACKAGE_SIZE, Urc}; use atat::{heapless::Vec, heapless_bytes::Bytes, AtatUrc}; + use ublox_sockets::PeerHandle; #[test] fn parse_at_urc() { diff --git a/ublox-short-range/src/command/ethernet/mod.rs b/src/command/ethernet/mod.rs similarity index 100% rename from ublox-short-range/src/command/ethernet/mod.rs rename to src/command/ethernet/mod.rs diff --git a/ublox-short-range/src/command/ethernet/responses.rs b/src/command/ethernet/responses.rs similarity index 100% rename from ublox-short-range/src/command/ethernet/responses.rs rename to src/command/ethernet/responses.rs diff --git a/ublox-short-range/src/command/ethernet/types.rs b/src/command/ethernet/types.rs similarity index 99% rename from ublox-short-range/src/command/ethernet/types.rs rename to src/command/ethernet/types.rs index eca3ffe..8c15549 100644 --- a/ublox-short-range/src/command/ethernet/types.rs +++ b/src/command/ethernet/types.rs @@ -1,7 +1,7 @@ //! Argument and parameter types used by Ethernet Commands and Responses use atat::atat_derive::AtatEnum; -use embedded_nal::Ipv4Addr; +use no_std_net::Ipv4Addr; use crate::command::OnOff; diff --git a/ublox-short-range/src/command/ethernet/urc.rs b/src/command/ethernet/urc.rs similarity index 100% rename from ublox-short-range/src/command/ethernet/urc.rs rename to src/command/ethernet/urc.rs diff --git a/ublox-short-range/src/command/general/mod.rs b/src/command/general/mod.rs similarity index 87% rename from ublox-short-range/src/command/general/mod.rs rename to src/command/general/mod.rs index 66e1b65..08ce8d8 100644 --- a/ublox-short-range/src/command/general/mod.rs +++ b/src/command/general/mod.rs @@ -61,8 +61,8 @@ pub struct SerialNumber2; /// /// Identificationinformation. #[derive(Clone, AtatCmd)] -#[at_cmd("I0", IdentificationInfomationTypeCodeResponse, timeout_ms = 1000)] -pub struct IdentificationInfomationTypeCode; +#[at_cmd("I0", IdentificationInformationTypeCodeResponse, timeout_ms = 1000)] +pub struct IdentificationInformationTypeCode; /// 3.9 Identification information I9 /// @@ -70,17 +70,17 @@ pub struct IdentificationInfomationTypeCode; #[derive(Clone, AtatCmd)] #[at_cmd( "I9", - IdentificationInfomationSoftwareVersionResponse, + IdentificationInformationSoftwareVersionResponse, timeout_ms = 1000 )] -pub struct IdentificationInfomationSoftwareVersion; +pub struct IdentificationInformationSoftwareVersion; /// 3.9 Identification information I10 /// /// Identificationinformation. #[derive(Clone, AtatCmd)] -#[at_cmd("I10", IdentificationInfomationMCUIDResponse, timeout_ms = 1000)] -pub struct IdentificationInfomationMCUID; +#[at_cmd("I10", IdentificationInformationMCUIDResponse, timeout_ms = 1000)] +pub struct IdentificationInformationMCUID; /// 3.11 Set greeting text +CSGT /// diff --git a/ublox-short-range/src/command/general/responses.rs b/src/command/general/responses.rs similarity index 90% rename from ublox-short-range/src/command/general/responses.rs rename to src/command/general/responses.rs index aea6181..155c8d3 100644 --- a/ublox-short-range/src/command/general/responses.rs +++ b/src/command/general/responses.rs @@ -37,7 +37,7 @@ pub struct SerialNumberResponse { /// 3.10 Identification information I0 #[derive(Clone, AtatResp)] -pub struct IdentificationInfomationTypeCodeResponse { +pub struct IdentificationInformationTypeCodeResponse { /// Text string that identifies the serial number. #[at_arg(position = 0)] pub serial_number: String<64>, @@ -45,7 +45,7 @@ pub struct IdentificationInfomationTypeCodeResponse { /// 3.10 Identification information I9 #[derive(Clone, AtatResp)] -pub struct IdentificationInfomationSoftwareVersionResponse { +pub struct IdentificationInformationSoftwareVersionResponse { /// Text string that identifies the firmware version. #[at_arg(position = 0)] pub version: String<64>, @@ -53,7 +53,7 @@ pub struct IdentificationInfomationSoftwareVersionResponse { /// 3.10 Identification information I10 #[derive(Clone, AtatResp)] -pub struct IdentificationInfomationMCUIDResponse { +pub struct IdentificationInformationMCUIDResponse { /// Text string that identifies the serial number. #[at_arg(position = 0)] pub serial_number: String<64>, diff --git a/ublox-short-range/src/command/general/types.rs b/src/command/general/types.rs similarity index 98% rename from ublox-short-range/src/command/general/types.rs rename to src/command/general/types.rs index 55f927f..58f3aa2 100644 --- a/ublox-short-range/src/command/general/types.rs +++ b/src/command/general/types.rs @@ -89,7 +89,7 @@ impl core::str::FromStr for FirmwareVersion { let (patch, meta) = match patch_meta.split_once('-') { Some((patch_str, meta)) => ( patch_str.parse().map_err(|_| DeserializeError)?, - Some(heapless::String::from(meta)), + heapless::String::try_from(meta).ok(), ), None => (patch_meta.parse().map_err(|_| DeserializeError)?, None), }; @@ -103,6 +103,7 @@ impl core::str::FromStr for FirmwareVersion { } } +#[cfg(feature = "defmt")] impl defmt::Format for FirmwareVersion { fn format(&self, fmt: defmt::Formatter) { if let Some(meta) = &self.meta { diff --git a/ublox-short-range/src/command/gpio/mod.rs b/src/command/gpio/mod.rs similarity index 95% rename from ublox-short-range/src/command/gpio/mod.rs rename to src/command/gpio/mod.rs index a5d4ab5..b73c4f2 100644 --- a/ublox-short-range/src/command/gpio/mod.rs +++ b/src/command/gpio/mod.rs @@ -41,7 +41,7 @@ pub struct ReadGPIO { /// Writes the value of an enabled GPIO pin configured as output. /// Supported by ODIN-W2 from software version 3.0.0 onwards only. #[derive(Clone, AtatCmd)] -#[at_cmd("+UGPIOW", NoResponse, value_sep = false, timeout_ms = 1000)] +#[at_cmd("+UGPIOW", NoResponse, timeout_ms = 1000)] pub struct WriteGPIO { #[at_arg(position = 0)] pub id: GPIOId, diff --git a/ublox-short-range/src/command/gpio/responses.rs b/src/command/gpio/responses.rs similarity index 83% rename from ublox-short-range/src/command/gpio/responses.rs rename to src/command/gpio/responses.rs index e51e308..9a4bbac 100644 --- a/ublox-short-range/src/command/gpio/responses.rs +++ b/src/command/gpio/responses.rs @@ -6,7 +6,7 @@ use atat::atat_derive::AtatResp; #[derive(Clone, PartialEq, AtatResp)] pub struct ReadGPIOResponse { #[at_arg(position = 0)] - id: GPIOId, + pub id: GPIOId, #[at_arg(position = 1)] - value: GPIOValue, + pub value: GPIOValue, } diff --git a/ublox-short-range/src/command/gpio/types.rs b/src/command/gpio/types.rs similarity index 100% rename from ublox-short-range/src/command/gpio/types.rs rename to src/command/gpio/types.rs diff --git a/ublox-short-range/src/command/mod.rs b/src/command/mod.rs similarity index 84% rename from ublox-short-range/src/command/mod.rs rename to src/command/mod.rs index a11c14d..864dec2 100644 --- a/ublox-short-range/src/command/mod.rs +++ b/src/command/mod.rs @@ -1,8 +1,10 @@ //! AT Commands for U-Blox short range module family\ //! Following the [u-connect ATCommands Manual](https://www.u-blox.com/sites/default/files/u-connect-ATCommands-Manual_(UBX-14044127).pdf) +#[cfg(feature = "edm")] pub mod custom_digest; pub mod data_mode; +#[cfg(feature = "edm")] pub mod edm; pub mod ethernet; pub mod general; @@ -13,42 +15,26 @@ pub mod security; pub mod system; pub mod wifi; -use atat::atat_derive::{AtatCmd, AtatEnum, AtatLen, AtatResp, AtatUrc}; -use serde::{Deserialize, Serialize}; +use atat::atat_derive::{AtatCmd, AtatEnum, AtatResp, AtatUrc}; #[derive(Debug, Clone, AtatResp, PartialEq)] pub struct NoResponse; #[derive(Debug, Clone, AtatCmd)] -#[at_cmd("", NoResponse, timeout_ms = 1000)] +#[at_cmd("", NoResponse, attempts = 3, timeout_ms = 1000)] pub struct AT; -#[derive( - Debug, - Clone, - Copy, - PartialEq, - Eq, - PartialOrd, - AtatLen, - Ord, - Default, - Serialize, - Deserialize, - defmt::Format, - hash32_derive::Hash32, -)] -pub struct PeerHandle(pub u8); - #[derive(Debug, PartialEq, Clone, AtatUrc)] pub enum Urc { /// Startup Message #[at_urc("+STARTUP")] StartUp, /// 5.10 Peer connected +UUDPC + #[cfg(feature = "internal-network-stack")] #[at_urc("+UUDPC")] PeerConnected(data_mode::urc::PeerConnected), /// 5.11 Peer disconnected +UUDPD + #[cfg(feature = "internal-network-stack")] #[at_urc("+UUDPD")] PeerDisconnected(data_mode::urc::PeerDisconnected), /// 7.15 Wi-Fi Link connected +UUWLE @@ -91,6 +77,7 @@ pub enum Urc { } #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum OnOff { On = 1, @@ -105,3 +92,12 @@ impl From for OnOff { } } } + +impl From for bool { + fn from(val: OnOff) -> Self { + match val { + OnOff::On => true, + OnOff::Off => false, + } + } +} diff --git a/ublox-short-range/src/command/network/mod.rs b/src/command/network/mod.rs similarity index 97% rename from ublox-short-range/src/command/network/mod.rs rename to src/command/network/mod.rs index c80b06e..81d4c8a 100644 --- a/ublox-short-range/src/command/network/mod.rs +++ b/src/command/network/mod.rs @@ -23,7 +23,7 @@ pub struct SetNetworkHostName<'a> { /// /// Shows current status of the network interface id. #[derive(Clone, AtatCmd)] -#[at_cmd("+UNSTAT", NetworkStatusResponse, timeout_ms = 1000)] +#[at_cmd("+UNSTAT", NetworkStatusResponse, attempts = 3, timeout_ms = 1000)] pub struct GetNetworkStatus { #[at_arg(position = 0)] pub interface_id: u8, diff --git a/ublox-short-range/src/command/network/responses.rs b/src/command/network/responses.rs similarity index 100% rename from ublox-short-range/src/command/network/responses.rs rename to src/command/network/responses.rs diff --git a/ublox-short-range/src/command/network/types.rs b/src/command/network/types.rs similarity index 99% rename from ublox-short-range/src/command/network/types.rs rename to src/command/network/types.rs index d27d7cc..97c8a4a 100644 --- a/ublox-short-range/src/command/network/types.rs +++ b/src/command/network/types.rs @@ -31,7 +31,7 @@ pub enum NetworkStatus { /// 101: The is the currently used IPv4_Addr (omitted if no IP address has /// been acquired). #[at_arg(value = 101)] - IPv4Address(#[at_arg(len = 16)] Bytes<40>), + IPv4Address(#[at_arg(len = 16)] Bytes<16>), /// 102: The is the currently used subnet mask (omitted if no IP address /// has been acquired). #[at_arg(value = 102)] diff --git a/ublox-short-range/src/command/network/urc.rs b/src/command/network/urc.rs similarity index 100% rename from ublox-short-range/src/command/network/urc.rs rename to src/command/network/urc.rs diff --git a/ublox-short-range/src/command/ping/mod.rs b/src/command/ping/mod.rs similarity index 100% rename from ublox-short-range/src/command/ping/mod.rs rename to src/command/ping/mod.rs diff --git a/ublox-short-range/src/command/ping/types.rs b/src/command/ping/types.rs similarity index 94% rename from ublox-short-range/src/command/ping/types.rs rename to src/command/ping/types.rs index cfe102c..3999593 100644 --- a/ublox-short-range/src/command/ping/types.rs +++ b/src/command/ping/types.rs @@ -18,14 +18,15 @@ use atat::atat_derive::AtatEnum; /// provides the TTL value received in the incoming packet. /// - Range: 1-255 /// - Default value: 32 -// pub type TTL = (u8, Option); +// pub type TTL = (u8, Option); /// The time in milliseconds to wait after an echo reply response before sending the next /// echo request. /// - Range: 0-60000 /// - Default value: 1000 -// pub type Inteval = u16; +// pub type Interval = u16; -#[derive(Debug, PartialEq, Clone, Copy, AtatEnum, defmt::Format)] +#[derive(Debug, PartialEq, Clone, Copy, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum PingError { /// 1 - 6: Internal error (ping level) diff --git a/ublox-short-range/src/command/ping/urc.rs b/src/command/ping/urc.rs similarity index 93% rename from ublox-short-range/src/command/ping/urc.rs rename to src/command/ping/urc.rs index 28231cf..0abc5c7 100644 --- a/ublox-short-range/src/command/ping/urc.rs +++ b/src/command/ping/urc.rs @@ -1,8 +1,8 @@ //! Responses for Ping Commands use super::types::*; use atat::atat_derive::AtatResp; -use embedded_nal::IpAddr; use heapless::String; +use no_std_net::IpAddr; /// 16.1 Ping command +UPING /// /// The ping command is the common method to know if a remote host is reachable on the Internet. @@ -24,7 +24,6 @@ use heapless::String; /// in another way. #[derive(Debug, PartialEq, Clone, AtatResp)] pub struct PingResponse { - /// Text string that identifies the serial number. #[at_arg(position = 0)] pub retrynum: u32, #[at_arg(position = 1)] @@ -39,7 +38,8 @@ pub struct PingResponse { pub rtt: i32, } -#[derive(Debug, PartialEq, Clone, AtatResp, defmt::Format)] +#[derive(Debug, PartialEq, Clone, AtatResp)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct PingErrorResponse { #[at_arg(position = 0)] pub error: PingError, diff --git a/ublox-short-range/src/command/security/mod.rs b/src/command/security/mod.rs similarity index 99% rename from ublox-short-range/src/command/security/mod.rs rename to src/command/security/mod.rs index 388057a..81fd730 100644 --- a/ublox-short-range/src/command/security/mod.rs +++ b/src/command/security/mod.rs @@ -41,7 +41,7 @@ pub struct PrepareSecurityDataImport<'a> { "", SecurityDataImport, value_sep = false, - timeout_ms = 1000, + timeout_ms = 3000, cmd_prefix = "", termination = "" )] diff --git a/ublox-short-range/src/command/security/responses.rs b/src/command/security/responses.rs similarity index 100% rename from ublox-short-range/src/command/security/responses.rs rename to src/command/security/responses.rs diff --git a/ublox-short-range/src/command/security/types.rs b/src/command/security/types.rs similarity index 100% rename from ublox-short-range/src/command/security/types.rs rename to src/command/security/types.rs diff --git a/ublox-short-range/src/command/system/mod.rs b/src/command/system/mod.rs similarity index 95% rename from ublox-short-range/src/command/system/mod.rs rename to src/command/system/mod.rs index ec1ac13..9ff5be2 100644 --- a/ublox-short-range/src/command/system/mod.rs +++ b/src/command/system/mod.rs @@ -29,7 +29,7 @@ pub struct SetToDefaultConfig; /// Reset to factory defined defaults. A reboot is required before using the new settings. #[derive(Debug, PartialEq, Clone, AtatCmd)] #[at_cmd("+UFACTORY", NoResponse, timeout_ms = 1000)] -pub struct ResetToFacroryDefaults; +pub struct ResetToFactoryDefaults; /// 4.4 Circuit 108/2 (DTR) behavior &D /// @@ -175,7 +175,7 @@ pub struct ModuleStart { #[at_cmd("+UMLA", NoResponse, timeout_ms = 1000)] pub struct SetLocalAddress<'a> { #[at_arg(position = 0)] - pub interface_id: InserfaceID, + pub interface_id: InterfaceID, /// MAC address of the interface id. If the address is set to 000000000000, the local /// address will be restored to factory-programmed value. /// The least significant bit of the first octet of the

must be 0; that is, the @@ -188,10 +188,10 @@ pub struct SetLocalAddress<'a> { /// /// Reads the local address of the interface id. #[derive(Debug, PartialEq, Clone, AtatCmd)] -#[at_cmd("+UMSM", LocalAddressResponse, timeout_ms = 1000)] +#[at_cmd("+UMLA", LocalAddressResponse, timeout_ms = 1000)] pub struct GetLocalAddress { #[at_arg(position = 0)] - pub interface_id: InserfaceID, + pub interface_id: InterfaceID, } /// 4.15 System status +UMSTAT @@ -215,14 +215,6 @@ pub struct SystemStatus { pub struct SetRS232Settings { #[at_arg(position = 0)] pub baud_rate: BaudRate, - // #[at_arg(position = 1)] - // pub settings: Option<( - // FlowControl, - // Option<( - // u8, - // Option<(StopBits, Option<(Parity, Option)>)>, - // )>, - // )>, #[at_arg(position = 1)] pub flow_control: FlowControl, #[at_arg(position = 2)] diff --git a/ublox-short-range/src/command/system/responses.rs b/src/command/system/responses.rs similarity index 92% rename from ublox-short-range/src/command/system/responses.rs rename to src/command/system/responses.rs index da05fd9..fcd5de8 100644 --- a/ublox-short-range/src/command/system/responses.rs +++ b/src/command/system/responses.rs @@ -1,6 +1,6 @@ //! Responses for System Commands use super::types::*; -use atat::atat_derive::AtatResp; +use atat::{atat_derive::AtatResp, serde_at::HexStr}; use heapless::String; /// 4.11 Software update +UFWUPD @@ -17,7 +17,7 @@ pub struct LocalAddressResponse { /// MAC address of the interface id. If the address is set to 000000000000, the local /// address will be restored to factory-programmed value. #[at_arg(position = 0)] - pub mac: String<64>, + pub mac: HexStr, } /// 4.15 System status +UMSTAT diff --git a/ublox-short-range/src/command/system/types.rs b/src/command/system/types.rs similarity index 96% rename from ublox-short-range/src/command/system/types.rs rename to src/command/system/types.rs index 9d4b17d..b00b2a7 100644 --- a/ublox-short-range/src/command/system/types.rs +++ b/src/command/system/types.rs @@ -40,7 +40,7 @@ pub enum DSRAssertMode { /// DSR line when no remote peers are connected. See Connect Peer +UDCP and Default /// remote peer +UDDRP for definition of the remote peer. This applies to both incoming /// and outgoing connections. - WhenPeersConected = 2, + WhenPeersConnected = 2, } /// Echo on @@ -81,10 +81,11 @@ pub enum ModuleStartMode { #[derive(Debug, Clone, PartialEq, AtatEnum)] #[repr(u8)] -pub enum InserfaceID { - Bluetooth = 0, - WiFi = 1, - Ethernet = 2, +pub enum InterfaceID { + Bluetooth = 1, + WiFi = 2, + Ethernet = 3, + WiFiAP = 4, } #[derive(Debug, Clone, PartialEq, AtatEnum)] @@ -98,7 +99,7 @@ pub enum StatusID { SavedStatus = 1, } -#[derive(Debug, Clone, PartialEq, AtatEnum)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, AtatEnum)] #[at_enum(u32)] /// ODIN-W2: /// 19200 - 5250000. The module will set a baud rate as close as possible to the diff --git a/ublox-short-range/src/command/wifi/mod.rs b/src/command/wifi/mod.rs similarity index 97% rename from ublox-short-range/src/command/wifi/mod.rs rename to src/command/wifi/mod.rs index f0c9223..9dd319a 100644 --- a/ublox-short-range/src/command/wifi/mod.rs +++ b/src/command/wifi/mod.rs @@ -34,14 +34,29 @@ impl<'a> atat::AtatLen for SetWifiStationConfig<'a> { const ATAT_SETWIFISTATIONCONFIG_LEN: usize = as atat::AtatLen>::LEN + ::LEN + 1usize; #[automatically_derived] -impl<'a> atat::AtatCmd<{ ATAT_SETWIFISTATIONCONFIG_LEN + 12usize }> for SetWifiStationConfig<'a> { +impl<'a> atat::AtatCmd for SetWifiStationConfig<'a> { type Response = NoResponse; const MAX_TIMEOUT_MS: u32 = 1000u32; #[inline] - fn as_bytes(&self) -> atat::heapless::Vec { - match atat::serde_at::to_vec( + fn parse( + &self, + res: Result<&[u8], atat::InternalError>, + ) -> core::result::Result { + match res { + Ok(resp) => { + atat::serde_at::from_slice::(resp).map_err(|_e| atat::Error::Parse) + } + Err(e) => Err(e.into()), + } + } + + const MAX_LEN: usize = ATAT_SETWIFISTATIONCONFIG_LEN + 12usize; + + fn write(&self, buf: &mut [u8]) -> usize { + match atat::serde_at::to_slice( self, "+UWSC", + buf, atat::serde_at::SerializeOptions { value_sep: true, cmd_prefix: "AT", @@ -53,18 +68,6 @@ impl<'a> atat::AtatCmd<{ ATAT_SETWIFISTATIONCONFIG_LEN + 12usize }> for SetWifiS Err(_) => panic!("Failed to serialize command"), } } - #[inline] - fn parse( - &self, - res: Result<&[u8], atat::InternalError>, - ) -> core::result::Result { - match res { - Ok(resp) => { - atat::serde_at::from_slice::(resp).map_err(|e| atat::Error::Parse) - } - Err(e) => Err(e.into()), - } - } } #[automatically_derived] impl<'a> atat::serde_at::serde::Serialize for SetWifiStationConfig<'a> { diff --git a/ublox-short-range/src/command/wifi/responses.rs b/src/command/wifi/responses.rs similarity index 100% rename from ublox-short-range/src/command/wifi/responses.rs rename to src/command/wifi/responses.rs diff --git a/ublox-short-range/src/command/wifi/types.rs b/src/command/wifi/types.rs similarity index 97% rename from ublox-short-range/src/command/wifi/types.rs rename to src/command/wifi/types.rs index 62e6566..25097a5 100644 --- a/ublox-short-range/src/command/wifi/types.rs +++ b/src/command/wifi/types.rs @@ -3,8 +3,8 @@ use crate::command::OnOff; use atat::atat_derive::AtatEnum; use atat::heapless_bytes::Bytes; -use embedded_nal::{Ipv4Addr, Ipv6Addr}; use heapless::{String, Vec}; +use no_std_net::{Ipv4Addr, Ipv6Addr}; use serde::Deserialize; #[derive(Clone, PartialEq, AtatEnum)] @@ -106,7 +106,7 @@ pub enum WifiStationConfigParameter { /// is the Wi-Fi beacon listen interval in units of beacon /// interval. The factory default value is 0, listen on all beacons. /// - Valid values 0-16 - WiFiBeaconListenInteval = 300, + WiFiBeaconListenInterval = 300, /// Enables DTIM in power save. If the DTIM is enabled and the /// module is in power save, the access point sends an indication when new /// data is available. If disabled, the module polls for data every beacon @@ -244,7 +244,7 @@ pub enum WifiStationConfig<'a> { /// interval. The factory default value is 0, listen on all beacons. /// - Valid values 0-16 #[at_arg(value = 300)] - WiFiBeaconListenInteval(u8), + WiFiBeaconListenInterval(u8), /// Enables DTIM in power save. If the DTIM is enabled and the /// module is in power save, the access point sends an indication when new /// data is available. If disabled, the module polls for data every beacon @@ -384,7 +384,7 @@ pub enum WifiStationConfigR { /// interval. The factory default value is 0, listen on all beacons. /// - Valid values 0-16 #[at_arg(value = 300)] - WiFiBeaconListenInteval(u8), + WiFiBeaconListenInterval(u8), /// Enables DTIM in power save. If the DTIM is enabled and the /// module is in power save, the access point sends an indication when new /// data is available. If disabled, the module polls for data every beacon @@ -426,7 +426,8 @@ pub enum WifiStationAction { Deactivate = 4, } -#[derive(Debug, Clone, PartialEq, AtatEnum, defmt::Format)] +#[derive(Debug, Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum OperationMode { Infrastructure = 1, @@ -434,6 +435,7 @@ pub enum OperationMode { } #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum StatusId { SSID = 0, @@ -447,7 +449,7 @@ pub enum StatusId { Status = 3, /// The is the RSSI value of the current connection; will /// return-32768, if not connected. - RSSI = 6, + Rssi = 6, /// The is the mobility domain of the last or current /// connection This tag is supported by ODIN-W2 from software version 6.0.0 /// onwards only. @@ -497,7 +499,7 @@ pub enum WifiStatus { /// The is the RSSI value of the current connection; will /// return-32768, if not connected. #[at_arg(value = 6)] - RSSI(i16), + Rssi(u32), /// The is the mobility domain of the last or current /// connection This tag is supported by ODIN-W2 from software version 6.0.0 /// onwards only. @@ -594,17 +596,35 @@ pub enum WifiConfigParameter { /// onwards RemainOnChannel = 15, /// Station TX rates bit mask where bit masks are defined according to: - /// 0x00000001: Rate 1 Mbps 0x00000002: Rate 2 Mbps 0x00000004: Rate 5.5 - /// Mbps 0x00000008: Rate 11 Mbps 0x00000010: Rate 6 Mbps 0x00000020: Rate 9 - /// Mbps 0x00000040: Rate 12 Mbps 0x00000080: Rate 18 Mbps 0x00000100: Rate - /// 24 Mbps 0x00000200: Rate 36 Mbps 0x00000400: Rate 48 Mbps 0x00000800: - /// Rate 54 Mbps 0x00001000: Rate MCS 0 0x00002000: Rate MCS 1 0x00004000: - /// Rate MCS 2 0x00008000: Rate MCS 3 0x00010000: Rate MCS 4 0x00020000: - /// Rate MCS 5 0x00040000: Rate MCS 6 0x00080000: Rate MCS 7 0x00100000: - /// Rate MCS 8 0x00200000: Rate MCS 9 0x00400000: Rate MCS 10 0x00800000: - /// Rate MCS 11 0x01000000: Rate MCS 12 0x02000000: Rate MCS 13 0x04000000: - /// Rate MCS 14 0x08000000: Rate MCS 15 Default value is 0, which means that - /// all rates are enabled. Supported software versions 7.0.0 onwards + /// - 0x00000001: Rate 1 Mbps + /// - 0x00000002: Rate 2 Mbps + /// - 0x00000004: Rate 5.5 Mbps + /// - 0x00000008: Rate 11 Mbps + /// - 0x00000010: Rate 6 Mbps + /// - 0x00000020: Rate 9 Mbps + /// - 0x00000040: Rate 12 Mbps + /// - 0x00000080: Rate 18 Mbps + /// - 0x00000100: Rate 24 Mbps + /// - 0x00000200: Rate 36 Mbps + /// - 0x00000400: Rate 48 Mbps + /// - 0x00000800: Rate 54 Mbps + /// - 0x00001000: Rate MCS 0 + /// - 0x00002000: Rate MCS 1 + /// - 0x00004000: Rate MCS 2 + /// - 0x00008000: Rate MCS 3 + /// - 0x00010000: Rate MCS 4 + /// - 0x00020000: Rate MCS 5 + /// - 0x00040000: Rate MCS 6 + /// - 0x00080000: Rate MCS 7 + /// - 0x00100000: Rate MCS 8 + /// - 0x00200000: Rate MCS 9 + /// - 0x00400000: Rate MCS 10 + /// - 0x00800000: Rate MCS 11 + /// - 0x01000000: Rate MCS 12 + /// - 0x02000000: Rate MCS 13 + /// - 0x04000000: Rate MCS 14 + /// - 0x08000000: Rate MCS 15 Default value is 0, which means that all rates + /// are enabled. Supported software versions 7.0.0 onwards StationTxRates = 16, /// Station short packet retry limit. Default value is 0x00141414. The /// definition of retry limits are listed below: diff --git a/ublox-short-range/src/command/wifi/urc.rs b/src/command/wifi/urc.rs similarity index 100% rename from ublox-short-range/src/command/wifi/urc.rs rename to src/command/wifi/urc.rs diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..5380f32 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,31 @@ +use embedded_hal::digital::OutputPin; +use embedded_io_async::{Read, Write}; + +use crate::{command::system::types::BaudRate, DEFAULT_BAUD_RATE}; + +pub trait WifiConfig<'a> { + type ResetPin: OutputPin; + + 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 TLS_IN_BUFFER_SIZE: Option = None; + #[cfg(feature = "internal-network-stack")] + const TLS_OUT_BUFFER_SIZE: Option = None; + + #[cfg(feature = "ppp")] + const PPP_CONFIG: embassy_net_ppp::Config<'a>; + + fn reset_pin(&mut self) -> Option<&mut Self::ResetPin> { + None + } +} + +pub trait Transport: Write + Read { + fn set_baudrate(&mut self, baudrate: u32); + fn split_ref(&mut self) -> (impl Write, impl Read); +} diff --git a/src/connection.rs b/src/connection.rs new file mode 100644 index 0000000..b5f7292 --- /dev/null +++ b/src/connection.rs @@ -0,0 +1,87 @@ +use no_std_net::Ipv4Addr; + +use crate::network::{WifiMode, WifiNetwork}; + +#[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum WiFiState { + Inactive, + /// Searching for Wifi + NotConnected, + SecurityProblems, + Connected, +} + +/// Static IP address configuration. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StaticConfigV4 { + /// IP address and subnet mask. + pub address: Ipv4Addr, + /// Default gateway. + pub gateway: Option, + /// DNS servers. + pub dns_servers: DnsServers, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DnsServers { + pub primary: Option, + pub secondary: Option, +} + +pub struct WifiConnection { + pub wifi_state: WiFiState, + pub ipv6_link_local_up: bool, + pub ipv4_up: bool, + #[cfg(feature = "ipv6")] + pub ipv6_up: bool, + pub network: Option, +} + +impl WifiConnection { + pub(crate) const fn new() -> Self { + WifiConnection { + wifi_state: WiFiState::Inactive, + ipv6_link_local_up: false, + network: None, + ipv4_up: false, + #[cfg(feature = "ipv6")] + ipv6_up: false, + } + } + + #[allow(dead_code)] + pub fn is_station(&self) -> bool { + self.network + .as_ref() + .map(|n| n.mode == WifiMode::Station) + .unwrap_or_default() + } + + #[allow(dead_code)] + pub fn is_access_point(&self) -> bool { + !self.is_station() + } + + /// Get whether the network stack has a valid IP configuration. + /// This is true if the network stack has a static IP configuration or if DHCP has completed + pub fn is_config_up(&self) -> bool { + let v6_up; + let v4_up = self.ipv4_up; + + #[cfg(feature = "ipv6")] + { + v6_up = self.ipv6_up; + } + #[cfg(not(feature = "ipv6"))] + { + v6_up = false; + } + + (v4_up || v6_up) && self.ipv6_link_local_up + } + + pub fn is_connected(&self) -> bool { + self.is_config_up() && self.wifi_state == WiFiState::Connected + } +} diff --git a/ublox-short-range/src/error.rs b/src/error.rs similarity index 79% rename from ublox-short-range/src/error.rs rename to src/error.rs index ea0b24a..fd38a1b 100644 --- a/ublox-short-range/src/error.rs +++ b/src/error.rs @@ -1,10 +1,13 @@ +#[cfg(feature = "internal-network-stack")] pub use ublox_sockets::Error as SocketError; -#[derive(Debug, defmt::Format)] +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum Error { Overflow, SetState, BadLength, + SecurityProblems, Network, Pin, BaudDetection, @@ -13,9 +16,10 @@ pub enum Error { SocketNotFound, SocketNotConnected, MissingSocketSet, - NetworkState(crate::wifi::connection::NetworkState), + // NetworkState(crate::wifi::connection::NetworkState), NoWifiSetup, - WifiState(crate::wifi::connection::WiFiState), + // WifiState(crate::wifi::connection::WiFiState), + #[cfg(feature = "internal-network-stack")] Socket(ublox_sockets::Error), AT(atat::Error), Busy, @@ -26,8 +30,10 @@ pub enum Error { Unimplemented, SocketMemory, SocketMapMemory, + Supplicant, Timeout, ShadowStoreBug, + AlreadyConnected, _Unknown, } @@ -37,6 +43,13 @@ impl From for Error { } } +impl From for Error { + fn from(_: embassy_time::TimeoutError) -> Self { + Error::Timeout + } +} + +#[cfg(feature = "internal-network-stack")] impl From for Error { fn from(e: ublox_sockets::Error) -> Self { Error::Socket(e) @@ -44,7 +57,8 @@ impl From for Error { } /// Error that occurs when attempting to connect to a wireless network. -#[derive(Debug, defmt::Format)] +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum WifiConnectionError { /// Failed to connect to wireless network. FailedToConnect, @@ -70,7 +84,8 @@ impl From for WifiConnectionError { } } -#[derive(Debug, defmt::Format)] +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum WifiError { // The specified wifi is currently disabled. Try switching it on. WifiDisabled, @@ -86,9 +101,10 @@ pub enum WifiError { // Other, } -#[derive(Debug, defmt::Format)] +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum WifiHotspotError { - /// Failed to ceate wireless hotspot. + /// Failed to create wireless hotspot. CreationFailed, /// Failed to stop wireless hotspot service. Try turning off /// the wireless interface via ```wifi.turn_off()```. diff --git a/src/fmt.rs b/src/fmt.rs new file mode 100644 index 0000000..35b929f --- /dev/null +++ b/src/fmt.rs @@ -0,0 +1,274 @@ +#![macro_use] +#![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)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! assert_eq { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert_eq!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert_eq!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! assert_ne { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert_ne!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert_ne!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug_assert { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug_assert_eq { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert_eq!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert_eq!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug_assert_ne { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert_ne!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert_ne!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! todo { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::todo!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::todo!($($x)*); + } + }; +} + +#[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] +macro_rules! unreachable { + ($($x:tt)*) => { + ::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)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::panic!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::panic!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! trace { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::trace!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::trace!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::debug!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::debug!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! info { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::info!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::info!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! warn { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::warn!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::warn!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! error { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::error!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::error!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] +macro_rules! unwrap { + ($($x:tt)*) => { + ::defmt::unwrap!($($x)*) + }; +} + +#[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] +macro_rules! unwrap { + ($arg:expr) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {:?}", ::core::stringify!($arg), e); + } + } + }; + ($arg:expr, $($msg:expr),+ $(,)? ) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {}: {:?}", ::core::stringify!($arg), ::core::format_args!($($msg,)*), e); + } + } + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct NoneError; + +pub trait Try { + type Ok; + type Error; + fn into_result(self) -> Result; +} + +impl Try for Option { + type Ok = T; + type Error = NoneError; + + #[inline] + fn into_result(self) -> Result { + self.ok_or(NoneError) + } +} + +impl Try for Result { + type Ok = T; + type Error = E; + + #[inline] + fn into_result(self) -> Self { + 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/ublox-short-range/src/hex.rs b/src/hex.rs similarity index 100% rename from ublox-short-range/src/hex.rs rename to src/hex.rs diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..283875a --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,34 @@ +#![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."); + +#[cfg(not(any( + feature = "odin-w2xx", + feature = "nina-w1xx", + feature = "nina-b1xx", + feature = "anna-b1xx", + feature = "nina-b2xx", + feature = "nina-b3xx" +)))] +compile_error!("No module feature activated. You must activate exactly one of the following features: odin-w2xx, nina-w1xx, nina-b1xx, anna-b1xx, nina-b2xx, nina-b3xx"); + +mod fmt; + +pub mod asynch; + +mod config; +mod connection; +mod network; + +mod hex; + +pub use atat; + +pub mod command; +pub mod error; +pub use config::{Transport, WifiConfig}; + +use command::system::types::BaudRate; +pub const DEFAULT_BAUD_RATE: BaudRate = BaudRate::B115200; diff --git a/ublox-short-range/src/wifi/network.rs b/src/network.rs similarity index 67% rename from ublox-short-range/src/wifi/network.rs rename to src/network.rs index 0ecb82f..0d9204c 100644 --- a/ublox-short-range/src/wifi/network.rs +++ b/src/network.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + use crate::command::wifi::types::{OperationMode, ScannedWifiNetwork}; use crate::error::WifiError; use crate::hex::from_hex; @@ -6,15 +8,16 @@ use heapless::String; use core::convert::TryFrom; -#[derive(PartialEq, Debug, defmt::Format)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum WifiMode { Station, AccessPoint, } -#[derive(Debug, defmt::Format)] +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct WifiNetwork { - #[defmt(Debug2Format)] + #[cfg_attr(feature = "defmt", defmt(Debug2Format))] pub bssid: Bytes<20>, pub op_mode: OperationMode, pub ssid: String<64>, @@ -26,6 +29,22 @@ pub struct WifiNetwork { pub mode: WifiMode, } +impl WifiNetwork { + pub fn new_station(bssid: Bytes<20>, channel: u8) -> Self { + Self { + bssid, + op_mode: OperationMode::Infrastructure, + ssid: String::new(), + channel, + rssi: 1, + authentication_suites: 0, + unicast_ciphers: 0, + group_ciphers: 0, + mode: WifiMode::Station, + } + } +} + impl TryFrom for WifiNetwork { type Error = WifiError; diff --git a/ublox-short-range/Cargo.toml b/ublox-short-range/Cargo.toml deleted file mode 100644 index 3097326..0000000 --- a/ublox-short-range/Cargo.toml +++ /dev/null @@ -1,56 +0,0 @@ -[package] -name = "ublox-short-range-rs" -version = "0.1.1" -authors = ["Mads Andresen "] -description = "Driver crate for u-blox short range devices, implementation follows 'UBX-14044127 - R40'" -readme = "../README.md" -keywords = ["ublox", "wifi", "shortrange", "bluetooth"] -categories = ["embedded", "no-std"] -license = "MIT OR Apache-2.0" -repository = "https://github.com/BlackbirdHQ/ublox-short-range-rs" -edition = "2021" - -[lib] -name = "ublox_short_range" -doctest = false - -[dependencies] -# atat = { version = "0.18.0", features = ["derive", "defmt", "bytes"] } -atat = { git = "https://github.com/BlackbirdHQ/atat", rev = "c5caaf7", features = ["derive", "defmt", "bytes"] } -heapless = { version = "^0.7", features = ["serde", "defmt-impl"] } -no-std-net = { version = "^0.5", features = ["serde"] } -serde = { version = "^1", default-features = false, features = ["derive"] } -# ublox-sockets = { version = "0.5", features = ["defmt"] } -ublox-sockets = { git = "https://github.com/BlackbirdHQ/ublox-sockets", rev = "b1ff942", features = ["defmt"] } - -hash32 = "^0.2.1" -hash32-derive = "^0.1.0" - -defmt = { version = "0.3" } -embedded-hal = "=1.0.0-rc.1" -embedded-io = "0.5" -embedded-nal = "0.6.0" - -embassy-time = "0.1" - -[dev-dependencies] -embedded-io = "0.4" - -[features] -default = ["odin_w2xx", "wifi_ap", "wifi_sta", "socket-udp", "socket-tcp"] - -async = ["atat/async"] - -odin_w2xx = [] -nina_w1xx = [] -nina_b1xx = [] -anna_b1xx = [] -nina_b2xx = [] -nina_b3xx = [] - -socket-tcp = ["ublox-sockets/socket-tcp"] -socket-udp = ["ublox-sockets/socket-udp"] - -wifi_ap = [] -wifi_sta = [] -bluetooth = [] diff --git a/ublox-short-range/src/blocking_timer.rs b/ublox-short-range/src/blocking_timer.rs deleted file mode 100644 index 15be27b..0000000 --- a/ublox-short-range/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-short-range/src/client.rs b/ublox-short-range/src/client.rs deleted file mode 100644 index 77ea337..0000000 --- a/ublox-short-range/src/client.rs +++ /dev/null @@ -1,925 +0,0 @@ -use core::str::FromStr; - -use crate::{ - blocking_timer::BlockingTimer, - command::{ - custom_digest::EdmDigester, - data_mode::{ - types::{IPProtocol, PeerConfigParameter}, - SetPeerConfiguration, - }, - edm::{types::Protocol, urc::EdmEvent, EdmAtCmdWrapper, SwitchToEdmCommand}, - general::{types::FirmwareVersion, SoftwareVersion}, - network::SetNetworkHostName, - ping::types::PingError, - system::{ - types::{BaudRate, ChangeAfterConfirm, FlowControl, Parity, StopBits}, - RebootDCE, SetRS232Settings, StoreCurrentConfig, - }, - wifi::{ - responses::WifiStatusResponse, - types::{DisconnectReason, StatusId, WifiConfig, WifiStatus}, - GetWifiStatus, SetWifiConfig, - }, - Urc, - }, - config::Config, - error::Error, - wifi::{ - connection::{NetworkState, WiFiState, WifiConnection}, - network::{WifiMode, WifiNetwork}, - SocketMap, - }, - UbloxWifiBuffers, UbloxWifiIngress, UbloxWifiUrcChannel, -}; -use atat::{blocking::AtatClient, heapless_bytes::Bytes, AtatUrcChannel, UrcSubscription}; -use defmt::{debug, error, trace}; -use embassy_time::{Duration, Instant}; -use embedded_hal::digital::OutputPin; -use embedded_nal::{IpAddr, Ipv4Addr, SocketAddr}; -use ublox_sockets::{ - udp_listener::UdpListener, AnySocket, SocketHandle, SocketSet, SocketType, TcpSocket, TcpState, - UdpSocket, UdpState, -}; - -#[derive(PartialEq, Eq, Copy, Clone)] -pub enum SerialMode { - Cmd, - ExtendedData, -} - -#[derive(PartialEq, Copy, Clone)] -pub enum DNSState { - Resolving, - Resolved(IpAddr), - Error(PingError), -} - -/// From u-connectXpress AT commands manual: -/// depends on the . For internet domain names, the maximum -/// length is 64 characters. -/// Domain name length is 128 for NINA-W13 and NINA-W15 software version 4.0 -/// .0 or later. -#[cfg(not(feature = "nina_w1xx"))] -pub const MAX_DOMAIN_NAME_LENGTH: usize = 64; - -#[cfg(feature = "nina_w1xx")] -pub const MAX_DOMAIN_NAME_LENGTH: usize = 128; - -pub struct DNSTableEntry { - domain_name: heapless::String, - state: DNSState, -} - -impl DNSTableEntry { - pub const fn new( - state: DNSState, - domain_name: heapless::String, - ) -> Self { - Self { domain_name, state } - } -} - -pub struct DNSTable { - pub table: heapless::Deque, -} - -impl DNSTable { - const fn new() -> Self { - Self { - table: heapless::Deque::new(), - } - } - pub fn upsert(&mut self, new_entry: DNSTableEntry) { - if let Some(entry) = self - .table - .iter_mut() - .find(|e| e.domain_name == new_entry.domain_name) - { - entry.state = new_entry.state; - return; - } - - if self.table.is_full() { - self.table.pop_front(); - } - unsafe { - self.table.push_back_unchecked(new_entry); - } - } - - pub fn get_state( - &self, - domain_name: heapless::String, - ) -> Option { - self.table - .iter() - .find(|e| e.domain_name == domain_name) - .map(|x| x.state) - } - pub fn reverse_lookup(&self, ip: IpAddr) -> Option<&heapless::String> { - match self - .table - .iter() - .find(|e| e.state == DNSState::Resolved(ip)) - { - Some(entry) => Some(&entry.domain_name), - None => None, - } - } -} - -#[derive(PartialEq, Eq, Clone, Default)] -pub struct SecurityCredentials { - pub ca_cert_name: Option>, - pub c_cert_name: Option>, // TODO: Make &str with lifetime - pub c_key_name: Option>, -} - -/// Creates new socket numbers -/// Properly not Async safe -pub fn new_socket_num(sockets: &SocketSet) -> Result { - let mut num = 0; - while sockets.socket_type(SocketHandle(num)).is_some() { - num += 1; - if num == u8::MAX { - return Err(()); - } - } - Ok(num) -} - -pub(crate) const URC_CAPACITY: usize = 2; -pub(crate) const URC_SUBSCRIBERS: usize = 1; - -pub struct UbloxClient<'buf, 'sub, AtCl, AtUrcCh, RST, const N: usize, const L: usize> -where - RST: OutputPin, -{ - pub(crate) module_started: bool, - pub(crate) initialized: bool, - serial_mode: SerialMode, - pub(crate) dns_table: DNSTable, - pub(crate) wifi_connection: Option, - pub(crate) wifi_config_active_on_startup: Option, - pub(crate) client: AtCl, - pub(crate) config: Config, - pub(crate) sockets: Option<&'static mut SocketSet>, - pub(crate) security_credentials: SecurityCredentials, - pub(crate) socket_map: SocketMap, - pub(crate) udp_listener: UdpListener<2, N>, - urc_subscription: UrcSubscription<'sub, EdmEvent, URC_CAPACITY, URC_SUBSCRIBERS>, - _urc_channel: &'buf AtUrcCh, -} - -impl<'buf, 'sub, W, RST, const INGRESS_BUF_SIZE: usize, const N: usize, const L: usize> - UbloxClient< - 'buf, - 'sub, - atat::blocking::Client<'buf, W, INGRESS_BUF_SIZE>, - UbloxWifiUrcChannel, - RST, - N, - L, - > -where - 'buf: 'sub, - W: embedded_io::Write, - RST: OutputPin, -{ - /// Create new u-blox device - /// - /// Look for [`data_service`](Device::data_service) how to handle data connection automatically. - /// - pub fn from_buffers( - buffers: &'buf UbloxWifiBuffers, - tx: W, - config: Config, - ) -> (UbloxWifiIngress, Self) { - let (ingress, client) = - buffers.split_blocking(tx, EdmDigester::default(), atat::Config::default()); - - ( - ingress, - UbloxClient::new(client, &buffers.urc_channel, config), - ) - } -} - -impl<'buf, 'sub, AtCl, AtUrcCh, RST, const N: usize, const L: usize> - UbloxClient<'buf, 'sub, AtCl, AtUrcCh, RST, N, L> -where - 'buf: 'sub, - AtCl: AtatClient, - AtUrcCh: AtatUrcChannel, - RST: OutputPin, -{ - pub fn new(client: AtCl, _urc_channel: &'buf AtUrcCh, config: Config) -> Self { - let urc_subscription = _urc_channel.subscribe().unwrap(); - UbloxClient { - module_started: false, - initialized: false, - serial_mode: SerialMode::Cmd, - dns_table: DNSTable::new(), - wifi_connection: None, - wifi_config_active_on_startup: None, - client, - config, - sockets: None, - security_credentials: SecurityCredentials::default(), - socket_map: SocketMap::default(), - udp_listener: UdpListener::new(), - urc_subscription, - _urc_channel, - } - } -} - -impl<'buf, 'sub, AtCl, AtUrcCh, RST, const N: usize, const L: usize> - UbloxClient<'buf, 'sub, AtCl, AtUrcCh, RST, N, L> -where - 'buf: 'sub, - AtCl: AtatClient, - RST: OutputPin, -{ - 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 has_socket_storage(&self) -> bool { - self.sockets.is_some() - } - - pub fn init(&mut self) -> Result<(), Error> { - // Initilize a new ublox device to a known state (set RS232 settings) - - debug!("Initializing wifi"); - // Hard reset module - self.reset()?; - - // Switch to EDM on Init. If in EDM, fail and check with autosense - // if self.serial_mode != SerialMode::ExtendedData { - // self.retry_send(&SwitchToEdmCommand, 5)?; - // self.serial_mode = SerialMode::ExtendedData; - // } - - while self.serial_mode != SerialMode::ExtendedData { - self.send_internal(&SwitchToEdmCommand, true).ok(); - BlockingTimer::after(Duration::from_millis(100)).wait(); - self.handle_urc()?; - } - - // TODO: handle EDM settings quirk see EDM datasheet: 2.2.5.1 AT Request Serial settings - self.retry_send( - &EdmAtCmdWrapper(SetRS232Settings { - baud_rate: BaudRate::B115200, - flow_control: FlowControl::On, - data_bits: 8, - stop_bits: StopBits::One, - parity: Parity::None, - change_after_confirm: ChangeAfterConfirm::ChangeAfterOK, - }), - 5, - )?; - - if let Some(hostname) = self.config.hostname.clone() { - self.send_internal( - &EdmAtCmdWrapper(SetNetworkHostName { - host_name: hostname.as_str(), - }), - false, - )?; - } - - self.send_internal( - &EdmAtCmdWrapper(SetWifiConfig { - config_param: WifiConfig::RemainOnChannel(0), - }), - false, - )?; - - self.send_internal(&EdmAtCmdWrapper(StoreCurrentConfig), false)?; - - self.software_reset()?; - - while self.serial_mode != SerialMode::ExtendedData { - self.send_internal(&SwitchToEdmCommand, true).ok(); - BlockingTimer::after(Duration::from_millis(100)).wait(); - self.handle_urc()?; - } - - let response = self.send_internal(&EdmAtCmdWrapper(SoftwareVersion), true)?; - if response.version < FirmwareVersion::new(8, 0, 0) { - self.config.network_up_bug = true; - } else { - if let Some(size) = self.config.tls_in_buffer_size { - self.send_internal( - &EdmAtCmdWrapper(SetPeerConfiguration { - parameter: PeerConfigParameter::TlsInBuffer(size), - }), - false, - )?; - } - - if let Some(size) = self.config.tls_out_buffer_size { - self.send_internal( - &EdmAtCmdWrapper(SetPeerConfiguration { - parameter: PeerConfigParameter::TlsOutBuffer(size), - }), - false, - )?; - } - } - - self.load_wifi_config(); - - self.initialized = true; - - Ok(()) - } - - pub fn firmware_version(&mut self) -> Result { - let response = self.send_at(SoftwareVersion)?; - Ok(response.version) - } - - pub fn signal_strength(&mut self) -> Result { - if let WifiStatusResponse { - status_id: WifiStatus::RSSI(rssi), - } = self.send_at(GetWifiStatus { - status_id: StatusId::RSSI, - })? { - Ok(rssi) - } else { - Err(Error::_Unknown) - } - } - - fn load_wifi_config(&mut self) { - //Check if we have active network config if so then update wifi_connection - if let Ok(active) = self.is_active_on_startup() { - if active { - if let Ok(ssid) = self.get_ssid() { - defmt::info!("Found network in module configuring as active network"); - self.wifi_connection.replace(WifiConnection::new( - WifiNetwork { - bssid: Bytes::new(), - op_mode: crate::command::wifi::types::OperationMode::Infrastructure, - ssid: ssid, - channel: 0, - rssi: 1, - authentication_suites: 0, - unicast_ciphers: 0, - group_ciphers: 0, - mode: WifiMode::Station, - }, - WiFiState::NotConnected, - )); - } - } - } - } - - pub fn retry_send( - &mut self, - cmd: &A, - attempts: usize, - ) -> Result - where - A: atat::AtatCmd, - { - for _ in 0..attempts { - match self.send_internal(cmd, true) { - Ok(resp) => { - return Ok(resp); - } - Err(_e) => {} - }; - } - Err(Error::BaudDetection) - } - - pub fn reset(&mut self) -> Result<(), Error> { - self.serial_mode = SerialMode::Cmd; - self.initialized = false; - self.module_started = false; - self.wifi_connection = None; - self.wifi_config_active_on_startup = None; - self.security_credentials = SecurityCredentials::default(); - self.socket_map = SocketMap::default(); - self.udp_listener = UdpListener::new(); - - self.clear_buffers()?; - - if let Some(ref mut pin) = self.config.rst_pin { - defmt::warn!("Hard resetting Ublox Short Range"); - pin.set_low().ok(); - - BlockingTimer::after(Duration::from_millis(50)).wait(); - - pin.set_high().ok(); - - let expiration = Instant::now() + Duration::from_secs(4); - - while Instant::now() < expiration { - self.handle_urc().ok(); - if self.module_started { - return Ok(()); - } - } - return Err(Error::Timeout); - } - - Ok(()) - } - - pub fn software_reset(&mut self) -> Result<(), Error> { - self.serial_mode = SerialMode::Cmd; - self.initialized = false; - self.module_started = false; - self.wifi_connection = None; - self.wifi_config_active_on_startup = None; - self.security_credentials = SecurityCredentials::default(); - self.socket_map = SocketMap::default(); - self.udp_listener = UdpListener::new(); - - defmt::warn!("Soft resetting Ublox Short Range"); - self.send_internal(&EdmAtCmdWrapper(RebootDCE), false)?; - self.clear_buffers()?; - - let expiration = Instant::now() + Duration::from_secs(4); - while Instant::now() < expiration { - self.handle_urc().ok(); - if self.module_started { - return Ok(()); - } - } - Err(Error::Timeout) - } - - pub(crate) fn clear_buffers(&mut self) -> Result<(), Error> { - if let Some(ref mut sockets) = self.sockets.as_deref_mut() { - sockets.prune(); - } - - Ok(()) - } - - pub fn spin(&mut self) -> Result<(), Error> { - if !self.initialized { - return Err(Error::Uninitialized); - } - - self.handle_urc()?; - - self.connected_to_network()?; - - 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.client.send(req).map_err(|e| { - error!("{:?}: {=[u8]:a}", e, req.as_bytes()); - e.into() - }) - } - - fn handle_urc(&mut self) -> Result<(), Error> { - if let Some(edm_urc) = self.urc_subscription.try_next_message_pure() { - match edm_urc { - EdmEvent::ATEvent(urc) => { - match urc { - Urc::StartUp => { - debug!("[URC] Startup"); - self.module_started = true; - self.initialized = false; - self.serial_mode = SerialMode::Cmd; - } - Urc::PeerConnected(event) => { - debug!("[URC] PeerConnected"); - - // TODO: - // - // We should probably move - // `tcp.set_state(TcpState::Connected(endpoint));` - // + `udp.set_state(UdpState::Established);` as - // well as `tcp.update_handle(*socket);` + - // `udp.update_handle(*socket);` here, to make - // sure that part also works without EDM mode - - if let Some(sockets) = self.sockets.as_deref_mut() { - let remote_ip = Ipv4Addr::from_str( - core::str::from_utf8(event.remote_address.as_slice()).unwrap(), - ) - .unwrap(); - - let remote = SocketAddr::new(remote_ip.into(), event.remote_port); - - if let Some(queue) = self.udp_listener.incoming(event.local_port) { - trace!("[UDP Server] Server socket incomming"); - 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. - // TODO Is this correct? - if !sockets.recycle() {} - } - let peer_handle = event.handle; - let socket_handle = - SocketHandle(new_socket_num(sockets).unwrap()); - let mut new_socket = UdpSocket::new(socket_handle.0); - new_socket.set_state(UdpState::Established); - if new_socket.bind(remote).is_err() { - error!("[UDP_URC] Binding connecting socket Error"); - } - if sockets.add(new_socket).is_err() { - error!("[UDP_URC] Opening socket Error: Socket set full"); - } - if self - .socket_map - .insert_peer(peer_handle, socket_handle) - .is_err() - { - error!("[UDP_URC] Opening socket Error: Socket Map full"); - } - debug!( - "[URC] Binding remote {=[u8]:a} to UDP server on port: {:?} with handle: {:?}", - event.remote_address.as_slice(), - event.local_port, - socket_handle - ); - queue.enqueue((socket_handle, remote)).ok(); - } else { - match event.protocol { - IPProtocol::TCP => { - // if let Ok(mut tcp) = - // sockets.get::>(event.handle) - // { - // debug!( - // "Binding remote {=[u8]:a} to TCP socket {:?}", - // event.remote_address.as_slice(), - // event.handle - // ); - // tcp.set_state(TcpState::Connected(remote)); - // return true; - // } - } - IPProtocol::UDP => { - // if let Ok(mut udp) = - // sockets.get::>(event.handle) - // { - // debug!( - // "Binding remote {=[u8]:a} to UDP socket {:?}", - // event.remote_address.as_slice(), - // event.handle - // ); - // udp.bind(remote).unwrap(); - // udp.set_state(UdpState::Established); - // return true; - // } - } - } - } - } - } - Urc::PeerDisconnected(msg) => { - debug!("[URC] PeerDisconnected"); - if let Some(sockets) = self.sockets.as_deref_mut() { - if let Some(handle) = self.socket_map.peer_to_socket(&msg.handle) { - match sockets.socket_type(*handle) { - Some(SocketType::Tcp) => { - if let Ok(mut tcp) = - sockets.get::>(*handle) - { - tcp.closed_by_remote(); - } - } - Some(SocketType::Udp) => { - if let Ok(mut udp) = - sockets.get::>(*handle) - { - udp.close(); - } - sockets.remove(*handle).ok(); - } - _ => {} - } - self.socket_map.remove_peer(&msg.handle); - } - } - } - Urc::WifiLinkConnected(msg) => { - debug!("[URC] WifiLinkConnected"); - if let Some(ref mut con) = self.wifi_connection { - con.wifi_state = WiFiState::Connected; - con.network.bssid = msg.bssid; - con.network.channel = msg.channel; - } else { - debug!("[URC] Active network config discovered"); - self.wifi_connection.replace( - WifiConnection::new( - WifiNetwork { - bssid: msg.bssid, - op_mode: crate::command::wifi::types::OperationMode::Infrastructure, - ssid: heapless::String::new(), - channel: msg.channel, - rssi: 1, - authentication_suites: 0, - unicast_ciphers: 0, - group_ciphers: 0, - mode: WifiMode::Station, - }, - WiFiState::Connected, - ).activate() - ); - } - } - Urc::WifiLinkDisconnected(msg) => { - debug!("[URC] WifiLinkDisconnected"); - if let Some(ref mut con) = self.wifi_connection { - match msg.reason { - DisconnectReason::NetworkDisabled => { - con.wifi_state = WiFiState::Inactive; - } - DisconnectReason::SecurityProblems => { - error!("Wifi Security Problems"); - } - _ => { - con.wifi_state = WiFiState::NotConnected; - } - } - } - } - Urc::WifiAPUp(_) => { - debug!("[URC] WifiAPUp"); - } - Urc::WifiAPDown(_) => { - debug!("[URC] WifiAPDown"); - } - Urc::WifiAPStationConnected(client) => { - debug!( - "[URC] WifiAPStationConnected {=[u8]:a}", - client.mac_addr.into_inner() - ); - } - Urc::WifiAPStationDisconnected(_) => { - debug!("[URC] WifiAPStationDisconnected"); - } - Urc::EthernetLinkUp(_) => { - debug!("[URC] EthernetLinkUp"); - } - Urc::EthernetLinkDown(_) => { - debug!("[URC] EthernetLinkDown"); - } - Urc::NetworkUp(_) => { - debug!("[URC] NetworkUp"); - if let Some(ref mut con) = self.wifi_connection { - if self.config.network_up_bug { - match con.network_state { - NetworkState::Attached => (), - NetworkState::AlmostAttached => { - con.network_state = NetworkState::Attached - } - NetworkState::Unattached => { - con.network_state = NetworkState::AlmostAttached - } - } - } else { - con.network_state = NetworkState::Attached; - } - } - } - Urc::NetworkDown(_) => { - debug!("[URC] NetworkDown"); - if let Some(ref mut con) = self.wifi_connection { - con.network_state = NetworkState::Unattached; - } - } - Urc::NetworkError(_) => { - debug!("[URC] NetworkError"); - } - Urc::PingResponse(resp) => { - debug!("[URC] PingResponse"); - self.dns_table.upsert(DNSTableEntry { - domain_name: resp.hostname, - state: DNSState::Resolved(resp.ip), - }); - } - Urc::PingErrorResponse(resp) => { - debug!("[URC] PingErrorResponse: {:?}", resp.error); - } - } - } // end match urc - EdmEvent::StartUp => { - debug!("[EDM_URC] STARTUP"); - self.module_started = true; - self.serial_mode = SerialMode::ExtendedData; - } - EdmEvent::IPv4ConnectEvent(event) => { - debug!( - "[EDM_URC] IPv4ConnectEvent! Channel_id: {:?}", - event.channel_id - ); - - if let Some(sockets) = self.sockets.as_deref_mut() { - let endpoint = SocketAddr::new(event.remote_ip.into(), event.remote_port); - - // This depends upon Connected AT-URC to arrive first. - if let Some(queue) = self.udp_listener.incoming(event.local_port) { - if let Some((socket_handle, _)) = - queue.into_iter().find(|(_, remote)| remote == &endpoint) - { - self.socket_map - .insert_channel(event.channel_id, *socket_handle) - .ok(); - } - } else { - for (h, s) in sockets.iter_mut() { - match (&event.protocol, s.get_type()) { - (Protocol::TCP, SocketType::Tcp) => { - let mut tcp = TcpSocket::downcast(s)?; - if tcp.endpoint() == Some(endpoint) { - self.socket_map - .insert_channel(event.channel_id, h) - .ok(); - tcp.set_state(TcpState::Connected(endpoint)); - } - } - (Protocol::UDP, SocketType::Udp) => { - let mut udp = UdpSocket::downcast(s)?; - if udp.endpoint() == Some(endpoint) { - self.socket_map - .insert_channel(event.channel_id, h) - .ok(); - udp.set_state(UdpState::Established); - } - } - _ => {} - } - } - } - } - } - EdmEvent::IPv6ConnectEvent(event) => { - debug!( - "[EDM_URC] IPv6ConnectEvent! Channel_id: {:?}", - event.channel_id - ); - - if let Some(sockets) = self.sockets.as_deref_mut() { - let endpoint = SocketAddr::new(event.remote_ip.into(), event.remote_port); - - // This depends upon Connected AT-URC to arrive first. - if let Some(queue) = self.udp_listener.incoming(event.local_port) { - if let Some((socket_handle, _)) = - queue.into_iter().find(|(_, remote)| remote == &endpoint) - { - self.socket_map - .insert_channel(event.channel_id, *socket_handle) - .ok(); - } - } else { - for (h, s) in sockets.iter_mut() { - match (&event.protocol, s.get_type()) { - (Protocol::TCP, SocketType::Tcp) => { - let mut tcp = TcpSocket::downcast(s)?; - if tcp.endpoint() == Some(endpoint) { - self.socket_map - .insert_channel(event.channel_id, h) - .ok(); - tcp.set_state(TcpState::Connected(endpoint)); - } - } - (Protocol::UDP, SocketType::Udp) => { - let mut udp = UdpSocket::downcast(s)?; - if udp.endpoint() == Some(endpoint) { - self.socket_map - .insert_channel(event.channel_id, h) - .ok(); - udp.set_state(UdpState::Established); - } - } - _ => {} - } - } - } - } - } - EdmEvent::BluetoothConnectEvent(_) => { - debug!("[EDM_URC] BluetoothConnectEvent"); - } - EdmEvent::DisconnectEvent(channel_id) => { - debug!("[EDM_URC] DisconnectEvent! Channel_id: {:?}", channel_id); - self.socket_map.remove_channel(&channel_id); - } - EdmEvent::DataEvent(event) => { - debug!("[EDM_URC] DataEvent! Channel_id: {:?}", event.channel_id); - if let Some(sockets) = self.sockets.as_deref_mut() { - if !event.data.is_empty() { - if let Some(socket_handle) = - self.socket_map.channel_to_socket(&event.channel_id) - { - match sockets.socket_type(*socket_handle) { - Some(SocketType::Tcp) => { - // Handle tcp socket - let mut tcp = - sockets.get::>(*socket_handle).unwrap(); - if tcp.can_recv() { - tcp.rx_enqueue_slice(&event.data); - } - } - Some(SocketType::Udp) => { - // Handle udp socket - let mut udp = - sockets.get::>(*socket_handle).unwrap(); - - if udp.can_recv() { - udp.rx_enqueue_slice(&event.data); - } - } - _ => { - error!("SocketNotFound {:?}", socket_handle); - } - } - } - } - } - } - }; - }; - Ok(()) - } - - /// Send AT command - /// Automaticaly waraps commands in EDM context - pub fn send_at(&mut self, cmd: A) -> Result - where - A: atat::AtatCmd, - { - if !self.initialized { - return Err(Error::Uninitialized); - } - match self.serial_mode { - SerialMode::ExtendedData => self.send_internal(&EdmAtCmdWrapper(cmd), true), - SerialMode::Cmd => self.send_internal(&cmd, true), - } - } - - /// Is the module attached to a WiFi and ready to open sockets - pub(crate) fn connected_to_network(&self) -> Result<(), Error> { - if let Some(ref con) = self.wifi_connection { - if !self.initialized { - Err(Error::Uninitialized) - } else if !con.is_connected() { - Err(Error::WifiState(con.wifi_state)) - } else if self.sockets.is_none() { - Err(Error::MissingSocketSet) - } else { - Ok(()) - } - } else { - Err(Error::NoWifiSetup) - } - } - - /// Is the module attached to a WiFi - /// - // TODO: handle this case for better stability - // WiFi connection can disconnect momentarily, but if the network state does not change - // the current context is safe. - pub fn attached_to_wifi(&self) -> Result<(), Error> { - if let Some(ref con) = self.wifi_connection { - if !self.initialized { - Err(Error::Uninitialized) - // } else if !(con.network_state == NetworkState::Attached) { - } else if !con.is_connected() { - if con.wifi_state == WiFiState::Connected { - Err(Error::NetworkState(con.network_state)) - } else { - Err(Error::WifiState(con.wifi_state)) - } - } else { - Ok(()) - } - } else { - Err(Error::NoWifiSetup) - } - } -} diff --git a/ublox-short-range/src/config.rs b/ublox-short-range/src/config.rs deleted file mode 100644 index 2002734..0000000 --- a/ublox-short-range/src/config.rs +++ /dev/null @@ -1,94 +0,0 @@ -use embedded_hal::digital::{ErrorType, OutputPin}; -use heapless::String; - -pub struct NoPin; - -impl ErrorType for NoPin { - type Error = core::convert::Infallible; -} - -impl OutputPin for NoPin { - fn set_low(&mut self) -> Result<(), Self::Error> { - Ok(()) - } - - fn set_high(&mut self) -> Result<(), Self::Error> { - Ok(()) - } -} - -#[derive(Debug)] -pub struct Config { - pub(crate) rst_pin: Option, - pub(crate) hostname: Option>, - pub(crate) tls_in_buffer_size: Option, - pub(crate) tls_out_buffer_size: Option, - pub(crate) network_up_bug: bool, -} - -impl Default for Config { - fn default() -> Self { - Config { - rst_pin: None, - hostname: None, - tls_in_buffer_size: None, - tls_out_buffer_size: None, - network_up_bug: true, - } - } -} - -impl Config -where - RST: OutputPin, -{ - pub fn new() -> Self { - Config { - rst_pin: None, - hostname: None, - tls_in_buffer_size: None, - tls_out_buffer_size: None, - network_up_bug: true, - } - } - - pub fn with_rst(self, rst_pin: RST) -> Self { - Config { - rst_pin: Some(rst_pin), - ..self - } - } - - pub fn with_hostname(self, hostname: &str) -> Self { - Config { - hostname: Some(String::from(hostname)), - ..self - } - } - - /// Experimental use of undocumented setting for TLS buffers - /// - /// For Odin: - /// Minimum is 512 and maximum is 16K (16384). - /// DEFAULT_TLS_IN_BUFFER_SIZE (7800) - pub fn tls_in_buffer_size(self, bytes: u16) -> Self { - assert!(bytes > 512); - Config { - tls_in_buffer_size: Some(bytes), - ..self - } - } - - /// Experimental use of undocumented setting for TLS buffers - /// - /// For Odin: - /// Minimum is 512 and maximum is 16K (16384). - /// DEFAULT_TLS_OUT_BUFFER_SIZE (3072) - pub fn tls_out_buffer_size(self, bytes: u16) -> Self { - assert!(bytes > 512); - Config { - tls_out_buffer_size: Some(bytes), - ..self - } - } -} diff --git a/ublox-short-range/src/lib.rs b/ublox-short-range/src/lib.rs deleted file mode 100644 index bd58012..0000000 --- a/ublox-short-range/src/lib.rs +++ /dev/null @@ -1,32 +0,0 @@ -#![cfg_attr(not(test), no_std)] - -mod blocking_timer; -mod client; -mod hex; - -pub use atat; -pub use client::UbloxClient; -use client::{URC_CAPACITY, URC_SUBSCRIBERS}; - -pub mod command; -pub mod config; -pub mod error; -pub mod wifi; - -use command::edm::urc::EdmEvent; -#[cfg(any(feature = "socket-udp", feature = "socket-tcp"))] -pub use wifi::tls::TLS; - -pub type UbloxWifiBuffers = - atat::Buffers; - -pub type UbloxWifiIngress<'a, const INGRESS_BUF_SIZE: usize> = atat::Ingress< - 'a, - command::custom_digest::EdmDigester, - EdmEvent, - INGRESS_BUF_SIZE, - URC_CAPACITY, - URC_SUBSCRIBERS, ->; - -pub type UbloxWifiUrcChannel = atat::UrcChannel; diff --git a/ublox-short-range/src/wifi/ap.rs b/ublox-short-range/src/wifi/ap.rs deleted file mode 100644 index e561c52..0000000 --- a/ublox-short-range/src/wifi/ap.rs +++ /dev/null @@ -1,228 +0,0 @@ -use crate::{ - client::UbloxClient, - command::{ - edm::EdmAtCmdWrapper, - wifi::{ - self, - types::{ - AccessPointAction, AccessPointConfig, AccessPointId, IPv4Mode, PasskeyR, - SecurityMode, SecurityModePSK, - }, - SetWifiAPConfig, WifiAPAction, - }, - }, - error::WifiHotspotError, - wifi::{ - network::{WifiMode, WifiNetwork}, - options::{ConnectionOptions, HotspotOptions}, - }, -}; -use atat::blocking::AtatClient; -use atat::heapless_bytes::Bytes; -use embedded_hal::digital::OutputPin; - -use super::connection::{WiFiState, WifiConnection}; - -impl<'buf, 'sub, AtCl, AtUrcCh, RST, const N: usize, const L: usize> - UbloxClient<'buf, 'sub, AtCl, AtUrcCh, RST, N, L> -where - 'buf: 'sub, - AtCl: AtatClient, - RST: OutputPin, -{ - /// Creates wireless hotspot service for host machine. - pub fn create_hotspot( - &mut self, - options: ConnectionOptions, - configuration: HotspotOptions, - ) -> Result<(), WifiHotspotError> { - let ap_config_id = AccessPointId::Id0; - - // Network part - // Deactivate network id 0 - self.send_internal( - &EdmAtCmdWrapper(WifiAPAction { - ap_config_id, - ap_action: AccessPointAction::Deactivate, - }), - true, - )?; - - self.send_internal( - &EdmAtCmdWrapper(WifiAPAction { - ap_config_id, - ap_action: AccessPointAction::Reset, - }), - true, - )?; - - if let Some(ref con) = self.wifi_connection { - if con.activated { - return Err(WifiHotspotError::CreationFailed); - } - } - - // Disable DHCP Server (static IP address will be used) - if options.ip.is_some() || options.subnet.is_some() || options.gateway.is_some() { - self.send_internal( - &EdmAtCmdWrapper(SetWifiAPConfig { - ap_config_id, - ap_config_param: AccessPointConfig::IPv4Mode(IPv4Mode::Static), - }), - true, - )?; - } - - // Network IP address - if let Some(ip) = options.ip { - self.send_internal( - &EdmAtCmdWrapper(SetWifiAPConfig { - ap_config_id, - ap_config_param: AccessPointConfig::IPv4Address(ip), - }), - true, - )?; - } - // Network Subnet mask - if let Some(subnet) = options.subnet { - self.send_internal( - &EdmAtCmdWrapper(SetWifiAPConfig { - ap_config_id, - ap_config_param: AccessPointConfig::SubnetMask(subnet), - }), - true, - )?; - } - // Network Default gateway - if let Some(gateway) = options.gateway { - self.send_internal( - &EdmAtCmdWrapper(SetWifiAPConfig { - ap_config_id, - ap_config_param: AccessPointConfig::DefaultGateway(gateway), - }), - true, - )?; - } - - self.send_internal( - &EdmAtCmdWrapper(SetWifiAPConfig { - ap_config_id, - ap_config_param: AccessPointConfig::DHCPServer(true.into()), - }), - true, - )?; - - // Active on startup - // self.send_internal(&SetWifiAPConfig{ - // ap_config_id, - // ap_config_param: AccessPointConfig::ActiveOnStartup(true), - // }, true)?; - - // Wifi part - // Set the Network SSID to connect to - self.send_internal( - &EdmAtCmdWrapper(SetWifiAPConfig { - ap_config_id, - ap_config_param: AccessPointConfig::SSID(options.ssid.clone()), - }), - true, - )?; - - if let Some(pass) = options.password.clone() { - // Use WPA2 as authentication type - self.send_internal( - &EdmAtCmdWrapper(SetWifiAPConfig { - ap_config_id, - ap_config_param: AccessPointConfig::SecurityMode( - SecurityMode::Wpa2AesCcmp, - SecurityModePSK::PSK, - ), - }), - true, - )?; - - // Input passphrase - self.send_internal( - &EdmAtCmdWrapper(SetWifiAPConfig { - ap_config_id, - ap_config_param: AccessPointConfig::PSKPassphrase(PasskeyR::Passphrase(pass)), - }), - true, - )?; - } else { - self.send_internal( - &EdmAtCmdWrapper(SetWifiAPConfig { - ap_config_id, - ap_config_param: AccessPointConfig::SecurityMode( - SecurityMode::Open, - SecurityModePSK::Open, - ), - }), - true, - )?; - } - - if let Some(channel) = configuration.channel { - self.send_internal( - &EdmAtCmdWrapper(SetWifiAPConfig { - ap_config_id, - ap_config_param: AccessPointConfig::Channel(channel as u8), - }), - true, - )?; - } - - self.send_internal( - &EdmAtCmdWrapper(WifiAPAction { - ap_config_id, - ap_action: AccessPointAction::Activate, - }), - true, - )?; - - self.wifi_connection.replace( - WifiConnection::new( - WifiNetwork { - bssid: Bytes::new(), - op_mode: wifi::types::OperationMode::AdHoc, - ssid: options.ssid, - channel: 0, - rssi: 1, - authentication_suites: 0, - unicast_ciphers: 0, - group_ciphers: 0, - mode: WifiMode::AccessPoint, - }, - WiFiState::NotConnected, - ) - .activate(), - ); - Ok(()) - } - - /// Stop serving a wireless network. - /// - /// **NOTE: All users connected will automatically be disconnected.** - pub fn stop_hotspot(&mut self) -> Result<(), WifiHotspotError> { - let ap_config_id = AccessPointId::Id0; - - if let Some(ref con) = self.wifi_connection { - if con.activated { - self.send_internal( - &EdmAtCmdWrapper(WifiAPAction { - ap_config_id, - ap_action: AccessPointAction::Deactivate, - }), - true, - )?; - } - } else { - return Err(WifiHotspotError::FailedToStop); - } - if let Some(ref mut con) = self.wifi_connection { - con.deactivate() - } - - Ok(()) - } -} diff --git a/ublox-short-range/src/wifi/connection.rs b/ublox-short-range/src/wifi/connection.rs deleted file mode 100644 index c3717e9..0000000 --- a/ublox-short-range/src/wifi/connection.rs +++ /dev/null @@ -1,61 +0,0 @@ -use crate::wifi::network::{WifiMode, WifiNetwork}; - -#[derive(Debug, Clone, Copy, PartialEq, defmt::Format)] -pub enum WiFiState { - Inactive, - /// Searching for Wifi - NotConnected, - Connected, -} - -/// Describes whether device is connected to a network and has an IP or not. -/// It is possible to be attached to a network but have no Wifi connection. -#[derive(Debug, Clone, Copy, PartialEq, defmt::Format)] -pub enum NetworkState { - Attached, - AlmostAttached, - Unattached, -} - -// Fold into wifi connectivity -#[derive(defmt::Format)] -pub struct WifiConnection { - /// Keeps track of connection state on module - pub wifi_state: WiFiState, - pub network_state: NetworkState, - pub network: WifiNetwork, - /// Keeps track of activation of the config by driver - pub activated: bool, -} - -impl WifiConnection { - pub(crate) fn new(network: WifiNetwork, wifi_state: WiFiState) -> Self { - WifiConnection { - wifi_state, - network_state: NetworkState::Unattached, - network, - activated: false, - } - } - - pub(crate) fn is_connected(&self) -> bool { - self.network_state == NetworkState::Attached && self.wifi_state == WiFiState::Connected - } - - pub fn is_station(&self) -> bool { - self.network.mode == WifiMode::Station - } - - pub fn is_access_point(&self) -> bool { - !self.is_station() - } - - pub(crate) fn activate(mut self) -> Self { - self.activated = true; - self - } - - pub(crate) fn deactivate(&mut self) { - self.activated = false; - } -} diff --git a/ublox-short-range/src/wifi/dns.rs b/ublox-short-range/src/wifi/dns.rs deleted file mode 100644 index 624949a..0000000 --- a/ublox-short-range/src/wifi/dns.rs +++ /dev/null @@ -1,63 +0,0 @@ -use crate::client::{DNSState, DNSTableEntry}; -use atat::blocking::AtatClient; -use embassy_time::{Duration, Instant}; -use embedded_hal::digital::OutputPin; -use embedded_nal::{nb, AddrType, Dns, IpAddr}; -use heapless::String; - -use crate::{command::ping::*, UbloxClient}; -use ublox_sockets::Error; - -impl<'buf, 'sub, AtCl, AtUrcCh, RST, const N: usize, const L: usize> Dns - for UbloxClient<'buf, 'sub, AtCl, AtUrcCh, RST, N, L> -where - 'buf: 'sub, - AtCl: AtatClient, - RST: OutputPin, -{ - type Error = Error; - - fn get_host_by_address(&mut self, _ip_addr: IpAddr) -> nb::Result, Self::Error> { - unimplemented!() - } - - fn get_host_by_name( - &mut self, - hostname: &str, - _addr_type: AddrType, - ) -> nb::Result { - defmt::debug!("Lookup hostname: {}", hostname); - self.send_at(Ping { - hostname, - retry_num: 1, - }) - .map_err(|_| nb::Error::Other(Error::Unaddressable))?; - - self.dns_table.upsert(DNSTableEntry::new( - DNSState::Resolving, - String::from(hostname), - )); - - let expiration = Instant::now() + Duration::from_secs(8); - - while let Some(DNSState::Resolving) = self.dns_table.get_state(String::from(hostname)) { - self.spin().map_err(|_| nb::Error::Other(Error::Illegal))?; - - if Instant::now() >= expiration { - break; - } - } - - match self.dns_table.get_state(String::from(hostname)) { - Some(DNSState::Resolved(ip)) => Ok(ip), - Some(DNSState::Resolving) => { - self.dns_table.upsert(DNSTableEntry::new( - DNSState::Error(types::PingError::Timeout), - String::from(hostname), - )); - Err(nb::Error::Other(Error::Timeout)) - } - _ => Err(nb::Error::Other(Error::Illegal)), - } - } -} diff --git a/ublox-short-range/src/wifi/mod.rs b/ublox-short-range/src/wifi/mod.rs deleted file mode 100644 index b570b1d..0000000 --- a/ublox-short-range/src/wifi/mod.rs +++ /dev/null @@ -1,122 +0,0 @@ -use crate::command::PeerHandle; -pub use ublox_sockets::SocketHandle; - -use crate::command::edm::types::ChannelId; - -pub mod ap; -pub mod connection; -pub mod dns; -pub mod network; -pub mod options; -pub mod sta; -pub mod tls; - -pub mod peer_builder; - -#[cfg(feature = "socket-udp")] -pub mod udp_stack; - -#[cfg(feature = "socket-tcp")] -pub mod tcp_stack; - -pub(crate) const EGRESS_CHUNK_SIZE: usize = 512; -/// The socket map, keeps mappings between `ublox::sockets`s `SocketHandle`, -/// and the modems `PeerHandle` and `ChannelId`. The peer handle is used -/// for controlling the connection, while the channel id is used for sending -/// data over the connection in EDM mode. - -pub enum SocketMapError { - Full, - NotFound, -} - -pub struct SocketMap { - channel_map: heapless::FnvIndexMap, - peer_map: heapless::FnvIndexMap, -} - -impl defmt::Format for SocketMap { - fn format(&self, fmt: defmt::Formatter) { - defmt::write!(fmt, "ChannelMap:\n"); - for (channel, socket) in self.channel_map.iter() { - defmt::write!(fmt, "channelId: {}, Handle: {}\n", channel.0, socket.0) - } - defmt::write!(fmt, "PeerMap:\n"); - for (peer, socket) in self.peer_map.iter() { - defmt::write!(fmt, "PeerId: {}, Handle: {}\n", peer.0, socket.0) - } - } -} - -impl Default for SocketMap { - fn default() -> Self { - Self::new() - } -} - -impl SocketMap { - fn new() -> Self { - Self { - channel_map: heapless::FnvIndexMap::new(), - peer_map: heapless::FnvIndexMap::new(), - } - } - - pub fn insert_channel( - &mut self, - channel_id: ChannelId, - socket_handle: SocketHandle, - ) -> Result<(), SocketMapError> { - defmt::trace!("[SOCK_MAP] {:?} tied to {:?}", socket_handle, channel_id); - match self.channel_map.insert(channel_id, socket_handle) { - Ok(_) => Ok(()), - Err(_) => { - defmt::error!("Failed inserting channel SocketMap full"); - Err(SocketMapError::Full) - } - } - } - - pub fn remove_channel(&mut self, channel_id: &ChannelId) { - defmt::trace!("[SOCK_MAP] {:?} removed", channel_id); - self.channel_map.remove(channel_id); - } - - pub fn channel_to_socket(&self, channel_id: &ChannelId) -> Option<&SocketHandle> { - self.channel_map.get(channel_id) - } - - pub fn socket_to_channel_id(&self, socket_handle: &SocketHandle) -> Option<&ChannelId> { - self.channel_map - .iter() - .find_map(|(c, s)| if s == socket_handle { Some(c) } else { None }) - } - - pub fn insert_peer( - &mut self, - peer: PeerHandle, - socket_handle: SocketHandle, - ) -> Result<(), SocketMapError> { - defmt::trace!("[SOCK_MAP] {:?} tied to {:?}", socket_handle, peer); - if self.peer_map.insert(peer, socket_handle).is_err() { - defmt::error!("Insert peer failed SocketMap is FULL"); - return Err(SocketMapError::Full); - }; - Ok(()) - } - - pub fn remove_peer(&mut self, peer: &PeerHandle) { - defmt::trace!("[SOCK_MAP] {:?} removed", peer); - self.peer_map.remove(peer); - } - - pub fn peer_to_socket(&self, peer: &PeerHandle) -> Option<&SocketHandle> { - self.peer_map.get(peer) - } - - pub fn socket_to_peer(&self, socket_handle: &SocketHandle) -> Option<&PeerHandle> { - self.peer_map - .iter() - .find_map(|(c, s)| if s == socket_handle { Some(c) } else { None }) - } -} diff --git a/ublox-short-range/src/wifi/options.rs b/ublox-short-range/src/wifi/options.rs deleted file mode 100644 index f76b581..0000000 --- a/ublox-short-range/src/wifi/options.rs +++ /dev/null @@ -1,136 +0,0 @@ -use embedded_nal::Ipv4Addr; -use heapless::String; -use serde::{Deserialize, Serialize}; - -#[allow(dead_code)] -#[derive(Debug, Clone, Copy)] -/// Channel to broadcast wireless hotspot on. -pub enum Channel { - /// Channel 1 - One = 1, - /// Channel 2 - Two = 2, - /// Channel 3 - Three = 3, - /// Channel 4 - Four = 4, - /// Channel 5 - Five = 5, - /// Channel 6 - Six = 6, -} - -#[allow(dead_code)] -#[derive(Debug)] -/// Band type of wireless hotspot. -pub enum Band { - /// Band `A` - A, - /// Band `BG` - Bg, -} - -#[derive(Debug, Default)] -pub struct HotspotOptions { - pub(crate) channel: Option, - pub(crate) band: Option, -} - -impl HotspotOptions { - pub fn new() -> Self { - Self { - channel: Some(Channel::One), - band: Some(Band::Bg), - } - } - - pub fn channel(mut self, channel: Channel) -> Self { - self.channel = Some(channel); - self - } - - pub fn band(mut self, band: Band) -> Self { - self.band = Some(band); - self - } -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize, defmt::Format)] -pub struct ConnectionOptions { - pub ssid: String<64>, - pub password: Option>, - - #[defmt(Debug2Format)] - pub ip: Option, - #[defmt(Debug2Format)] - pub subnet: Option, - #[defmt(Debug2Format)] - pub gateway: Option, -} - -impl ConnectionOptions { - pub fn new() -> Self { - Self::default() - } - - pub fn ssid(mut self, ssid: String<64>) -> Self { - self.ssid = ssid; - self - } - - pub fn password(mut self, password: String<64>) -> Self { - self.password = Some(password); - self - } - - pub fn ip_address(mut self, ip_addr: Ipv4Addr) -> Self { - self.ip = Some(ip_addr); - self.subnet = if let Some(subnet) = self.subnet { - Some(subnet) - } else { - Some(Ipv4Addr::new(255, 255, 255, 0)) - }; - - self.gateway = if let Some(gateway) = self.gateway { - Some(gateway) - } else { - Some(Ipv4Addr::new(192, 168, 2, 1)) - }; - self - } - - pub fn subnet_address(mut self, subnet_addr: Ipv4Addr) -> Self { - self.subnet = Some(subnet_addr); - - self.ip = if let Some(ip) = self.ip { - Some(ip) - } else { - Some(Ipv4Addr::new(192, 168, 2, 1)) - }; - - self.gateway = if let Some(gateway) = self.gateway { - Some(gateway) - } else { - Some(Ipv4Addr::new(192, 168, 2, 1)) - }; - - self - } - - pub fn gateway_address(mut self, gateway_addr: Ipv4Addr) -> Self { - self.gateway = Some(gateway_addr); - - self.subnet = if let Some(subnet) = self.subnet { - Some(subnet) - } else { - Some(Ipv4Addr::new(255, 255, 255, 0)) - }; - - self.ip = if let Some(ip) = self.ip { - Some(ip) - } else { - Some(Ipv4Addr::new(192, 168, 2, 1)) - }; - self - } -} diff --git a/ublox-short-range/src/wifi/sta.rs b/ublox-short-range/src/wifi/sta.rs deleted file mode 100644 index 2d850bb..0000000 --- a/ublox-short-range/src/wifi/sta.rs +++ /dev/null @@ -1,220 +0,0 @@ -use crate::{ - client::UbloxClient, - command::{ - edm::EdmAtCmdWrapper, - wifi::{types::*, *}, - *, - }, - error::{WifiConnectionError, WifiError}, - wifi::{ - connection::{WiFiState, WifiConnection}, - network::{WifiMode, WifiNetwork}, - options::ConnectionOptions, - }, -}; - -use atat::{blocking::AtatClient, heapless_bytes::Bytes}; -use core::convert::TryFrom; -use embedded_hal::digital::OutputPin; -use heapless::Vec; - -const CONFIG_ID: u8 = 0; - -impl<'buf, 'sub, AtCl, AtUrcCh, RST, const N: usize, const L: usize> - UbloxClient<'buf, 'sub, AtCl, AtUrcCh, RST, N, L> -where - 'buf: 'sub, - AtCl: AtatClient, - RST: OutputPin, -{ - /// Attempts to connect to a wireless network with the given connection options. - pub fn connect(&mut self, options: &ConnectionOptions) -> Result<(), WifiConnectionError> { - defmt::info!("Connecting to {:?}", options); - // Network part - - if let Some(ref con) = self.wifi_connection { - defmt::warn!("WaitingForWifiDeactivation {:#?}", con); - if con.wifi_state != WiFiState::Inactive { - return Err(WifiConnectionError::WaitingForWifiDeactivation); - } - } - - // Disable DHCP Client (static IP address will be used) - if options.ip.is_some() || options.subnet.is_some() || options.gateway.is_some() { - self.send_internal( - &EdmAtCmdWrapper(SetWifiStationConfig { - config_id: CONFIG_ID, - config_param: WifiStationConfig::IPv4Mode(IPv4Mode::Static), - }), - true, - )?; - } - - // Network IP address - if let Some(ip) = options.ip { - self.send_internal( - &EdmAtCmdWrapper(SetWifiStationConfig { - config_id: CONFIG_ID, - config_param: WifiStationConfig::IPv4Address(ip), - }), - true, - )?; - } - // Network Subnet mask - if let Some(subnet) = options.subnet { - self.send_internal( - &EdmAtCmdWrapper(SetWifiStationConfig { - config_id: CONFIG_ID, - config_param: WifiStationConfig::SubnetMask(subnet), - }), - true, - )?; - } - // Network Default gateway - if let Some(gateway) = options.gateway { - self.send_internal( - &EdmAtCmdWrapper(SetWifiStationConfig { - config_id: CONFIG_ID, - config_param: WifiStationConfig::DefaultGateway(gateway), - }), - true, - )?; - } - - // Wifi part - // Set the Network SSID to connect to - self.send_internal( - &EdmAtCmdWrapper(SetWifiStationConfig { - config_id: CONFIG_ID, - config_param: WifiStationConfig::SSID(&options.ssid), - }), - true, - )?; - - if let Some(ref pass) = options.password { - // Use WPA2 as authentication type - self.send_internal( - &EdmAtCmdWrapper(SetWifiStationConfig { - config_id: CONFIG_ID, - config_param: WifiStationConfig::Authentication(Authentication::WpaWpa2Psk), - }), - true, - )?; - - // Input passphrase - self.send_internal( - &EdmAtCmdWrapper(SetWifiStationConfig { - config_id: CONFIG_ID, - config_param: WifiStationConfig::WpaPskOrPassphrase(&pass), - }), - true, - )?; - } - - self.wifi_connection.replace(WifiConnection::new( - WifiNetwork { - bssid: Bytes::new(), - op_mode: wifi::types::OperationMode::Infrastructure, - ssid: options.ssid.clone(), - channel: 0, - rssi: 1, - authentication_suites: 0, - unicast_ciphers: 0, - group_ciphers: 0, - mode: WifiMode::Station, - }, - WiFiState::NotConnected, - )); - self.send_internal( - &EdmAtCmdWrapper(ExecWifiStationAction { - config_id: CONFIG_ID, - action: WifiStationAction::Activate, - }), - true, - )?; - - // TODO: Await connected event? - - Ok(()) - } - - pub fn activate(&mut self) -> Result<(), WifiConnectionError> { - self.send_internal( - &EdmAtCmdWrapper(ExecWifiStationAction { - config_id: CONFIG_ID, - action: WifiStationAction::Activate, - }), - true, - )?; - return Ok(()); - } - - pub fn scan(&mut self) -> Result, WifiError> { - match self.send_internal(&EdmAtCmdWrapper(WifiScan { ssid: None }), true) { - Ok(resp) => resp - .network_list - .into_iter() - .map(WifiNetwork::try_from) - .collect(), - Err(_) => Err(WifiError::UnexpectedResponse), - } - } - - pub fn is_connected(&self) -> bool { - if !self.initialized { - return false; - } - - self.wifi_connection - .as_ref() - .map(|c| c.is_connected()) - .unwrap_or_default() - } - - pub fn is_active_on_startup(&mut self) -> Result { - if let Ok(resp) = self.send_internal( - &EdmAtCmdWrapper(GetWifiStationConfig { - config_id: CONFIG_ID, - parameter: Some(WifiStationConfigParameter::ActiveOnStartup), - }), - false, - ) { - if let WifiStationConfigR::ActiveOnStartup(active) = resp.parameter { - return Ok(active == OnOff::On); - } - } - Err(WifiConnectionError::Illegal) - } - - pub fn get_ssid(&mut self) -> Result, WifiConnectionError> { - if let Ok(resp) = self.send_internal( - &EdmAtCmdWrapper(GetWifiStationConfig { - config_id: CONFIG_ID, - parameter: Some(WifiStationConfigParameter::SSID), - }), - false, - ) { - if let WifiStationConfigR::SSID(ssid) = resp.parameter { - return Ok(ssid); - } - }; - return Err(WifiConnectionError::Illegal); - } - - pub fn reset_config_profile(&mut self) -> Result<(), WifiConnectionError> { - self.send_at(EdmAtCmdWrapper(ExecWifiStationAction { - config_id: CONFIG_ID, - action: WifiStationAction::Reset, - }))?; - Ok(()) - } - - pub fn disconnect(&mut self) -> Result<(), WifiConnectionError> { - defmt::debug!("Disconnecting"); - self.send_at(EdmAtCmdWrapper(ExecWifiStationAction { - config_id: CONFIG_ID, - action: WifiStationAction::Deactivate, - }))?; - Ok(()) - } -} diff --git a/ublox-short-range/src/wifi/tcp_stack.rs b/ublox-short-range/src/wifi/tcp_stack.rs deleted file mode 100644 index 9f5800c..0000000 --- a/ublox-short-range/src/wifi/tcp_stack.rs +++ /dev/null @@ -1,245 +0,0 @@ -use crate::{ - client::new_socket_num, - command::data_mode::*, - command::edm::{EdmAtCmdWrapper, EdmDataCommand}, - wifi::peer_builder::PeerUrlBuilder, - UbloxClient, -}; -use atat::blocking::AtatClient; -use embedded_hal::digital::OutputPin; -/// Handles receiving data from sockets -/// implements TCP and UDP for WiFi client -use embedded_nal::{nb, SocketAddr, TcpClientStack}; - -use ublox_sockets::{Error, SocketHandle, TcpSocket, TcpState}; - -use super::EGRESS_CHUNK_SIZE; - -impl<'buf, 'sub, AtCl, AtUrcCh, RST, const N: usize, const L: usize> TcpClientStack - for UbloxClient<'buf, 'sub, AtCl, AtUrcCh, RST, N, L> -where - 'buf: 'sub, - AtCl: AtatClient, - RST: OutputPin, -{ - type Error = Error; - - // Only return a SocketHandle to reference into the SocketSet owned by the UbloxClient, - // 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 { - self.connected_to_network().map_err(|_| Error::Illegal)?; - 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); - } - } - - defmt::debug!("[TCP] Opening socket"); - - let socket_id = new_socket_num(sockets).unwrap(); - sockets.add(TcpSocket::new(socket_id)).map_err(|e| { - defmt::error!("[TCP] Opening socket Error: {:?}", e); - e - }) - } 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 self.sockets.is_none() { - return Err(Error::Illegal.into()); - } - - defmt::debug!("[TCP] Connect socket"); - self.connected_to_network().map_err(|_| Error::Illegal)?; - - let url = if let Some(hostname) = self.dns_table.reverse_lookup(remote.ip()) { - PeerUrlBuilder::new() - .hostname(hostname.as_str()) - .port(remote.port()) - .creds(self.security_credentials.clone()) - .tcp() - .map_err(|_| Error::Unaddressable)? - } else { - PeerUrlBuilder::new() - .ip_addr(remote.ip()) - .port(remote.port()) - .creds(self.security_credentials.clone()) - .tcp() - .map_err(|_| Error::Unaddressable)? - }; - - defmt::debug!("[TCP] Connecting socket: {:?} to url: {=str}", socket, url); - - // If no socket is found we stop here - let mut tcp = self - .sockets - .as_mut() - .unwrap() - .get::>(*socket) - .map_err(Self::Error::from)?; - - tcp.set_state(TcpState::WaitingForConnect(remote)); - - match self - .send_internal(&EdmAtCmdWrapper(ConnectPeer { url: &url }), false) - .map_err(|_| Error::Unaddressable) - { - Ok(resp) => self - .socket_map - .insert_peer(resp.peer_handle, *socket) - .map_err(|_| Error::InvalidSocket)?, - Err(e) => { - let mut tcp = self - .sockets - .as_mut() - .unwrap() - .get::>(*socket) - .map_err(Self::Error::from)?; - tcp.set_state(TcpState::Created); - return Err(nb::Error::Other(e)); - } - } - - defmt::debug!("[TCP] Connecting socket: {:?} to url: {=str}", socket, url); - - // TODO: Timeout? - // TODO: Fix the fact that it doesen't wait for both connect messages - while { - matches!( - self.sockets - .as_mut() - .unwrap() - .get::>(*socket) - .map_err(Self::Error::from)? - .state(), - TcpState::WaitingForConnect(_) - ) - } { - self.spin().map_err(|_| Error::Illegal)?; - } - Ok(()) - } - - /// Check if this socket is still connected - fn is_connected(&mut self, socket: &Self::TcpSocket) -> Result { - if self.connected_to_network().is_err() { - return Ok(false); - } - if let Some(ref mut sockets) = self.sockets { - let tcp = sockets.get::>(*socket)?; - Ok(tcp.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 { - self.connected_to_network().map_err(|_| Error::Illegal)?; - if let Some(ref mut sockets) = self.sockets { - let tcp = sockets - .get::>(*socket) - .map_err(nb::Error::Other)?; - - if !tcp.is_connected() { - return Err(Error::SocketClosed.into()); - } - - let channel = *self - .socket_map - .socket_to_channel_id(socket) - .ok_or(nb::Error::Other(Error::SocketClosed))?; - - for chunk in buffer.chunks(EGRESS_CHUNK_SIZE) { - self.send_internal( - &EdmDataCommand { - channel, - data: chunk, - }, - true, - ) - .map_err(|_| nb::Error::Other(Error::Unaddressable))?; - } - Ok(buffer.len()) - } else { - Err(Error::Illegal.into()) - } - } - - fn receive( - &mut self, - socket: &mut Self::TcpSocket, - buffer: &mut [u8], - ) -> nb::Result { - // TODO: Handle error states - self.spin().map_err(|_| nb::Error::Other(Error::Illegal))?; - if let Some(ref mut sockets) = self.sockets { - // Enable detecting closed socket from receive function - sockets.recycle(); - - 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 { - defmt::debug!("[TCP] Closing socket: {:?}", socket); - // If the socket is not found it is already removed - if let Ok(ref tcp) = sockets.get::>(socket) { - // If socket is not closed that means a connection excists which has to be closed - if !matches!( - tcp.state(), - TcpState::ShutdownForWrite(_) | TcpState::Created - ) { - if let Some(peer_handle) = self.socket_map.socket_to_peer(&tcp.handle()) { - let peer_handle = *peer_handle; - match self.send_at(ClosePeerConnection { peer_handle }) { - Err(crate::error::Error::AT(atat::Error::InvalidResponse)) | Ok(_) => { - () - } - Err(_) => return Err(Error::Unaddressable), - } - } else { - defmt::error!( - "Illigal state! Socket connected but not in socket map: {:?}", - tcp.handle() - ); - return Err(Error::Illegal); - } - } else { - // No connection exists the socket should be removed from the set here - sockets.remove(socket)?; - } - } - Ok(()) - } else { - Err(Error::Illegal) - } - } -} diff --git a/ublox-short-range/src/wifi/tls.rs b/ublox-short-range/src/wifi/tls.rs deleted file mode 100644 index 0ffc35e..0000000 --- a/ublox-short-range/src/wifi/tls.rs +++ /dev/null @@ -1,107 +0,0 @@ -use crate::{ - command::edm::BigEdmAtCmdWrapper, - command::security::{types::*, *}, - error::Error, - UbloxClient, -}; -use embedded_hal::digital::OutputPin; -use heapless::String; - -pub trait TLS { - fn import_certificate(&mut self, name: &str, certificate: &[u8]) -> Result<(), Error>; - fn import_root_ca(&mut self, name: &str, root_ca: &[u8]) -> Result<(), Error>; - fn import_private_key( - &mut self, - name: &str, - private_key: &[u8], - password: Option<&str>, - ) -> Result<(), Error>; -} - -impl<'buf, 'sub, AtCl, AtUrcCh, RST, const N: usize, const L: usize> TLS - for UbloxClient<'buf, 'sub, AtCl, AtUrcCh, RST, N, L> -where - 'buf: 'sub, - AtCl: atat::blocking::AtatClient, - RST: OutputPin, -{ - /// Importing credentials enabeles their use for all further TCP connections - fn import_certificate(&mut self, name: &str, certificate: &[u8]) -> Result<(), Error> { - assert!(name.len() < 16); - - self.send_at(PrepareSecurityDataImport { - data_type: SecurityDataType::ClientCertificate, - data_size: certificate.len(), - internal_name: name, - password: None, - })?; - - self.send_internal( - &BigEdmAtCmdWrapper(SendSecurityDataImport { - data: atat::serde_bytes::Bytes::new(certificate), - }), - false, - )?; - - self.security_credentials - .c_cert_name - .replace(String::from(name)); - - Ok(()) - } - - /// Importing credentials enabeles their use for all further TCP connections - fn import_root_ca(&mut self, name: &str, root_ca: &[u8]) -> Result<(), Error> { - assert!(name.len() < 16); - - self.send_at(PrepareSecurityDataImport { - data_type: SecurityDataType::TrustedRootCA, - data_size: root_ca.len(), - internal_name: name, - password: None, - })?; - - self.send_internal( - &BigEdmAtCmdWrapper(SendSecurityDataImport { - data: atat::serde_bytes::Bytes::new(root_ca), - }), - false, - )?; - - self.security_credentials - .ca_cert_name - .replace(String::from(name)); - - Ok(()) - } - - /// Importing credentials enabeles their use for all further TCP connections - fn import_private_key( - &mut self, - name: &str, - private_key: &[u8], - password: Option<&str>, - ) -> Result<(), Error> { - assert!(name.len() < 16); - - self.send_at(PrepareSecurityDataImport { - data_type: SecurityDataType::ClientPrivateKey, - data_size: private_key.len(), - internal_name: name, - password, - })?; - - self.send_internal( - &BigEdmAtCmdWrapper(SendSecurityDataImport { - data: atat::serde_bytes::Bytes::new(private_key), - }), - false, - )?; - - self.security_credentials - .c_key_name - .replace(String::from(name)); - - Ok(()) - } -} diff --git a/ublox-short-range/src/wifi/udp_stack.rs b/ublox-short-range/src/wifi/udp_stack.rs deleted file mode 100644 index 60f7f61..0000000 --- a/ublox-short-range/src/wifi/udp_stack.rs +++ /dev/null @@ -1,411 +0,0 @@ -use crate::{ - client::new_socket_num, - command::data_mode::*, - command::{ - data_mode::types::{IPVersion, ServerType, UDPBehaviour}, - edm::{EdmAtCmdWrapper, EdmDataCommand}, - }, - wifi::peer_builder::PeerUrlBuilder, - UbloxClient, -}; -use atat::blocking::AtatClient; -use embedded_hal::digital::OutputPin; -use embedded_nal::{nb, SocketAddr, UdpFullStack}; - -use embedded_nal::UdpClientStack; -use ublox_sockets::{Error, SocketHandle, UdpSocket, UdpState}; - -use super::EGRESS_CHUNK_SIZE; - -impl<'buf, 'sub, AtCl, AtUrcCh, RST, const N: usize, const L: usize> UdpClientStack - for UbloxClient<'buf, 'sub, AtCl, AtUrcCh, RST, N, L> -where - 'buf: 'sub, - AtCl: AtatClient, - RST: OutputPin, -{ - type Error = Error; - - // Only return a SocketHandle to reference into the SocketSet owned by the UbloxClient, - // as the Socket object itself provides no value without accessing it though the client. - type UdpSocket = SocketHandle; - - fn socket(&mut self) -> Result { - self.connected_to_network().map_err(|_| Error::Illegal)?; - 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_id = new_socket_num(sockets).unwrap(); - defmt::debug!("[UDP] Opening socket"); - sockets.add(UdpSocket::new(socket_id)).map_err(|_| { - defmt::error!("[UDP] Opening socket Error: Socket set full"); - Error::SocketSetFull - }) - } else { - defmt::error!("[UDP] Opening socket Error: Missing socket set"); - Err(Error::Illegal) - } - } - - /// Connect a UDP socket with a peer using a dynamically selected port. - /// Selects a port number automatically and initializes for read/writing. - fn connect( - &mut self, - socket: &mut Self::UdpSocket, - remote: SocketAddr, - ) -> Result<(), Self::Error> { - let mut peer_handle = crate::command::PeerHandle(0); - - if self.sockets.is_none() { - defmt::error!("[UDP] Connecting socket Error: Missing socket set"); - return Err(Error::Illegal); - } - let url = PeerUrlBuilder::new() - .address(&remote) - .udp() - .map_err(|_| Error::Unaddressable)?; - defmt::debug!("[UDP] Connecting Socket: {:?} to URL: {=str}", socket, url); - - self.connected_to_network().map_err(|_| Error::Illegal)?; - - // First look to see if socket is valid - let mut udp = self - .sockets - .as_mut() - .unwrap() - .get::>(*socket)?; - udp.bind(remote)?; - - // Then connect modem - match self - .send_internal(&EdmAtCmdWrapper(ConnectPeer { url: &url }), true) - .map_err(|_| Error::Unaddressable) - { - Ok(resp) => { - peer_handle = resp.peer_handle; - - self.socket_map - .insert_peer(resp.peer_handle, *socket) - .map_err(|_| Error::InvalidSocket)? - } - - Err(e) => { - let mut udp = self - .sockets - .as_mut() - .unwrap() - .get::>(*socket)?; - udp.close(); - return Err(e); - } - } - while self - .sockets - .as_mut() - .unwrap() - .get::>(*socket)? - .state() - == UdpState::Closed - { - match self.spin() { - Ok(_) => {} - Err(_) => { - defmt::error!("ERROR connection UDP removing peer"); - self.socket_map.remove_peer(&peer_handle); - return Err(Error::Illegal); - } - }; - } - Ok(()) - } - - /// Send a datagram to the remote host. - fn send(&mut self, socket: &mut Self::UdpSocket, buffer: &[u8]) -> nb::Result<(), Self::Error> { - self.spin().map_err(|_| Error::Illegal)?; - if let Some(ref mut sockets) = self.sockets { - // No send for server sockets! - if self.udp_listener.is_bound(*socket) { - return Err(nb::Error::Other(Error::Illegal)); - } - - let udp = sockets - .get::>(*socket) - .map_err(Self::Error::from)?; - - if !udp.is_open() { - return Err(Error::SocketClosed.into()); - } - - let channel = *self - .socket_map - .socket_to_channel_id(socket) - .ok_or(nb::Error::Other(Error::SocketClosed))?; - - for chunk in buffer.chunks(EGRESS_CHUNK_SIZE) { - self.send_internal( - &EdmDataCommand { - channel, - data: chunk, - }, - true, - ) - .map_err(|_| nb::Error::Other(Error::Unaddressable))?; - } - 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> { - self.spin().ok(); - let udp_listener = &mut self.udp_listener; - // Handle server sockets - if udp_listener.is_bound(*socket) { - // Nothing available, would block - if !udp_listener.available(*socket).unwrap_or(false) { - return Err(nb::Error::WouldBlock); - } - - let (connection_handle, remote) = self - .udp_listener - .peek_remote(*socket) - .map_err(|_| Error::NotBound)?; - - if let Some(ref mut sockets) = self.sockets { - let mut udp = sockets - .get::>(*connection_handle) - .map_err(|_| Self::Error::InvalidSocket)?; - - let bytes = udp.recv_slice(buffer).map_err(Self::Error::from)?; - Ok((bytes, *remote)) - } else { - Err(Error::Illegal.into()) - } - - // Handle reciving for udp normal sockets - } else 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> { - self.spin().ok(); - // Close server socket - if self.udp_listener.is_bound(socket) { - defmt::debug!("[UDP] Closing Server socket: {:?}", socket); - - // ID 2 used by UDP server - self.send_internal( - &EdmAtCmdWrapper(ServerConfiguration { - id: 2, - server_config: ServerType::Disabled, - }), - true, - ) - .map_err(|_| Error::Unaddressable)?; - - // Borrow socket set to close server socket - if let Some(ref mut sockets) = self.sockets { - // If socket in socket set close - if sockets.remove(socket).is_err() { - defmt::error!( - "[UDP] Closing server socket error: No socket matching: {:?}", - socket - ); - return Err(Error::InvalidSocket); - } - } else { - return Err(Error::Illegal); - } - - // Close incomming connections - while self.udp_listener.available(socket).unwrap_or(false) { - if let Ok((connection_handle, _)) = self.udp_listener.get_remote(socket) { - defmt::debug!( - "[UDP] Closing incomming socket for Server: {:?}", - connection_handle - ); - self.close(connection_handle)?; - } else { - defmt::error!("[UDP] Incomming socket for server error - Listener says available, while nothing present"); - } - } - - // Unbind server socket in listener - self.udp_listener.unbind(socket).map_err(|_| { - defmt::error!( - "[UDP] Closing socket error: No server socket matching: {:?}", - socket - ); - Error::Illegal - }) - // Handle normal sockets - } else if let Some(ref mut sockets) = self.sockets { - defmt::debug!("[UDP] Closing socket: {:?}", socket); - // If no sockets exists, nothing to close. - if let Ok(ref mut udp) = sockets.get::>(socket) { - defmt::trace!("[UDP] Closing socket state: {:?}", udp.state()); - match udp.state() { - UdpState::Closed => { - sockets.remove(socket).ok(); - } - UdpState::Established => { - udp.close(); - if let Some(peer_handle) = self.socket_map.socket_to_peer(&udp.handle()) { - let peer_handle = *peer_handle; - self.send_at(ClosePeerConnection { peer_handle }) - .map_err(|_| Error::Unaddressable)?; - } - } - } - } else { - defmt::error!( - "[UDP] Closing socket error: No socket matching: {:?}", - socket - ); - return Err(Error::InvalidSocket); - } - Ok(()) - } else { - Err(Error::Illegal) - } - } -} - -/// UDP Full Stack -/// -/// This fullstack is build for request-response type servers due to HW/SW limitations -/// Limitations: -/// - The driver can only send to Socket addresses that have send data first. -/// - The driver can only call send_to once after reciving data once. -/// - The driver has to call send_to after reciving data, to release the socket bound by remote host, -/// even if just sending no bytes. Else these sockets will be held open until closure of server socket. -/// -impl<'buf, 'sub, AtCl, AtUrcCh, RST, const N: usize, const L: usize> UdpFullStack - for UbloxClient<'buf, 'sub, AtCl, AtUrcCh, RST, N, L> -where - 'buf: 'sub, - AtCl: AtatClient, - RST: OutputPin, -{ - fn bind(&mut self, socket: &mut Self::UdpSocket, local_port: u16) -> Result<(), Self::Error> { - if self.connected_to_network().is_err() || self.udp_listener.is_port_bound(local_port) { - return Err(Error::Illegal); - } - - defmt::debug!( - "[UDP] binding socket: {:?} to port: {:?}", - socket, - local_port - ); - - // ID 2 used by UDP server - self.send_internal( - &EdmAtCmdWrapper(ServerConfiguration { - id: 2, - server_config: ServerType::UDP( - local_port, - UDPBehaviour::AutoConnect, - IPVersion::IPv4, - ), - }), - true, - ) - .map_err(|_| Error::Unaddressable)?; - - self.udp_listener - .bind(*socket, local_port) - .map_err(|_| Error::Illegal)?; - - Ok(()) - } - - fn send_to( - &mut self, - socket: &mut Self::UdpSocket, - remote: SocketAddr, - buffer: &[u8], - ) -> nb::Result<(), Self::Error> { - self.spin().map_err(|_| Error::Illegal)?; - // Protect against non server sockets - if !self.udp_listener.is_bound(*socket) { - return Err(Error::Illegal.into()); - } - // Check incomming sockets for the socket address - if let Some(connection_socket) = self.udp_listener.get_outgoing(socket, remote) { - if let Some(ref mut sockets) = self.sockets { - if buffer.is_empty() { - self.close(connection_socket)?; - return Ok(()); - } - - let udp = sockets - .get::>(connection_socket) - .map_err(Self::Error::from)?; - - if !udp.is_open() { - return Err(Error::SocketClosed.into()); - } - - let channel = *self - .socket_map - .socket_to_channel_id(&connection_socket) - .ok_or(nb::Error::WouldBlock)?; - - for chunk in buffer.chunks(EGRESS_CHUNK_SIZE) { - self.send_internal( - &EdmDataCommand { - channel, - data: chunk, - }, - false, - ) - .map_err(|_| nb::Error::Other(Error::Unaddressable))?; - } - self.close(connection_socket).unwrap(); - Ok(()) - } else { - Err(Error::Illegal.into()) - } - } else { - Err(Error::Illegal.into()) - } - - ////// Do with URC - // Crate a new SocketBuffer allocation for the incoming connection - // let mut tcp = self - // .sockets - // .as_mut() - // .ok_or(Error::Illegal)? - // .get::>(data_socket) - // .map_err(Self::Error::from)?; - - // tcp.update_handle(handle); - // tcp.set_state(TcpState::Connected(remote.clone())); - } -}

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