From e96769a72dde1ef719007c33abc91745d3e1af17 Mon Sep 17 00:00:00 2001 From: Mathias Date: Fri, 23 Feb 2024 19:20:17 +0100 Subject: [PATCH 1/7] Simplify initialization of both ppp mode and ublox mode, by providing batteries included new functions that sets up ATAT and all related resources --- Cargo.toml | 48 +- examples/linux.rs | 152 ---- examples/rpi-pico/.cargo/config.toml | 3 +- examples/rpi-pico/.vscode/settings.json | 16 + examples/rpi-pico/Cargo.toml | 60 +- examples/rpi-pico/rust-toolchain.toml | 7 + examples/rpi-pico/src/bin/embassy-async.rs | 90 +- examples/rpi-pico/src/bin/embassy-perf.rs | 3 - .../rpi-pico/src/bin/embassy-smoltcp-ppp.rs | 109 +++ examples/rpi-pico/src/common.rs | 56 -- src/asynch/control.rs | 84 +- src/asynch/internal_stack.rs | 117 +++ src/asynch/mod.rs | 82 +- src/asynch/ppp.rs | 338 +++++++ src/asynch/resources.rs | 60 ++ src/asynch/runner.rs | 115 +-- src/asynch/state.rs | 27 +- src/blocking/client.rs | 859 ------------------ src/blocking/dns.rs | 52 -- src/blocking/mod.rs | 12 - src/blocking/tcp_stack.rs | 227 ----- src/blocking/timer.rs | 42 - src/blocking/tls.rs | 105 --- src/blocking/udp_stack.rs | 393 -------- src/command/data_mode/mod.rs | 4 +- src/command/data_mode/responses.rs | 14 +- src/command/data_mode/urc.rs | 18 +- src/command/edm/urc.rs | 9 + src/command/mod.rs | 4 + src/command/network/mod.rs | 2 +- src/error.rs | 3 + src/lib.rs | 11 +- 32 files changed, 935 insertions(+), 2187 deletions(-) delete mode 100644 examples/linux.rs create mode 100644 examples/rpi-pico/.vscode/settings.json create mode 100644 examples/rpi-pico/rust-toolchain.toml create mode 100644 examples/rpi-pico/src/bin/embassy-smoltcp-ppp.rs delete mode 100644 examples/rpi-pico/src/common.rs create mode 100644 src/asynch/internal_stack.rs create mode 100644 src/asynch/ppp.rs create mode 100644 src/asynch/resources.rs delete mode 100644 src/blocking/client.rs delete mode 100644 src/blocking/dns.rs delete mode 100644 src/blocking/mod.rs delete mode 100644 src/blocking/tcp_stack.rs delete mode 100644 src/blocking/timer.rs delete mode 100644 src/blocking/tls.rs delete mode 100644 src/blocking/udp_stack.rs diff --git a/Cargo.toml b/Cargo.toml index 521c679..d4825d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,38 +15,50 @@ name = "ublox_short_range" doctest = false [dependencies] -atat = { version = "0.21", features = ["derive", "bytes"] } +# atat = { version = "0.21", features = ["derive", "bytes"] } +atat = { path = "../atat/atat", features = ["derive", "bytes"] } + # atat = { git = "https://github.com/BlackbirdHQ/atat", rev = "70283be", features = ["derive", "defmt", "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", features = ["edm"], optional = true } -ublox-sockets = { git = "https://github.com/BlackbirdHQ/ublox-sockets", rev = "9f7fe54", features = ["edm"], optional = true } +ublox-sockets = { git = "https://github.com/BlackbirdHQ/ublox-sockets", rev = "9f7fe54", optional = true } postcard = "1.0.4" -portable-atomic = "1.5" +portable-atomic = "1.6" -defmt = { version = "0.3", optional = true } -log = { version = "0.4.14", optional = true } +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.5" embassy-futures = "0.1" -embassy-net-driver = "0.2" embedded-nal-async = { version = "0.7" } -futures = { version = "0.3.17", default-features = false, features = [ - "async-await", -] } +futures-util = { version = "0.3.29", default-features = false } embedded-io = "0.6" 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 = ["odin_w2xx", "ublox-sockets", "socket-tcp", "socket-udp"] +default = ["socket-tcp", "socket-udp", "odin_w2xx"] + +internal-network-stack = ["dep:ublox-sockets", "edm"] +edm = ["ublox-sockets?/edm"] +# 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"] -std = [] +socket-tcp = ["ublox-sockets?/socket-tcp", "embassy-net?/tcp"] +socket-udp = ["ublox-sockets?/socket-udp", "embassy-net?/udp"] defmt = [ "dep:defmt", @@ -54,7 +66,10 @@ defmt = [ "heapless/defmt-03", "atat/defmt", "ublox-sockets?/defmt", + "embassy-net-ppp?/defmt", + "embassy-net?/defmt", ] +log = ["dep:log", "ublox-sockets?/log", "atat/log"] odin_w2xx = [] nina_w1xx = [] @@ -63,15 +78,6 @@ anna_b1xx = [] nina_b2xx = [] nina_b3xx = [] -socket-tcp = [ - "ublox-sockets?/socket-tcp", - # "smoltcp?/socket-tcp" -] -socket-udp = [ - "ublox-sockets?/socket-udp", - # "smoltcp?/socket-udp" -] - [workspace] members = [] default-members = ["."] @@ -80,4 +86,4 @@ exclude = ["examples"] [patch.crates-io] no-std-net = { git = "https://github.com/rushmorem/no-std-net", branch = "issue-15" } -atat = { path = "../atat/atat" } \ No newline at end of file +atat = { path = "../atat/atat" } 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 index 3217579..f7e22c1 100644 --- a/examples/rpi-pico/.cargo/config.toml +++ b/examples/rpi-pico/.cargo/config.toml @@ -1,6 +1,5 @@ [target.'cfg(all(target_arch = "arm", target_os = "none"))'] -# runner = "probe-rs-cli run --chip RP2040" -runner = "probe-run --chip RP2040" +runner = "probe-rs run --chip RP2040" [build] target = "thumbv6m-none-eabi" 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 index 0839378..196424c 100644 --- a/examples/rpi-pico/Cargo.toml +++ b/examples/rpi-pico/Cargo.toml @@ -5,10 +5,24 @@ edition = "2021" [dependencies] -ublox-short-range-rs = { path = "../../", features = ["odin_w2xx", "ublox-sockets", "socket-tcp"] } -embassy-executor = { version = "0.5", features = ["defmt", "integrated-timers", "nightly"] } -embassy-time = { version = "0.3", features = ["defmt", "defmt-timestamp-uptime"] } -embassy-rp = { version = "0.1.0", features = ["defmt", "unstable-pac", "time-driver"] } +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.5" } +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"] } @@ -19,11 +33,31 @@ 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"] } +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", +] } +embassy-net-ppp = { version = "0.1", optional = true, features = ["defmt"] } +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" } @@ -34,13 +68,15 @@ heapless = "0.8" # embassy-net-driver-channel = { git = "https://github.com/embassy-rs/embassy", rev = "03d6363d5af5dcaf21b52734994a466ca593d2b6" } -embassy-executor = { path = "../../../embassy/embassy-executor" } -embassy-hal-internal = { path = "../../../embassy/embassy-hal-internal" } -embassy-time = { path = "../../../embassy/embassy-time" } -embassy-futures = { path = "../../../embassy/embassy-futures" } -embassy-sync = { path = "../../../embassy/embassy-sync" } -embassy-rp = { path = "../../../embassy/embassy-rp" } -embassy-net-driver = { path = "../../../embassy/embassy-net-driver" } +# embassy-executor = { path = "../../../embassy/embassy-executor" } +# embassy-hal-internal = { path = "../../../embassy/embassy-hal-internal" } +# embassy-time = { path = "../../../embassy/embassy-time" } +# embassy-futures = { path = "../../../embassy/embassy-futures" } +# embassy-sync = { path = "../../../embassy/embassy-sync" } +# embassy-rp = { path = "../../../embassy/embassy-rp" } +# embassy-net-driver = { path = "../../../embassy/embassy-net-driver" } +# embassy-net = { path = "../../../embassy/embassy-net" } +# embassy-net-ppp = { path = "../../../embassy/embassy-net-ppp" } atat = { path = "../../../atat/atat" } ublox-sockets = { path = "../../../ublox-sockets" } no-std-net = { path = "../../../no-std-net" } 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 index 7ef8b7a..81090c4 100644 --- a/examples/rpi-pico/src/bin/embassy-async.rs +++ b/examples/rpi-pico/src/bin/embassy-async.rs @@ -1,18 +1,16 @@ +#![cfg(feature = "internal-network-stack")] #![no_std] #![no_main] #![feature(type_alias_impl_trait)] #![feature(async_fn_in_trait)] #![allow(incomplete_features)] -#[path = "../common.rs"] -mod common; - use core::fmt::Write as _; use embassy_executor::Spawner; use embassy_futures::select::{select, Either}; -use embassy_rp::gpio::{Input, Level, Output, Pull}; +use embassy_rp::gpio::{AnyPin, Input, Level, Output, Pull}; use embassy_rp::peripherals::{PIN_26, UART1}; -use embassy_rp::uart::BufferedInterruptHandler; +use embassy_rp::uart::{BufferedInterruptHandler, BufferedUartTx}; use embassy_rp::{bind_interrupts, uart}; use embassy_time::{Duration, Timer}; use embedded_io_async::Write; @@ -22,25 +20,33 @@ 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, State}; +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 RX_BUF_LEN: usize = 4096; -const URC_CAPACITY: usize = 3; +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>, - RX_BUF_LEN, + INGRESS_BUF_SIZE, >; #[embassy_executor::task] async fn wifi_task( - runner: Runner<'static, AtClient, Output<'static, PIN_26>, 8, URC_CAPACITY>, + runner: InternalRunner< + 'a, + BufferedUartRx<'static, UART1>, + BufferedUartTx<'static, UART1>, + Output<'static, AnyPin>, + INGRESS_BUF_SIZE, + URC_CAPACITY, + >, ) -> ! { runner.run().await } @@ -120,14 +126,6 @@ async fn echo_task( } } -#[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; }); @@ -141,46 +139,50 @@ async fn main(spawner: Spawner) { let rst = Output::new(p.PIN_26, Level::High); let mut btn = Input::new(p.PIN_27, Pull::Up); - let (tx_pin, rx_pin, rts_pin, cts_pin, uart) = - (p.PIN_24, p.PIN_25, p.PIN_23, p.PIN_22, p.UART1); + static TX_BUF: StaticCell<[u8; 16]> = StaticCell::new(); + static RX_BUF: StaticCell<[u8; 16]> = StaticCell::new(); - let tx_buf = &mut make_static!([0u8; 64])[..]; - let rx_buf = &mut make_static!([0u8; 64])[..]; let uart = uart::BufferedUart::new_with_rtscts( - uart, + p.UART1, Irqs, - tx_pin, - rx_pin, - rts_pin, - cts_pin, - tx_buf, - rx_buf, + 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 (rx, tx) = uart.split(); + 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, + ); - let buffers = &*make_static!(atat::Buffers::new()); - let (ingress, client) = buffers.split(tx, EdmDigester::default(), atat::Config::new()); - defmt::unwrap!(spawner.spawn(ingress_task(ingress, rx))); + // Init network stack + static STACK: StaticCell>> = StaticCell::new(); + static STACK_RESOURCES: StaticCell> = StaticCell::new(); - let state = make_static!(State::new(client)); - let (net_device, mut control, runner) = new(state, &buffers.urc_channel, rst).await; + let stack = &*STACK.init(UbloxStack::new( + net_device, + STACK_RESOURCES.init(StackResources::new()), + )); - defmt::unwrap!(spawner.spawn(wifi_task(runner))); + spawner.spawn(net_task(stack)).unwrap(); + spawner.spawn(wifi_task(runner)).unwrap(); control .set_hostname("Factbird-duo-wifi-test") .await .unwrap(); - // Init network stack - let stack = &*make_static!(UbloxStack::new( - net_device, - make_static!(StackResources::<4>::new()), - )); - - defmt::unwrap!(spawner.spawn(net_task(stack))); - // And now we can use it! info!("Device initialized!"); diff --git a/examples/rpi-pico/src/bin/embassy-perf.rs b/examples/rpi-pico/src/bin/embassy-perf.rs index 796fd84..f990655 100644 --- a/examples/rpi-pico/src/bin/embassy-perf.rs +++ b/examples/rpi-pico/src/bin/embassy-perf.rs @@ -4,9 +4,6 @@ #![feature(async_fn_in_trait)] #![allow(incomplete_features)] -#[path = "../common.rs"] -mod common; - use embassy_executor::Spawner; use embassy_futures::join::join; use embassy_rp::gpio::{Level, Output}; 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..f89cdb9 --- /dev/null +++ b/examples/rpi-pico/src/bin/embassy-smoltcp-ppp.rs @@ -0,0 +1,109 @@ +#![cfg(feature = "ppp")] +#![no_std] +#![no_main] +#![feature(type_alias_impl_trait)] + +use defmt::*; +use embassy_executor::Spawner; +use embassy_net::{Stack, StackResources}; +use embassy_rp::gpio::{AnyPin, Level, Output, Pin}; +use embassy_rp::peripherals::UART1; +use embassy_rp::uart::{BufferedInterruptHandler, BufferedUart}; +use embassy_rp::{bind_interrupts, uart}; +use embassy_time::{Duration, Timer}; +use static_cell::StaticCell; +use ublox_short_range::asynch::{PPPRunner, Resources}; +use {defmt_rtt as _, panic_probe as _}; + +const CMD_BUF_SIZE: usize = 128; +const INGRESS_BUF_SIZE: usize = 512; +const URC_CAPACITY: usize = 2; + +#[embassy_executor::task] +async fn net_task(stack: &'static Stack>) -> ! { + stack.run().await +} + +#[embassy_executor::task] +async fn ppp_task( + mut runner: PPPRunner<'static, Output<'static, AnyPin>, INGRESS_BUF_SIZE, URC_CAPACITY>, + interface: BufferedUart<'static, UART1>, + stack: &'static embassy_net::Stack>, +) -> ! { + runner.run(interface, 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 = Output::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 (net_device, mut control, runner) = + ublox_short_range::asynch::new_ppp(RESOURCES.init(Resources::new()), rst); + + // Generate random seed + let seed = 0x0123_4567_89ab_cdef; // chosen by fair dice roll. guarenteed to be random. + + // Init network stack + static STACK: StaticCell>> = StaticCell::new(); + static STACK_RESOURCES: StaticCell> = StaticCell::new(); + + let stack = &*STACK.init(Stack::new( + net_device, + embassy_net::Config::default(), + STACK_RESOURCES.init(StackResources::new()), + seed, + )); + + spawner.spawn(net_task(stack)).unwrap(); + spawner.spawn(ppp_task(runner, wifi_uart, &stack)).unwrap(); + + stack.wait_config_up().await; + + Timer::after(Duration::from_secs(1)).await; + + control + .set_hostname("Factbird-duo-wifi-test") + .await + .unwrap(); + + control.join_open("Test").await; + + // // Then we can use it! + // 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 remote_endpoint = (Ipv4Address::new(93, 184, 216, 34), 12345); + // 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!"); +} diff --git a/examples/rpi-pico/src/common.rs b/examples/rpi-pico/src/common.rs deleted file mode 100644 index e341bb1..0000000 --- a/examples/rpi-pico/src/common.rs +++ /dev/null @@ -1,56 +0,0 @@ -use embassy_rp::uart; -use ublox_short_range::atat; - -// pub struct TxWrap(pub TX); - -// impl embedded_io::Io for TxWrap { -// type Error = ::Error; -// } - -// impl embedded_io::asynch::Write for TxWrap { -// async fn write(&mut self, buf: &[u8]) -> Result { -// self.0.write(buf).await -// } -// } - -// impl atat::UartExt for TxWrap> { -// type Error = (); - -// fn set_baudrate(&mut self, baud: u32) -> Result<(), Self::Error> { -// let r = T::regs(); - -// let clk_base = 125_000_000; - -// let baud_rate_div = (8 * clk_base) / baud; -// let mut baud_ibrd = baud_rate_div >> 7; -// let mut baud_fbrd = ((baud_rate_div & 0x7f) + 1) / 2; - -// if baud_ibrd == 0 { -// baud_ibrd = 1; -// baud_fbrd = 0; -// } else if baud_ibrd >= 65535 { -// baud_ibrd = 65535; -// baud_fbrd = 0; -// } - -// r.uartcr().modify(|m| { -// m.set_uarten(false); -// }); - -// // Load PL011's baud divisor registers -// r.uartibrd() -// .write_value(embassy_rp::pac::uart::regs::Uartibrd(baud_ibrd)); -// r.uartfbrd() -// .write_value(embassy_rp::pac::uart::regs::Uartfbrd(baud_fbrd)); - -// // PL011 needs a (dummy) line control register write to latch in the -// // divisors. We don't want to actually change LCR contents here. -// r.uartlcr_h().modify(|_| {}); - -// r.uartcr().modify(|m| { -// m.set_uarten(true); -// }); - -// Ok(()) -// } -// } diff --git a/src/asynch/control.rs b/src/asynch/control.rs index e826bf5..fd658c6 100644 --- a/src/asynch/control.rs +++ b/src/asynch/control.rs @@ -36,59 +36,9 @@ impl<'a, AT: AtatClient> Control<'a, AT> { Self { state_ch, at } } - pub(crate) async fn init(&mut self) -> Result<(), Error> { - debug!("Initalizing ublox control"); - // read MAC addr. - // let mut resp = self.at.send_edm(GetWifiMac).await?; - // self.state_ch.set_ethernet_address( - // hex::from_hex(resp.mac_addr.as_mut_slice()) - // .unwrap() - // .try_into() - // .unwrap(), - // ); - - // let country = countries::WORLD_WIDE_XX; - // let country_info = CountryInfo { - // country_abbrev: [country.code[0], country.code[1], 0, 0], - // country_code: [country.code[0], country.code[1], 0, 0], - // rev: if country.rev == 0 { - // -1 - // } else { - // country.rev as _ - // }, - // }; - // self.set_iovar("country", &country_info.to_bytes()).await; - - // // set country takes some time, next ioctls fail if we don't wait. - // Timer::after(Duration::from_millis(100)).await; - - // // Set antenna to chip antenna - // self.ioctl_set_u32(IOCTL_CMD_ANTDIV, 0, 0).await; - - // self.set_iovar_u32("bus:txglom", 0).await; - // Timer::after(Duration::from_millis(100)).await; - // //self.set_iovar_u32("apsta", 1).await; // this crashes, also we already did it before...?? - // //Timer::after(Duration::from_millis(100)).await; - // self.set_iovar_u32("ampdu_ba_wsize", 8).await; - // Timer::after(Duration::from_millis(100)).await; - // self.set_iovar_u32("ampdu_mpdu", 4).await; - // Timer::after(Duration::from_millis(100)).await; - // //self.set_iovar_u32("ampdu_rx_factor", 0).await; // this crashes - - // // set wifi up - // self.ioctl(ControlType::Set, IOCTL_CMD_UP, 0, &mut []).await; - - // Timer::after(Duration::from_millis(100)).await; - - // self.ioctl_set_u32(110, 0, 1).await; // SET_GMODE = auto - // self.ioctl_set_u32(142, 0, 0).await; // SET_BAND = any - - Ok(()) - } - pub async fn set_hostname(&mut self, hostname: &str) -> Result<(), Error> { self.at - .send_edm(SetNetworkHostName { + .send(SetNetworkHostName { host_name: hostname, }) .await?; @@ -98,7 +48,7 @@ impl<'a, AT: AtatClient> Control<'a, AT> { async fn get_wifi_status(&mut self) -> Result { match self .at - .send_edm(GetWifiStatus { + .send(GetWifiStatus { status_id: StatusId::Status, }) .await? @@ -112,7 +62,7 @@ impl<'a, AT: AtatClient> Control<'a, AT> { async fn get_connected_ssid(&mut self) -> Result, Error> { match self .at - .send_edm(GetWifiStatus { + .send(GetWifiStatus { status_id: StatusId::SSID, }) .await? @@ -135,14 +85,14 @@ impl<'a, AT: AtatClient> Control<'a, AT> { } self.at - .send_edm(SetWifiStationConfig { + .send(SetWifiStationConfig { config_id: CONFIG_ID, config_param: WifiStationConfig::ActiveOnStartup(OnOff::Off), }) .await?; self.at - .send_edm(SetWifiStationConfig { + .send(SetWifiStationConfig { config_id: CONFIG_ID, config_param: WifiStationConfig::SSID( heapless::String::try_from(ssid).map_err(|_| Error::Overflow)?, @@ -151,14 +101,14 @@ impl<'a, AT: AtatClient> Control<'a, AT> { .await?; self.at - .send_edm(SetWifiStationConfig { + .send(SetWifiStationConfig { config_id: CONFIG_ID, config_param: WifiStationConfig::Authentication(Authentication::Open), }) .await?; self.at - .send_edm(ExecWifiStationAction { + .send(ExecWifiStationAction { config_id: CONFIG_ID, action: WifiStationAction::Activate, }) @@ -183,21 +133,21 @@ impl<'a, AT: AtatClient> Control<'a, AT> { } self.at - .send_edm(ExecWifiStationAction { + .send(ExecWifiStationAction { config_id: CONFIG_ID, action: WifiStationAction::Reset, }) .await?; self.at - .send_edm(SetWifiStationConfig { + .send(SetWifiStationConfig { config_id: CONFIG_ID, config_param: WifiStationConfig::ActiveOnStartup(OnOff::Off), }) .await?; self.at - .send_edm(SetWifiStationConfig { + .send(SetWifiStationConfig { config_id: CONFIG_ID, config_param: WifiStationConfig::SSID( heapless::String::try_from(ssid).map_err(|_| Error::Overflow)?, @@ -206,14 +156,14 @@ impl<'a, AT: AtatClient> Control<'a, AT> { .await?; self.at - .send_edm(SetWifiStationConfig { + .send(SetWifiStationConfig { config_id: CONFIG_ID, config_param: WifiStationConfig::Authentication(Authentication::WpaWpa2Psk), }) .await?; self.at - .send_edm(SetWifiStationConfig { + .send(SetWifiStationConfig { config_id: CONFIG_ID, config_param: WifiStationConfig::WpaPskOrPassphrase( heapless::String::try_from(passphrase).map_err(|_| Error::Overflow)?, @@ -222,7 +172,7 @@ impl<'a, AT: AtatClient> Control<'a, AT> { .await?; self.at - .send_edm(ExecWifiStationAction { + .send(ExecWifiStationAction { config_id: CONFIG_ID, action: WifiStationAction::Activate, }) @@ -240,7 +190,7 @@ impl<'a, AT: AtatClient> Control<'a, AT> { WifiStatusVal::Disabled => {} WifiStatusVal::Disconnected | WifiStatusVal::Connected => { self.at - .send_edm(ExecWifiStationAction { + .send(ExecWifiStationAction { config_id: CONFIG_ID, action: WifiStationAction::Deactivate, }) @@ -277,7 +227,7 @@ impl<'a, AT: AtatClient> Control<'a, AT> { } pub async fn gpio_set(&mut self, id: GPIOId, value: GPIOValue) -> Result<(), Error> { - self.at.send_edm(WriteGPIO { id, value }).await?; + self.at.send(WriteGPIO { id, value }).await?; Ok(()) } @@ -294,7 +244,7 @@ impl<'a, AT: AtatClient> Control<'a, AT> { info!("Importing {:?} bytes as {:?}", data.len(), name); self.at - .send_edm(PrepareSecurityDataImport { + .send(PrepareSecurityDataImport { data_type, data_size: data.len(), internal_name: name, @@ -304,7 +254,7 @@ impl<'a, AT: AtatClient> Control<'a, AT> { let import_data = self .at - .send_edm(SendSecurityDataImport { + .send(SendSecurityDataImport { data: atat::serde_bytes::Bytes::new(data), }) .await?; diff --git a/src/asynch/internal_stack.rs b/src/asynch/internal_stack.rs new file mode 100644 index 0000000..02cdbac --- /dev/null +++ b/src/asynch/internal_stack.rs @@ -0,0 +1,117 @@ +// pub mod ublox_stack; + +use core::mem::MaybeUninit; + +use atat::{asynch::Client, AtatIngress}; +use embassy_sync::{blocking_mutex::raw::NoopRawMutex, mutex::Mutex}; +use embedded_hal::digital::OutputPin; +use embedded_io_async::{Read, Write}; + +use crate::command::custom_digest::EdmDigester; + +pub use super::resources::UbxResources as Resources; + +use super::{control::Control, runner::Runner, state, AtHandle, UbloxUrc}; + +pub struct InternalRunner< + 'a, + R: Read, + W: Write, + RST: OutputPin, + const INGRESS_BUF_SIZE: usize, + const URC_CAPACITY: usize, +> { + pub cellular_runner: Runner<'a, Client<'a, W, INGRESS_BUF_SIZE>, RST, URC_CAPACITY>, + pub ingress: atat::Ingress<'a, EdmDigester, UbloxUrc, INGRESS_BUF_SIZE, URC_CAPACITY, 2>, + pub reader: R, +} + +impl< + 'a, + R: Read, + W: Write, + RST: OutputPin, + const INGRESS_BUF_SIZE: usize, + const URC_CAPACITY: usize, + > InternalRunner<'a, R, W, RST, INGRESS_BUF_SIZE, URC_CAPACITY> +{ + pub async fn run(&mut self) -> ! { + self.cellular_runner.init().await.unwrap(); + + embassy_futures::join::join( + self.ingress.read_from(&mut self.reader), + self.cellular_runner.run(), + ) + .await; + core::unreachable!() + } +} + +pub fn new_internal< + 'a, + R: Read, + W: Write, + RST: OutputPin, + const CMD_BUF_SIZE: usize, + const INGRESS_BUF_SIZE: usize, + const URC_CAPACITY: usize, +>( + reader: R, + writer: W, + resources: &'a mut Resources, + reset: RST, +) -> ( + state::Device<'a, Client<'a, W, INGRESS_BUF_SIZE>, URC_CAPACITY>, + Control<'a, Client<'a, W, INGRESS_BUF_SIZE>>, + InternalRunner<'a, R, W, RST, INGRESS_BUF_SIZE, URC_CAPACITY>, +) { + // safety: this is a self-referential struct, however: + // - it can't move while the `'a` borrow is active. + // - when the borrow ends, the dangling references inside the MaybeUninit will never be used again. + let at_client_uninit: *mut MaybeUninit>> = + (&mut resources.at_client + as *mut MaybeUninit>>) + .cast(); + + unsafe { &mut *at_client_uninit }.write(Mutex::new(Client::new( + writer, + &resources.res_slot, + &mut resources.cmd_buf, + atat::Config::default(), + ))); + + let at_client = unsafe { (&*at_client_uninit).assume_init_ref() }; + + let (ch_runner, net_device) = state::new( + &mut resources.ch, + AtHandle(at_client), + resources.urc_channel.subscribe().unwrap(), + ); + + let control = Control::new(ch_runner.state_runner(), AtHandle(at_client)); + + let runner = Runner::new( + ch_runner, + AtHandle(at_client), + reset, + resources.urc_channel.subscribe().unwrap(), + ); + + // runner.init().await.unwrap(); + // control.init().await.unwrap(); + + let ingress = atat::Ingress::new( + EdmDigester, + &mut resources.ingress_buf, + &resources.res_slot, + &resources.urc_channel, + ); + + let runner = InternalRunner { + cellular_runner: runner, + ingress, + reader, + }; + + (net_device, control, runner) +} diff --git a/src/asynch/mod.rs b/src/asynch/mod.rs index 2e6776b..de8313c 100644 --- a/src/asynch/mod.rs +++ b/src/asynch/mod.rs @@ -1,78 +1,48 @@ pub mod control; +mod resources; pub mod runner; #[cfg(feature = "ublox-sockets")] pub mod ublox_stack; pub(crate) mod state; -use crate::command::edm::{urc::EdmEvent, EdmAtCmdWrapper}; +#[cfg(feature = "internal-network-stack")] +mod internal_stack; +#[cfg(feature = "internal-network-stack")] +pub use internal_stack::{new_internal, InternalRunner, Resources}; + +#[cfg(feature = "ppp")] +mod ppp; +#[cfg(feature = "ppp")] +pub use ppp::{new_ppp, PPPRunner, Resources}; + use atat::asynch::AtatClient; use embassy_sync::{blocking_mutex::raw::NoopRawMutex, mutex::Mutex}; -use embedded_hal::digital::OutputPin; -use runner::Runner; -use state::Device; -use self::control::Control; +#[cfg(feature = "edm")] +pub type UbloxUrc = crate::command::edm::urc::EdmEvent; -// NOTE: Must be pow(2) due to internal usage of `FnvIndexMap` -const MAX_CONNS: usize = 8; +#[cfg(not(feature = "edm"))] +pub type UbloxUrc = crate::command::Urc; pub struct AtHandle<'d, AT: AtatClient>(&'d Mutex); impl<'d, AT: AtatClient> AtHandle<'d, AT> { - async fn send_edm( - &mut self, - cmd: Cmd, - ) -> Result { - self.send(EdmAtCmdWrapper(cmd)).await + #[cfg(feature = "edm")] + async fn send(&mut self, cmd: Cmd) -> Result { + self.send_raw(crate::command::edm::EdmAtCmdWrapper(cmd)) + .await } + #[cfg(not(feature = "edm"))] async fn send(&mut self, cmd: Cmd) -> Result { - self.0.lock().await.send_retry::(&cmd).await + self.send_raw(cmd).await } -} - -pub struct State { - ch: state::State, - at_handle: Mutex, -} -impl State { - pub fn new(at_handle: AT) -> Self { - Self { - ch: state::State::new(), - at_handle: Mutex::new(at_handle), - } + async fn send_raw( + &mut self, + cmd: Cmd, + ) -> Result { + self.0.lock().await.send_retry::(&cmd).await } } - -pub async fn new<'a, AT: AtatClient, RST: OutputPin, const URC_CAPACITY: usize>( - state: &'a mut State, - subscriber: &'a atat::UrcChannel, - reset: RST, -) -> ( - Device<'a, AT, URC_CAPACITY>, - Control<'a, AT>, - Runner<'a, AT, RST, MAX_CONNS, URC_CAPACITY>, -) { - let (ch_runner, net_device) = state::new( - &mut state.ch, - AtHandle(&state.at_handle), - subscriber.subscribe().unwrap(), - ); - let state_ch = ch_runner.state_runner(); - - let mut runner = Runner::new( - ch_runner, - AtHandle(&state.at_handle), - reset, - subscriber.subscribe().unwrap(), - ); - - runner.init().await.unwrap(); - - let mut control = Control::new(state_ch, AtHandle(&state.at_handle)); - control.init().await.unwrap(); - - (net_device, control, runner) -} diff --git a/src/asynch/ppp.rs b/src/asynch/ppp.rs new file mode 100644 index 0000000..dfd3588 --- /dev/null +++ b/src/asynch/ppp.rs @@ -0,0 +1,338 @@ +use core::mem::MaybeUninit; + +use atat::{ + asynch::{AtatClient, Client, SimpleClient}, + AtatIngress, +}; +use embassy_futures::select::Either; +use embassy_net::{ + udp::{PacketMetadata, UdpSocket}, + IpEndpoint, Ipv4Address, +}; +use embassy_sync::{ + blocking_mutex::raw::NoopRawMutex, + mutex::Mutex, + pipe::{Reader, Writer}, +}; +use embassy_time::{Duration, Instant, Timer}; +use embedded_hal::digital::OutputPin; +use embedded_io_async::{BufRead, Read, Write}; + +use crate::command::{ + data_mode::{self, ChangeMode}, + system::{self, SetEcho}, +}; + +use super::{control::Control, resources::UbxResources, runner::Runner, state, AtHandle, UbloxUrc}; + +const PPP_AT_PORT: u16 = 23; +pub const SOCKET_BUF_SIZE: usize = 128; + +pub type Resources< + 'a, + const CMD_BUF_SIZE: usize, + const INGRESS_BUF_SIZE: usize, + const URC_CAPACITY: usize, +> = UbxResources< + Writer<'a, NoopRawMutex, SOCKET_BUF_SIZE>, + CMD_BUF_SIZE, + INGRESS_BUF_SIZE, + URC_CAPACITY, +>; + +pub fn new_ppp< + 'a, + RST: OutputPin, + const CMD_BUF_SIZE: usize, + const INGRESS_BUF_SIZE: usize, + const URC_CAPACITY: usize, +>( + resources: &'a mut Resources<'a, CMD_BUF_SIZE, INGRESS_BUF_SIZE, URC_CAPACITY>, + reset: RST, +) -> ( + embassy_net_ppp::Device<'a>, + Control<'a, Client<'a, Writer<'a, NoopRawMutex, SOCKET_BUF_SIZE>, INGRESS_BUF_SIZE>>, + PPPRunner<'a, RST, INGRESS_BUF_SIZE, URC_CAPACITY>, +) { + let ch_runner = state::new_ppp(&mut resources.ch); + let state_ch = ch_runner.state_runner(); + + let (control_rx_reader, control_rx_writer) = resources.control_rx.split(); + let (control_tx_reader, control_tx_writer) = resources.control_tx.split(); + + // safety: this is a self-referential struct, however: + // - it can't move while the `'a` borrow is active. + // - when the borrow ends, the dangling references inside the MaybeUninit will never be used again. + let at_client_uninit: *mut MaybeUninit< + Mutex< + NoopRawMutex, + Client<'a, Writer<'a, NoopRawMutex, SOCKET_BUF_SIZE>, INGRESS_BUF_SIZE>, + >, + > = (&mut resources.at_client + as *mut MaybeUninit< + Mutex< + NoopRawMutex, + Client<'static, Writer<'a, NoopRawMutex, SOCKET_BUF_SIZE>, INGRESS_BUF_SIZE>, + >, + >) + .cast(); + + unsafe { &mut *at_client_uninit }.write(Mutex::new(Client::new( + control_tx_writer, + &resources.res_slot, + &mut resources.cmd_buf, + atat::Config::default(), + ))); + + let at_client = unsafe { (&*at_client_uninit).assume_init_ref() }; + + let wifi_runner = Runner::new( + ch_runner, + AtHandle(at_client), + reset, + resources.urc_channel.subscribe().unwrap(), + ); + + let ingress = atat::Ingress::new( + atat::AtDigester::::new(), + &mut resources.ingress_buf, + &resources.res_slot, + &resources.urc_channel, + ); + + let control = Control::new(state_ch, AtHandle(at_client)); + + let (net_device, ppp_runner) = embassy_net_ppp::new(&mut resources.ppp_state); + + let runner = PPPRunner { + ppp_runner, + wifi_runner, + ingress, + control_rx_reader, + control_rx_writer, + control_tx_reader, + }; + + (net_device, control, runner) +} + +pub struct PPPRunner<'a, RST: OutputPin, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> { + pub ppp_runner: embassy_net_ppp::Runner<'a>, + pub wifi_runner: Runner< + 'a, + Client<'a, Writer<'a, NoopRawMutex, SOCKET_BUF_SIZE>, INGRESS_BUF_SIZE>, + RST, + URC_CAPACITY, + >, + pub ingress: + atat::Ingress<'a, atat::AtDigester, UbloxUrc, INGRESS_BUF_SIZE, URC_CAPACITY, 2>, + pub control_rx_reader: Reader<'a, NoopRawMutex, SOCKET_BUF_SIZE>, + pub control_rx_writer: Writer<'a, NoopRawMutex, SOCKET_BUF_SIZE>, + pub control_tx_reader: Reader<'a, NoopRawMutex, SOCKET_BUF_SIZE>, +} + +impl<'a, RST: OutputPin, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> + PPPRunner<'a, RST, INGRESS_BUF_SIZE, URC_CAPACITY> +{ + async fn configure(at_client: &mut A) -> Result<(), atat::Error> { + let _ = at_client + .send(&ChangeMode { + mode: data_mode::types::Mode::CommandMode, + }) + .await; + + at_client + .send(&SetEcho { + on: system::types::EchoOn::Off, + }) + .await?; + + // Initialize `ublox` module to desired baudrate + at_client + .send(&system::SetRS232Settings { + baud_rate: system::types::BaudRate::B115200, + flow_control: system::types::FlowControl::On, + data_bits: 8, + stop_bits: system::types::StopBits::One, + parity: system::types::Parity::None, + change_after_confirm: system::types::ChangeAfterConfirm::ChangeAfterOK, + }) + .await?; + + Ok(()) + } + + pub async fn run( + &mut self, + mut iface: RW, + stack: &embassy_net::Stack>, + ) -> ! { + // self.wifi_runner.init().await.unwrap(); + // Timer::after(Duration::from_secs(4)).await; + + loop { + // Reset modem + self.wifi_runner.reset().await; + + Timer::after(Duration::from_secs(1)).await; + + let control_fut = async { + stack.wait_config_up().await; + + let mut rx_meta = [PacketMetadata::EMPTY; 1]; + let mut tx_meta = [PacketMetadata::EMPTY; 1]; + let mut socket_rx_buf = [0u8; 32]; + let mut socket_tx_buf = [0u8; 32]; + let mut socket = UdpSocket::new( + stack, + &mut rx_meta, + &mut socket_rx_buf, + &mut tx_meta, + &mut socket_tx_buf, + ); + + let endpoint = stack.config_v4().unwrap(); + + info!("Socket bound!"); + socket + .bind((endpoint.address.address(), PPP_AT_PORT)) + .unwrap(); + + let mut tx_buf = [0u8; 32]; + let mut rx_buf = [0u8; 32]; + + loop { + match embassy_futures::select::select( + self.control_tx_reader.read(&mut tx_buf), + socket.recv_from(&mut rx_buf), + ) + .await + { + Either::First(n) => { + socket + .send_to( + &tx_buf[..n], + (Ipv4Address::new(172, 30, 0, 251), PPP_AT_PORT), + ) + .await + .unwrap(); + } + Either::Second(Ok((n, _))) => { + self.control_rx_writer + .write_all(&rx_buf[..n]) + .await + .unwrap(); + } + Either::Second(_) => {} + } + } + }; + + let ppp_fut = async { + let mut fails = 0; + let mut last_start = None; + + loop { + if let Some(last_start) = last_start { + Timer::at(last_start + Duration::from_secs(10)).await; + // Do not attempt to start too fast. + + // If was up stably for at least 1 min, reset fail counter. + if Instant::now() > last_start + Duration::from_secs(60) { + fails = 0; + } else { + fails += 1; + if fails == 10 { + warn!("modem: PPP failed too much, rebooting modem."); + break; + } + } + } + last_start = Some(Instant::now()); + + let mut buf = [0u8; 64]; + let mut at_client = SimpleClient::new( + &mut iface, + atat::AtDigester::::new(), + &mut buf, + atat::Config::default(), + ); + + if let Err(e) = Self::configure(&mut at_client).await { + warn!("modem: configure failed {:?}", e); + continue; + } + + Timer::after(Duration::from_secs(2)).await; + + // Send AT command `ATO3` to enter PPP mode + let res = at_client + .send(&ChangeMode { + mode: data_mode::types::Mode::PPPMode, + }) + .await; + + if let Err(e) = res { + warn!("ppp dial failed {:?}", e); + continue; + } + + drop(at_client); + + // Drain the UART + let _ = embassy_time::with_timeout(Duration::from_secs(2), async { + loop { + iface.read(&mut buf).await.ok(); + } + }) + .await; + + info!("RUNNING PPP"); + let config = embassy_net_ppp::Config { + username: b"", + password: b"", + }; + let res = self + .ppp_runner + .run(&mut iface, config, |ipv4| { + let Some(addr) = ipv4.address else { + warn!("PPP did not provide an IP address."); + return; + }; + let mut dns_servers = heapless::Vec::new(); + for s in ipv4.dns_servers.iter().flatten() { + let _ = + dns_servers.push(embassy_net::Ipv4Address::from_bytes(&s.0)); + } + let config = + embassy_net::ConfigV4::Static(embassy_net::StaticConfigV4 { + address: embassy_net::Ipv4Cidr::new( + embassy_net::Ipv4Address::from_bytes(&addr.0), + 0, + ), + gateway: None, + dns_servers, + }); + + stack.set_config_v4(config); + }) + .await; + + info!("ppp failed"); + } + }; + + let ingress_fut = async { + self.ingress.read_from(&mut self.control_rx_reader).await; + }; + + embassy_futures::select::select4( + ppp_fut, + ingress_fut, + control_fut, + self.wifi_runner.run(), + ) + .await; + } + } +} diff --git a/src/asynch/resources.rs b/src/asynch/resources.rs new file mode 100644 index 0000000..d55fb2f --- /dev/null +++ b/src/asynch/resources.rs @@ -0,0 +1,60 @@ +use core::mem::MaybeUninit; + +use atat::{asynch::Client, ResponseSlot, UrcChannel}; +use embassy_sync::{blocking_mutex::raw::NoopRawMutex, mutex::Mutex}; +use embedded_io_async::Write; + +use super::{state, UbloxUrc}; + +pub struct UbxResources< + W: Write, + const CMD_BUF_SIZE: usize, + const INGRESS_BUF_SIZE: usize, + const URC_CAPACITY: usize, +> { + pub(crate) ch: state::State, + + pub(crate) res_slot: ResponseSlot, + pub(crate) urc_channel: UrcChannel, + pub(crate) cmd_buf: [u8; CMD_BUF_SIZE], + pub(crate) ingress_buf: [u8; INGRESS_BUF_SIZE], + + pub(crate) at_client: MaybeUninit>>, + + #[cfg(feature = "ppp")] + pub(crate) ppp_state: embassy_net_ppp::State<2, 2>, + + #[cfg(feature = "ppp")] + pub(crate) control_rx: embassy_sync::pipe::Pipe, + #[cfg(feature = "ppp")] + pub(crate) control_tx: embassy_sync::pipe::Pipe, +} + +impl< + W: Write, + const CMD_BUF_SIZE: usize, + const INGRESS_BUF_SIZE: usize, + const URC_CAPACITY: usize, + > UbxResources +{ + pub fn new() -> Self { + Self { + ch: state::State::new(), + + res_slot: ResponseSlot::new(), + urc_channel: UrcChannel::new(), + cmd_buf: [0; CMD_BUF_SIZE], + ingress_buf: [0; INGRESS_BUF_SIZE], + + at_client: MaybeUninit::uninit(), + + #[cfg(feature = "ppp")] + ppp_state: embassy_net_ppp::State::new(), + + #[cfg(feature = "ppp")] + control_rx: embassy_sync::pipe::Pipe::new(), + #[cfg(feature = "ppp")] + control_tx: embassy_sync::pipe::Pipe::new(), + } + } +} diff --git a/src/asynch/runner.rs b/src/asynch/runner.rs index bab1368..fb0be9d 100644 --- a/src/asynch/runner.rs +++ b/src/asynch/runner.rs @@ -1,9 +1,13 @@ use core::str::FromStr; -use super::state::{self, LinkState}; +use super::{ + state::{self, LinkState}, + UbloxUrc, +}; +#[cfg(feature = "edm")] +use crate::command::edm::SwitchToEdmCommand; use crate::{ command::{ - edm::{urc::EdmEvent, SwitchToEdmCommand}, general::SoftwareVersion, network::{ responses::NetworkStatusResponse, @@ -12,8 +16,8 @@ use crate::{ GetNetworkStatus, }, system::{ - types::{BaudRate, ChangeAfterConfirm, EchoOn, FlowControl, Parity, StopBits}, - RebootDCE, SetEcho, SetRS232Settings, StoreCurrentConfig, + types::{BaudRate, ChangeAfterConfirm, FlowControl, Parity, StopBits}, + RebootDCE, SetRS232Settings, StoreCurrentConfig, }, wifi::{ types::DisconnectReason, @@ -35,19 +39,12 @@ use super::AtHandle; /// Background runner for the Ublox Module. /// /// You must call `.run()` in a background task for the Ublox Module to operate. -pub struct Runner< - 'd, - AT: AtatClient, - RST: OutputPin, - const MAX_CONNS: usize, - const URC_CAPACITY: usize, -> { +pub struct Runner<'d, AT: AtatClient, RST: OutputPin, const URC_CAPACITY: usize> { ch: state::Runner<'d>, at: AtHandle<'d, AT>, reset: RST, wifi_connection: Option, - // connections: FnvIndexMap, - urc_subscription: UrcSubscription<'d, EdmEvent, URC_CAPACITY, 2>, + urc_subscription: UrcSubscription<'d, UbloxUrc, URC_CAPACITY, 2>, } impl< @@ -55,15 +52,14 @@ impl< AT: AtatClient, // AT: AtatClient + atat::UartExt, RST: OutputPin, - const MAX_CONNS: usize, const URC_CAPACITY: usize, - > Runner<'d, AT, RST, MAX_CONNS, URC_CAPACITY> + > Runner<'d, AT, RST, URC_CAPACITY> { pub(crate) fn new( ch: state::Runner<'d>, at: AtHandle<'d, AT>, reset: RST, - urc_subscription: UrcSubscription<'d, EdmEvent, URC_CAPACITY, 2>, + urc_subscription: UrcSubscription<'d, UbloxUrc, URC_CAPACITY, 2>, ) -> Self { Self { ch, @@ -71,7 +67,6 @@ impl< reset, wifi_connection: None, urc_subscription, - // connections: IndexMap::new(), } } @@ -89,10 +84,9 @@ impl< // parameter. Instead, the // parameter must be set to 0 and the serial settings will take effect // when the module is reset. - let baud_rate = BaudRate::B115200; self.at - .send_edm(SetRS232Settings { - baud_rate, + .send(SetRS232Settings { + baud_rate: BaudRate::B115200, flow_control: FlowControl::On, data_bits: 8, stop_bits: StopBits::One, @@ -103,12 +97,12 @@ impl< self.restart(true).await?; - self.at.send_edm(SoftwareVersion).await?; + self.at.send(SoftwareVersion).await?; // Move to control // if let Some(size) = self.config.tls_in_buffer_size { // self.at - // .send_edm(SetPeerConfiguration { + // .send(SetPeerConfiguration { // parameter: PeerConfigParameter::TlsInBuffer(size), // }) // .await?; @@ -116,7 +110,7 @@ impl< // if let Some(size) = self.config.tls_out_buffer_size { // self.at - // .send_edm(SetPeerConfiguration { + // .send(SetPeerConfiguration { // parameter: PeerConfigParameter::TlsOutBuffer(size), // }) // .await?; @@ -128,8 +122,15 @@ impl< async fn wait_startup(&mut self, timeout: Duration) -> Result<(), Error> { let fut = async { loop { - match self.urc_subscription.next_message_pure().await { - EdmEvent::ATEvent(Urc::StartUp) => return, + let event = self.urc_subscription.next_message_pure().await; + + #[cfg(feature = "edm")] + let Some(event) = event.extract_urc() else { + continue; + }; + + match event { + Urc::StartUp => return, _ => {} } } @@ -146,6 +147,7 @@ impl< self.wait_startup(Duration::from_secs(4)).await?; + #[cfg(feature = "edm")] self.enter_edm(Duration::from_secs(4)).await?; Ok(()) @@ -154,19 +156,21 @@ impl< pub async fn restart(&mut self, store: bool) -> Result<(), Error> { warn!("Soft resetting Ublox Short Range"); if store { - self.at.send_edm(StoreCurrentConfig).await?; + self.at.send(StoreCurrentConfig).await?; } - self.at.send_edm(RebootDCE).await?; + self.at.send(RebootDCE).await?; self.wait_startup(Duration::from_secs(10)).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"); @@ -189,7 +193,11 @@ impl< .await .map_err(|_| Error::Timeout)?; - self.at.send_edm(SetEcho { on: EchoOn::On }).await?; + self.at + .send(crate::command::system::SetEcho { + on: crate::command::system::types::EchoOn::Off, + }) + .await?; Ok(()) } @@ -210,20 +218,26 @@ impl< Ok(link_state == LinkState::Up) } - pub async fn run(mut self) -> ! { + pub async fn run(&mut self) -> ! { loop { let wait_link_up = { let event = self.urc_subscription.next_message_pure().await; + + #[cfg(feature = "edm")] + let Some(event) = event.extract_urc() else { + continue; + }; + match event { - EdmEvent::ATEvent(Urc::StartUp) => { + Urc::StartUp => { error!("AT startup event?! Device restarted unintentionally!"); false } - EdmEvent::ATEvent(Urc::WifiLinkConnected(WifiLinkConnected { + Urc::WifiLinkConnected(WifiLinkConnected { connection_id: _, bssid, channel, - })) => { + }) => { if let Some(ref mut con) = self.wifi_connection { con.wifi_state = WiFiState::Connected; con.network.bssid = bssid; @@ -241,10 +255,7 @@ impl< } true } - EdmEvent::ATEvent(Urc::WifiLinkDisconnected(WifiLinkDisconnected { - reason, - .. - })) => { + Urc::WifiLinkDisconnected(WifiLinkDisconnected { reason, .. }) => { if let Some(ref mut con) = self.wifi_connection { match reason { DisconnectReason::NetworkDisabled => { @@ -262,27 +273,23 @@ impl< true } - EdmEvent::ATEvent(Urc::WifiAPUp(_)) => todo!(), - EdmEvent::ATEvent(Urc::WifiAPDown(_)) => todo!(), - EdmEvent::ATEvent(Urc::WifiAPStationConnected(_)) => todo!(), - EdmEvent::ATEvent(Urc::WifiAPStationDisconnected(_)) => todo!(), - EdmEvent::ATEvent(Urc::EthernetLinkUp(_)) => todo!(), - EdmEvent::ATEvent(Urc::EthernetLinkDown(_)) => todo!(), - EdmEvent::ATEvent(Urc::NetworkUp(NetworkUp { interface_id })) => { + Urc::WifiAPUp(_) => todo!(), + Urc::WifiAPDown(_) => todo!(), + Urc::WifiAPStationConnected(_) => todo!(), + Urc::WifiAPStationDisconnected(_) => todo!(), + Urc::EthernetLinkUp(_) => todo!(), + Urc::EthernetLinkDown(_) => todo!(), + Urc::NetworkUp(NetworkUp { interface_id }) => { drop(event); - self.network_status_callback(interface_id).await.unwrap(); + self.network_status_callback(interface_id).await.ok(); true } - EdmEvent::ATEvent(Urc::NetworkDown(NetworkDown { interface_id })) => { + Urc::NetworkDown(NetworkDown { interface_id }) => { drop(event); - self.network_status_callback(interface_id).await.unwrap(); + self.network_status_callback(interface_id).await.ok(); true } - EdmEvent::ATEvent(Urc::NetworkError(_)) => todo!(), - EdmEvent::StartUp => { - error!("EDM startup event?! Device restarted unintentionally!"); - false - } + Urc::NetworkError(_) => todo!(), _ => false, } }; @@ -299,7 +306,7 @@ impl< .. } = self .at - .send_edm(GetNetworkStatus { + .send(GetNetworkStatus { interface_id, status: NetworkStatusParameter::InterfaceType, }) @@ -313,7 +320,7 @@ impl< .. } = self .at - .send_edm(GetNetworkStatus { + .send(GetNetworkStatus { interface_id, status: NetworkStatusParameter::Gateway, }) @@ -333,7 +340,7 @@ impl< .. } = self .at - .send_edm(GetNetworkStatus { + .send(GetNetworkStatus { interface_id, status: NetworkStatusParameter::IPv6LinkLocalAddress, }) diff --git a/src/asynch/state.rs b/src/asynch/state.rs index 7b45214..816e1ac 100644 --- a/src/asynch/state.rs +++ b/src/asynch/state.rs @@ -10,8 +10,6 @@ use embassy_sync::blocking_mutex::raw::NoopRawMutex; use embassy_sync::blocking_mutex::Mutex; use embassy_sync::waitqueue::WakerRegistration; -use crate::command::edm::urc::EdmEvent; - /// The link state of a network device. #[derive(PartialEq, Eq, Clone, Copy)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] @@ -22,7 +20,7 @@ pub enum LinkState { Up, } -use super::AtHandle; +use super::{AtHandle, UbloxUrc}; pub struct State { inner: MaybeUninit, @@ -92,7 +90,7 @@ impl<'d> StateRunner<'d> { pub fn new<'d, AT: AtatClient, const URC_CAPACITY: usize>( state: &'d mut State, at: AtHandle<'d, AT>, - urc_subscription: UrcSubscription<'d, EdmEvent, URC_CAPACITY, 2>, + urc_subscription: UrcSubscription<'d, UbloxUrc, URC_CAPACITY, 2>, ) -> (Runner<'d>, Device<'d, AT, URC_CAPACITY>) { // safety: this is a self-referential struct, however: // - it can't move while the `'d` borrow is active. @@ -121,6 +119,25 @@ pub fn new<'d, AT: AtatClient, const URC_CAPACITY: usize>( ) } +pub fn new_ppp<'d>(state: &'d mut State) -> Runner<'d> { + // safety: this is a self-referential struct, however: + // - it can't move while the `'d` borrow is active. + // - when the borrow ends, the dangling references inside the MaybeUninit will never be used again. + let state_uninit: *mut MaybeUninit = + (&mut state.inner as *mut MaybeUninit).cast(); + + let state = unsafe { &mut *state_uninit }.write(StateInner { + shared: Mutex::new(RefCell::new(Shared { + link_state: LinkState::Down, + waker: WakerRegistration::new(), + })), + }); + + Runner { + shared: &state.shared, + } +} + pub struct TestShared<'d> { inner: &'d Mutex>, } @@ -128,7 +145,7 @@ pub struct TestShared<'d> { pub struct Device<'d, AT: AtatClient, const URC_CAPACITY: usize> { pub(crate) shared: TestShared<'d>, pub(crate) at: AtHandle<'d, AT>, - pub(crate) urc_subscription: UrcSubscription<'d, EdmEvent, URC_CAPACITY, 2>, + pub(crate) urc_subscription: UrcSubscription<'d, UbloxUrc, URC_CAPACITY, 2>, } impl<'d> TestShared<'d> { diff --git a/src/blocking/client.rs b/src/blocking/client.rs deleted file mode 100644 index 762d279..0000000 --- a/src/blocking/client.rs +++ /dev/null @@ -1,859 +0,0 @@ -use core::str::FromStr; - -use crate::{ - blocking::timer::Timer, - command::{ - 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::{ - types::{DisconnectReason, WifiConfig}, - SetWifiConfig, - }, - Urc, - }, - config::Config, - error::Error, - wifi::{ - connection::{NetworkState, WiFiState, WifiConnection}, - network::{WifiMode, WifiNetwork}, - supplicant::Supplicant, - SocketMap, - }, -}; -use defmt::{debug, error, trace}; -use embassy_time::Duration; -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, Copy, Clone)] -pub enum SerialMode { - Cmd, - ExtendedData, -} - -#[derive(PartialEq, Copy, Clone)] -pub enum DNSState { - NotResolving, - Resolving, - Resolved(IpAddr), - Error(PingError), -} - -#[derive(PartialEq, 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<'a, const N: usize, const L: usize>( - sockets: &'a 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 struct UbloxClient -where - C: atat::blocking::AtatClient, - RST: OutputPin, -{ - pub(crate) module_started: bool, - pub(crate) initialized: bool, - serial_mode: SerialMode, - pub(crate) wifi_connection: Option, - pub(crate) wifi_config_active_on_startup: Option, - pub(crate) client: C, - pub(crate) config: Config, - pub(crate) sockets: Option<&'static mut SocketSet>, - pub(crate) dns_state: DNSState, - pub(crate) urc_attempts: u8, - pub(crate) security_credentials: SecurityCredentials, - pub(crate) socket_map: SocketMap, - pub(crate) udp_listener: UdpListener<4, N>, -} - -impl UbloxClient -where - C: atat::blocking::AtatClient, - RST: OutputPin, -{ - pub fn new(client: C, config: Config) -> Self { - UbloxClient { - module_started: false, - initialized: false, - serial_mode: SerialMode::Cmd, - wifi_connection: None, - wifi_config_active_on_startup: None, - client, - config, - sockets: None, - dns_state: DNSState::NotResolving, - urc_attempts: 0, - security_credentials: SecurityCredentials::default(), - socket_map: SocketMap::default(), - udp_listener: UdpListener::new(), - } - } - - 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(); - Timer::after(Duration::from_millis(100)).wait(); - while self.handle_urc()? {} - } - - // TODO: handle EDM settings quirk see EDM datasheet: 2.2.5.1 AT Request Serial settings - self.send_internal( - &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, - }), - false, - )?; - - 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(); - Timer::after(Duration::from_millis(100)).wait(); - while self.handle_urc()? {} - } - - if self.firmware_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.initialized = true; - self.supplicant::<10>()?.init()?; - - Ok(()) - } - - pub fn firmware_version(&mut self) -> Result { - let response = self.send_internal(&EdmAtCmdWrapper(SoftwareVersion), false)?; - Ok(response.version) - } - - 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.dns_state = DNSState::NotResolving; - self.urc_attempts = 0; - 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 { - warn!("Hard resetting Ublox Short Range"); - pin.set_low().ok(); - Timer::after(Duration::from_millis(50)).wait(); - pin.set_high().ok(); - - Timer::with_timeout(Duration::from_secs(4), || { - self.handle_urc().ok(); - if self.module_started { - Some(Ok::<(), ()>(())) - } else { - None - } - }) - .map_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.dns_state = DNSState::NotResolving; - self.urc_attempts = 0; - self.security_credentials = SecurityCredentials::default(); - self.socket_map = SocketMap::default(); - self.udp_listener = UdpListener::new(); - - warn!("Soft resetting Ublox Short Range"); - self.send_internal(&EdmAtCmdWrapper(RebootDCE), false)?; - self.clear_buffers()?; - - Timer::with_timeout(Duration::from_secs(4), || { - self.handle_urc().ok(); - if self.module_started { - Some(Ok::<(), ()>(())) - } else { - None - } - }) - .map_err(|_| Error::Timeout)?; - - Ok(()) - } - - pub(crate) fn clear_buffers(&mut self) -> Result<(), Error> { - // self.client.reset(); deprecated - - if let Some(ref mut sockets) = self.sockets.as_deref_mut() { - sockets.prune(); - } - - // Allow ATAT some time to clear the buffers - Timer::after(Duration::from_millis(300)).wait(); - - Ok(()) - } - - pub fn spin(&mut self) -> Result<(), Error> { - if !self.initialized { - return Err(Error::Uninitialized); - } - - while self.handle_urc()? {} - - self.connected_to_network()?; - - // TODO: Is this smart? - // if let Some(ref mut sockets) = self.sockets.as_deref_mut() { - // sockets.recycle(self.timer.now()); - // } - - 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 { - let mut ran = false; - let socket_set = self.sockets.as_deref_mut(); - let dns_state = &mut self.dns_state; - let socket_map = &mut self.socket_map; - let udp_listener = &mut self.udp_listener; - let wifi_connection = &mut self.wifi_connection; - - let mut a = self.urc_attempts; - let max = self.config.max_urc_attempts; - - self.client.try_read_urc_with::(|edm_urc, _| { - ran = true; - let res = 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; - true - } - 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) = socket_set { - 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) = udp_listener.incoming(event.local_port) { - trace!("[UDP Server] Server socket incomming"); - let mut handled = true; - 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() { - handled = false; - } - } - 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"); - handled = false - } - if sockets.add(new_socket).map_err(|_| { - error!("[UDP_URC] Opening socket Error: Socket set full"); - Error::SocketMemory - }).is_err(){ - handled = false; - } - - if socket_map.insert_peer(peer_handle, socket_handle).map_err(|_| { - error!("[UDP_URC] Opening socket Error: Socket Map full"); - Error::SocketMapMemory - }).is_err(){ - handled = false; - } - debug!( - "[URC] Binding remote {=[u8]:a} to UDP server on port: {:?} with handle: {:?}", - event.remote_address.as_slice(), - event.local_port, - socket_handle - ); - if queue.enqueue((socket_handle, remote)).is_err(){ - handled = false - } - handled - } 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; - // } - } - } - true - } - } else { - true - } - } - Urc::PeerDisconnected(msg) => { - debug!("[URC] PeerDisconnected"); - if let Some(sockets) = socket_set { - if let Some(handle) = 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(); - } - _ => {} - } - socket_map.remove_peer(&msg.handle).unwrap(); - } - } - true - } - Urc::WifiLinkConnected(msg) => { - debug!("[URC] WifiLinkConnected"); - if let Some(ref mut con) = wifi_connection { - con.wifi_state = WiFiState::Connected; - con.network.bssid = msg.bssid; - con.network.channel = msg.channel; - } else { - debug!("[URC] Active network config discovered"); - 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, - 255, - ).activate() - ); - } - true - } - Urc::WifiLinkDisconnected(msg) => { - debug!("[URC] WifiLinkDisconnected"); - if let Some(con) = wifi_connection { - match msg.reason { - DisconnectReason::NetworkDisabled => { - con.wifi_state = WiFiState::Inactive; - } - DisconnectReason::SecurityProblems => { - error!("Wifi Security Problems"); - } - _ => { - con.wifi_state = WiFiState::NotConnected; - } - } - } - true - } - Urc::WifiAPUp(_) => { - debug!("[URC] WifiAPUp"); - true - } - Urc::WifiAPDown(_) => { - debug!("[URC] WifiAPDown"); - true - } - Urc::WifiAPStationConnected(client) => { - debug!( - "[URC] WifiAPStationConnected {=[u8]:a}", - client.mac_addr.into_inner() - ); - true - } - Urc::WifiAPStationDisconnected(_) => { - debug!("[URC] WifiAPStationDisconnected"); - true - } - Urc::EthernetLinkUp(_) => { - debug!("[URC] EthernetLinkUp"); - true - } - Urc::EthernetLinkDown(_) => { - debug!("[URC] EthernetLinkDown"); - true - } - Urc::NetworkUp(_) => { - debug!("[URC] NetworkUp"); - if let Some(con) = 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; - } - } - true - } - Urc::NetworkDown(_) => { - debug!("[URC] NetworkDown"); - if let Some(con) = wifi_connection { - con.network_state = NetworkState::Unattached; - } - true - } - Urc::NetworkError(_) => { - debug!("[URC] NetworkError"); - true - } - Urc::PingResponse(resp) => { - debug!("[URC] PingResponse"); - if *dns_state == DNSState::Resolving { - *dns_state = DNSState::Resolved(resp.ip) - } - true - } - Urc::PingErrorResponse(resp) => { - debug!("[URC] PingErrorResponse: {:?}", resp.error); - if *dns_state == DNSState::Resolving { - *dns_state = DNSState::Error(resp.error) - } - true - } - } - } // end match urc - EdmEvent::StartUp => { - debug!("[EDM_URC] STARTUP"); - self.module_started = true; - self.serial_mode = SerialMode::ExtendedData; - true - } - EdmEvent::IPv4ConnectEvent(event) => { - debug!( - "[EDM_URC] IPv4ConnectEvent! Channel_id: {:?}", - event.channel_id - ); - - if let Some(sockets) = socket_set { - let endpoint = SocketAddr::new(event.remote_ip.into(), event.remote_port); - - // This depends upon Connected AT-URC to arrive first. - if let Some(queue) = udp_listener.incoming(event.local_port) { - if let Some((socket_handle, _ )) = queue.into_iter().find(|(_, remote)| remote == &endpoint) { - socket_map.insert_channel(event.channel_id, *socket_handle).is_ok() - } else { - false - } - } else { - sockets - .iter_mut() - .find_map(|(h, s)| { - match event.protocol { - Protocol::TCP => { - let mut tcp = TcpSocket::downcast(s).ok()?; - if tcp.endpoint() == Some(endpoint) { - socket_map.insert_channel(event.channel_id, h).unwrap(); - tcp.set_state(TcpState::Connected(endpoint)); - return Some(true); - } - } - Protocol::UDP => { - let mut udp = UdpSocket::downcast(s).ok()?; - if udp.endpoint() == Some(endpoint) { - socket_map.insert_channel(event.channel_id, h).unwrap(); - udp.set_state(UdpState::Established); - return Some(true); - } - } - _ => {} - } - None - }) - .is_some() - } - } else { - true - } - } - EdmEvent::IPv6ConnectEvent(event) => { - debug!( - "[EDM_URC] IPv6ConnectEvent! Channel_id: {:?}", - event.channel_id - ); - - if let Some(sockets) = socket_set { - let endpoint = SocketAddr::new(event.remote_ip.into(), event.remote_port); - - // This depends upon Connected AT-URC to arrive first. - if let Some(queue) = udp_listener.incoming(event.local_port) { - if let Some((socket_handle, _ )) = queue.into_iter().find(|(_, remote)| remote == &endpoint) { - socket_map.insert_channel(event.channel_id, *socket_handle).is_ok() - } else { - false - } - } else { - sockets - .iter_mut() - .find_map(|(h, s)| { - match event.protocol { - Protocol::TCP => { - let mut tcp = TcpSocket::downcast(s).ok()?; - if tcp.endpoint() == Some(endpoint) { - socket_map.insert_channel(event.channel_id, h).unwrap(); - tcp.set_state(TcpState::Connected(endpoint)); - return Some(true); - } - } - Protocol::UDP => { - let mut udp = UdpSocket::downcast(s).ok()?; - if udp.endpoint() == Some(endpoint) { - socket_map.insert_channel(event.channel_id, h).unwrap(); - udp.set_state(UdpState::Established); - return Some(true); - } - } - _ => {} - } - None - }) - .is_some() - } - } else { - true - } - } - EdmEvent::BluetoothConnectEvent(_) => { - debug!("[EDM_URC] BluetoothConnectEvent"); - true - } - EdmEvent::DisconnectEvent(channel_id) => { - debug!("[EDM_URC] DisconnectEvent! Channel_id: {:?}", channel_id); - socket_map.remove_channel(&channel_id).ok(); - true - } - EdmEvent::DataEvent(event) => { - debug!("[EDM_URC] DataEvent! Channel_id: {:?}", event.channel_id); - if let Some(sockets) = socket_set { - if !event.data.is_empty() { - if let Some(socket_handle) = - 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); - true - } else { - false - } - } - Some(SocketType::Udp) => { - // Handle udp socket - let mut udp = sockets - .get::>(*socket_handle) - .unwrap(); - - if udp.can_recv() { - udp.rx_enqueue_slice(&event.data); - true - } else { - false - } - } - _ => { - error!("SocketNotFound {:?}", socket_handle); - false - } - } - } else { - false - } - } else { - false - } - } else { - true - } - } - }; // end match edm-urc - if !res { - if a < max { - error!("[EDM_URC] URC handeling failed"); - a += 1; - return false; - } - error!("[EDM_URC] URC thrown away"); - } - a = 0; - true - }); - self.urc_attempts = a; - Ok(ran) - } - - /// 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 { - self.init()?; - } - match self.serial_mode { - SerialMode::ExtendedData => self.send_internal(&EdmAtCmdWrapper(cmd), true), - SerialMode::Cmd => self.send_internal(&cmd, true), - } - } - - pub fn supplicant(&mut self) -> Result, Error> { - // TODO: better solution - if !self.initialized { - return Err(Error::Uninitialized); - } - - Ok(Supplicant { - client: &mut self.client, - wifi_connection: &mut self.wifi_connection, - active_on_startup: &mut self.wifi_config_active_on_startup, - }) - } - /// Is the module attached to a WiFi and ready to open sockets - pub 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/src/blocking/dns.rs b/src/blocking/dns.rs deleted file mode 100644 index 48b9904..0000000 --- a/src/blocking/dns.rs +++ /dev/null @@ -1,52 +0,0 @@ -use super::client::{DNSState, UbloxClient}; -use super::timer; -use embassy_time::Duration; -use embedded_hal::digital::OutputPin; -use embedded_nal::{nb, AddrType, Dns, IpAddr}; -use heapless::String; -use ublox_sockets::Error; - -use crate::{blocking::timer::Timer, command::ping::*}; - -impl Dns for UbloxClient -where - C: atat::blocking::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 { - debug!("Lookup hostname: {}", hostname); - self.send_at(Ping { - hostname, - retry_num: 1, - }) - .map_err(|_| nb::Error::Other(Error::Unaddressable))?; - - self.dns_state = DNSState::Resolving; - - match Timer::with_timeout(Duration::from_secs(8), || { - if self.spin().is_err() { - return Some(Err(Error::Illegal)); - } - - match self.dns_state { - DNSState::Resolving => None, - DNSState::Resolved(ip) => Some(Ok(ip)), - _ => Some(Err(Error::Illegal)), - } - }) { - Ok(ip) => Ok(ip), - Err(timer::Error::Timeout) => Err(nb::Error::Other(Error::Timeout)), - Err(timer::Error::Other(e)) => Err(nb::Error::Other(e)), - } - } -} diff --git a/src/blocking/mod.rs b/src/blocking/mod.rs deleted file mode 100644 index 208e4a5..0000000 --- a/src/blocking/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -pub(crate) mod client; -mod dns; -pub mod timer; -pub mod tls; - -#[cfg(feature = "socket-udp")] -pub mod udp_stack; - -#[cfg(feature = "socket-tcp")] -pub mod tcp_stack; - -pub use client::UbloxClient; diff --git a/src/blocking/tcp_stack.rs b/src/blocking/tcp_stack.rs deleted file mode 100644 index fb72ce3..0000000 --- a/src/blocking/tcp_stack.rs +++ /dev/null @@ -1,227 +0,0 @@ -use super::{client::new_socket_num, UbloxClient}; -use crate::{ - command::data_mode::*, - command::edm::{EdmAtCmdWrapper, EdmDataCommand}, - wifi::peer_builder::PeerUrlBuilder, -}; -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 crate::wifi::EGRESS_CHUNK_SIZE; - -impl TcpClientStack for UbloxClient -where - C: atat::blocking::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); - } - } - - debug!("[TCP] Opening socket"); - - let socket_id = new_socket_num(sockets).unwrap(); - sockets.add(TcpSocket::new(socket_id)).map_err(|e| { - 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()); - } - - debug!("[TCP] Connect socket"); - self.connected_to_network().map_err(|_| Error::Illegal)?; - - let url = PeerUrlBuilder::new() - .address(&remote) - .creds(self.security_credentials.clone()) - .tcp() - .map_err(|_| Error::Unaddressable)?; - - // 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)); - } - } - - trace!("[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 { - self.connected_to_network().map_err(|_| Error::Illegal)?; - 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(|e| nb::Error::Other(e.into()))?; - - 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 { - 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 { - 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/src/blocking/timer.rs b/src/blocking/timer.rs deleted file mode 100644 index 7ddae99..0000000 --- a/src/blocking/timer.rs +++ /dev/null @@ -1,42 +0,0 @@ -use embassy_time::{Duration, Instant}; - -pub struct Timer { - expires_at: Instant, -} - -pub enum Error { - Timeout, - Other(E), -} - -impl Timer { - pub fn after(duration: Duration) -> Self { - Self { - expires_at: Instant::now() + duration, - } - } - - pub fn with_timeout(timeout: Duration, mut e: F) -> Result> - where - F: FnMut() -> Option>, - { - let timer = Timer::after(timeout); - - loop { - if let Some(res) = e() { - return res.map_err(Error::Other); - } - if timer.expires_at <= Instant::now() { - return Err(Error::Timeout); - } - } - } - - pub fn wait(self) { - loop { - if self.expires_at <= Instant::now() { - break; - } - } - } -} diff --git a/src/blocking/tls.rs b/src/blocking/tls.rs deleted file mode 100644 index ed5687f..0000000 --- a/src/blocking/tls.rs +++ /dev/null @@ -1,105 +0,0 @@ -use super::UbloxClient; -use crate::{ - command::edm::BigEdmAtCmdWrapper, - command::security::{types::*, *}, - error::Error, -}; -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 TLS for UbloxClient -where - C: 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/src/blocking/udp_stack.rs b/src/blocking/udp_stack.rs deleted file mode 100644 index 795bc93..0000000 --- a/src/blocking/udp_stack.rs +++ /dev/null @@ -1,393 +0,0 @@ -use super::client::new_socket_num; -use super::UbloxClient; -use crate::{ - command::data_mode::*, - command::{ - data_mode::types::{IPVersion, ServerType, UDPBehaviour}, - edm::{EdmAtCmdWrapper, EdmDataCommand}, - }, - wifi::peer_builder::PeerUrlBuilder, -}; -use embedded_hal::digital::OutputPin; -use embedded_nal::{nb, SocketAddr, UdpFullStack}; - -use embedded_nal::UdpClientStack; -use ublox_sockets::{Error, SocketHandle, UdpSocket, UdpState}; - -use crate::wifi::EGRESS_CHUNK_SIZE; - -impl UdpClientStack for UbloxClient -where - C: atat::blocking::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(); - debug!("[UDP] Opening socket"); - sockets.add(UdpSocket::new(socket_id)).map_err(|_| { - error!("[UDP] Opening socket Error: Socket set full"); - Error::SocketSetFull - }) - } else { - 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> { - if self.sockets.is_none() { - error!("[UDP] Connecting socket Error: Missing socket set"); - return Err(Error::Illegal); - } - let url = PeerUrlBuilder::new() - .address(&remote) - .udp() - .map_err(|_| Error::Unaddressable)?; - 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) => self - .socket_map - .insert_peer(resp.peer_handle.into(), *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 - { - self.spin().map_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.clone())) - } 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) { - 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() { - 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) { - debug!( - "[UDP] Closing incomming socket for Server: {:?}", - connection_handle - ); - self.close(connection_handle)?; - } else { - 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(|_| { - error!( - "[UDP] Closing socket error: No server socket matching: {:?}", - socket - ); - Error::Illegal - }) - // Handle normal sockets - } else if let Some(ref mut sockets) = self.sockets { - debug!("[UDP] Closing socket: {:?}", socket); - // If no sockets exists, nothing to close. - if let Ok(ref mut udp) = sockets.get::>(socket) { - trace!("[UDP] Closing socket state: {:?}", udp.state()); - match udp.state() { - UdpState::Closed => { - sockets.remove(socket).ok(); - } - UdpState::Established => { - // FIXME: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 { - 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 UdpFullStack for UbloxClient -where - C: atat::blocking::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); - } - - 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.len() == 0 { - 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())); - } -} diff --git a/src/command/data_mode/mod.rs b/src/command/data_mode/mod.rs index fa9da8d..616b03d 100644 --- a/src/command/data_mode/mod.rs +++ b/src/command/data_mode/mod.rs @@ -7,7 +7,6 @@ use atat::atat_derive::AtatCmd; use heapless::String; use responses::*; use types::*; -use ublox_sockets::PeerHandle; use super::NoResponse; @@ -28,6 +27,7 @@ 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 = "ublox-sockets")] #[derive(Clone, AtatCmd)] #[at_cmd("+UDCP", ConnectPeerResponse, timeout_ms = 5000)] pub struct ConnectPeer<'a> { @@ -38,6 +38,7 @@ pub struct ConnectPeer<'a> { /// 5.3 Close peer connection +UDCPC /// /// Closes an existing peer connection. +#[cfg(feature = "ublox-sockets")] #[derive(Clone, AtatCmd)] #[at_cmd("+UDCPC", NoResponse, timeout_ms = 1000)] pub struct ClosePeerConnection { @@ -65,6 +66,7 @@ pub struct SetDefaultRemotePeer<'a> { /// 5.5 Peer list +UDLP /// /// This command reads the connected peers (peer handle). +#[cfg(feature = "ublox-sockets")] #[derive(Clone, AtatCmd)] #[at_cmd("+UDLP?", PeerListResponse, timeout_ms = 1000)] pub struct PeerList; diff --git a/src/command/data_mode/responses.rs b/src/command/data_mode/responses.rs index 0c8832c..03354b0 100644 --- a/src/command/data_mode/responses.rs +++ b/src/command/data_mode/responses.rs @@ -1,26 +1,26 @@ //! Responses for Data Mode use atat::atat_derive::AtatResp; -use heapless::String; -use ublox_sockets::PeerHandle; /// 5.2 Connect peer +UDCP +#[cfg(feature = "ublox-sockets")] #[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 = "ublox-sockets")] #[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/src/command/data_mode/urc.rs b/src/command/data_mode/urc.rs index 8eb2441..4ec7f75 100644 --- a/src/command/data_mode/urc.rs +++ b/src/command/data_mode/urc.rs @@ -1,15 +1,14 @@ //! Unsolicited responses for Data mode Commands +#[allow(unused_imports)] use super::types::*; -use atat::atat_derive::AtatResp; -use atat::heapless_bytes::Bytes; -use ublox_sockets::PeerHandle; /// 5.10 Peer connected +UUDPC -#[derive(Debug, PartialEq, Clone, AtatResp)] +#[cfg(feature = "ublox-sockets")] +#[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)] @@ -18,21 +17,22 @@ pub struct PeerConnected { // pub local_address: IpAddr, #[at_arg(position = 3)] #[cfg_attr(feature = "defmt", defmt(Debug2Format))] - pub local_address: Bytes<40>, + 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)] #[cfg_attr(feature = "defmt", defmt(Debug2Format))] - pub remote_address: Bytes<40>, + 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 = "ublox-sockets")] +#[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/src/command/edm/urc.rs b/src/command/edm/urc.rs index 7d382d8..4b78503 100644 --- a/src/command/edm/urc.rs +++ b/src/command/edm/urc.rs @@ -20,6 +20,15 @@ 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 = Self; diff --git a/src/command/mod.rs b/src/command/mod.rs index 083b282..6b2b680 100644 --- a/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; @@ -28,9 +30,11 @@ pub enum Urc { #[at_urc("+STARTUP")] StartUp, /// 5.10 Peer connected +UUDPC + #[cfg(feature = "ublox-sockets")] #[at_urc("+UUDPC")] PeerConnected(data_mode::urc::PeerConnected), /// 5.11 Peer disconnected +UUDPD + #[cfg(feature = "ublox-sockets")] #[at_urc("+UUDPD")] PeerDisconnected(data_mode::urc::PeerDisconnected), /// 7.15 Wi-Fi Link connected +UUWLE diff --git a/src/command/network/mod.rs b/src/command/network/mod.rs index c80b06e..c8e401b 100644 --- a/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, timeout_ms = 3000)] pub struct GetNetworkStatus { #[at_arg(position = 0)] pub interface_id: u8, diff --git a/src/error.rs b/src/error.rs index 5c4d426..c770c9b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "ublox-sockets")] pub use ublox_sockets::Error as SocketError; #[derive(Debug)] @@ -17,6 +18,7 @@ pub enum Error { // NetworkState(crate::wifi::connection::NetworkState), NoWifiSetup, // WifiState(crate::wifi::connection::WiFiState), + #[cfg(feature = "ublox-sockets")] Socket(ublox_sockets::Error), AT(atat::Error), Busy, @@ -40,6 +42,7 @@ impl From for Error { } } +#[cfg(feature = "ublox-sockets")] impl From for Error { fn from(e: ublox_sockets::Error) -> Self { Error::Socket(e) diff --git a/src/lib.rs b/src/lib.rs index c1adaa5..180e4fb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,19 +1,17 @@ -#![cfg_attr(all(not(test), not(feature = "std")), no_std)] +#![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."); + mod fmt; pub mod asynch; -pub use embedded_nal_async; - -pub use ublox_sockets; - mod connection; mod network; mod peer_builder; -// mod blocking; mod hex; pub use atat; @@ -29,7 +27,6 @@ pub use peer_builder::SecurityCredentials; // - Network scan // - AP Mode (control) // - TCP listener stack -// - (Blocking client?) // - // // FIXME: From 27ed97ea6c36df2199e67b8c4f3063e5c6423cb7 Mon Sep 17 00:00:00 2001 From: Mathias Date: Fri, 31 May 2024 14:45:50 +0200 Subject: [PATCH 2/7] Refactor async completely for a more intuitive API. URCs over PPP UDP socket is still not working properly --- .vscode/settings.json | 4 + Cargo.toml | 11 +- examples/rpi-pico/Cargo.toml | 26 +- .../rpi-pico/src/bin/embassy-smoltcp-ppp.rs | 84 ++- src/asynch/at_udp_socket.rs | 60 ++ src/asynch/control.rs | 174 ++++-- src/asynch/internal_stack.rs | 117 ---- src/asynch/mod.rs | 54 +- src/asynch/network.rs | 292 +++++++++ src/asynch/ppp.rs | 338 ----------- src/asynch/resources.rs | 47 +- src/asynch/runner.rs | 562 ++++++++---------- src/asynch/state.rs | 157 ++--- src/asynch/ublox_stack/tcp.rs | 2 +- src/asynch/ublox_stack/tls.rs | 2 +- src/command/custom_digest.rs | 14 - src/command/network/mod.rs | 2 +- src/command/system/mod.rs | 2 +- src/config.rs | 17 + src/connection.rs | 24 +- src/lib.rs | 24 +- 21 files changed, 955 insertions(+), 1058 deletions(-) create mode 100644 src/asynch/at_udp_socket.rs delete mode 100644 src/asynch/internal_stack.rs create mode 100644 src/asynch/network.rs delete mode 100644 src/asynch/ppp.rs create mode 100644 src/config.rs diff --git a/.vscode/settings.json b/.vscode/settings.json index bd3b577..b297366 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,6 +6,10 @@ "rust-analyzer.cargo.target": "thumbv6m-none-eabi", "rust-analyzer.check.allTargets": false, "rust-analyzer.linkedProjects": [], + "rust-analyzer.cargo.features": [ + "odin_w2xx", + "ppp" + ], "rust-analyzer.server.extraEnv": { "WIFI_NETWORK": "foo", "WIFI_PASSWORD": "foo", diff --git a/Cargo.toml b/Cargo.toml index d4825d0..c8b21f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,16 +15,13 @@ name = "ublox_short_range" doctest = false [dependencies] -# atat = { version = "0.21", features = ["derive", "bytes"] } -atat = { path = "../atat/atat", features = ["derive", "bytes"] } +atat = { version = "0.22", features = ["derive", "bytes"] } -# atat = { git = "https://github.com/BlackbirdHQ/atat", rev = "70283be", features = ["derive", "defmt", "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", features = ["edm"], optional = true } ublox-sockets = { git = "https://github.com/BlackbirdHQ/ublox-sockets", rev = "9f7fe54", optional = true } -postcard = "1.0.4" portable-atomic = "1.6" log = { version = "^0.4", default-features = false, optional = true } @@ -38,7 +35,6 @@ embassy-futures = "0.1" embedded-nal-async = { version = "0.7" } futures-util = { version = "0.3.29", default-features = false } -embedded-io = "0.6" embedded-io-async = "0.6" embassy-net-ppp = { version = "0.1", optional = true } @@ -49,7 +45,7 @@ embassy-net = { version = "0.4", features = [ [features] -default = ["socket-tcp", "socket-udp", "odin_w2xx"] +default = ["socket-tcp", "socket-udp"] internal-network-stack = ["dep:ublox-sockets", "edm"] edm = ["ublox-sockets?/edm"] @@ -62,7 +58,6 @@ socket-udp = ["ublox-sockets?/socket-udp", "embassy-net?/udp"] defmt = [ "dep:defmt", - "postcard/use-defmt", "heapless/defmt-03", "atat/defmt", "ublox-sockets?/defmt", @@ -86,4 +81,4 @@ exclude = ["examples"] [patch.crates-io] no-std-net = { git = "https://github.com/rushmorem/no-std-net", branch = "issue-15" } -atat = { path = "../atat/atat" } +atat = { path = "../atat/atat" } \ No newline at end of file diff --git a/examples/rpi-pico/Cargo.toml b/examples/rpi-pico/Cargo.toml index 196424c..a07f340 100644 --- a/examples/rpi-pico/Cargo.toml +++ b/examples/rpi-pico/Cargo.toml @@ -49,11 +49,14 @@ embassy-net = { version = "0.4", optional = true, features = [ "medium-ip", "tcp", "udp", + "dns" ] } embassy-net-ppp = { version = "0.1", optional = true, features = ["defmt"] } -embedded-tls = { path = "../../../embedded-tls", default-features = false, 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"] @@ -68,16 +71,13 @@ ppp = ["dep:embassy-net", "dep:embassy-net-ppp", "ublox-short-range-rs/ppp"] # embassy-net-driver-channel = { git = "https://github.com/embassy-rs/embassy", rev = "03d6363d5af5dcaf21b52734994a466ca593d2b6" } -# embassy-executor = { path = "../../../embassy/embassy-executor" } -# embassy-hal-internal = { path = "../../../embassy/embassy-hal-internal" } -# embassy-time = { path = "../../../embassy/embassy-time" } -# embassy-futures = { path = "../../../embassy/embassy-futures" } -# embassy-sync = { path = "../../../embassy/embassy-sync" } -# embassy-rp = { path = "../../../embassy/embassy-rp" } -# embassy-net-driver = { path = "../../../embassy/embassy-net-driver" } -# embassy-net = { path = "../../../embassy/embassy-net" } -# embassy-net-ppp = { path = "../../../embassy/embassy-net-ppp" } -atat = { path = "../../../atat/atat" } +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" } diff --git a/examples/rpi-pico/src/bin/embassy-smoltcp-ppp.rs b/examples/rpi-pico/src/bin/embassy-smoltcp-ppp.rs index f89cdb9..2991638 100644 --- a/examples/rpi-pico/src/bin/embassy-smoltcp-ppp.rs +++ b/examples/rpi-pico/src/bin/embassy-smoltcp-ppp.rs @@ -1,16 +1,30 @@ -#![cfg(feature = "ppp")] #![no_std] #![no_main] #![feature(type_alias_impl_trait)] +#[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::{Stack, StackResources}; +use embassy_net::tcp::TcpSocket; +use embassy_net::{Ipv4Address, Stack, StackResources}; use embassy_rp::gpio::{AnyPin, Level, Output, Pin}; use embassy_rp::peripherals::UART1; use embassy_rp::uart::{BufferedInterruptHandler, BufferedUart}; use embassy_rp::{bind_interrupts, uart}; use embassy_time::{Duration, Timer}; +use embedded_tls::Aes128GcmSha256; +use embedded_tls::TlsConfig; +use embedded_tls::TlsConnection; +use embedded_tls::TlsContext; +use embedded_tls::UnsecureProvider; +use rand_chacha::rand_core::SeedableRng; +use rand_chacha::ChaCha8Rng; +use reqwless::headers::ContentType; +use reqwless::request::Request; +use reqwless::request::RequestBuilder as _; +use reqwless::response::Response; use static_cell::StaticCell; use ublox_short_range::asynch::{PPPRunner, Resources}; use {defmt_rtt as _, panic_probe as _}; @@ -26,7 +40,7 @@ async fn net_task(stack: &'static Stack>) -> ! #[embassy_executor::task] async fn ppp_task( - mut runner: PPPRunner<'static, Output<'static, AnyPin>, INGRESS_BUF_SIZE, URC_CAPACITY>, + mut runner: PPPRunner<'static, Output<'static>, INGRESS_BUF_SIZE, URC_CAPACITY>, interface: BufferedUart<'static, UART1>, stack: &'static embassy_net::Stack>, ) -> ! { @@ -68,7 +82,7 @@ async fn main(spawner: Spawner) { // Init network stack static STACK: StaticCell>> = StaticCell::new(); - static STACK_RESOURCES: StaticCell> = StaticCell::new(); + static STACK_RESOURCES: StaticCell> = StaticCell::new(); let stack = &*STACK.init(Stack::new( net_device, @@ -91,19 +105,51 @@ async fn main(spawner: Spawner) { control.join_open("Test").await; - // // Then we can use it! - // 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 remote_endpoint = (Ipv4Address::new(93, 184, 216, 34), 12345); - // 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!"); + info!("We have network!"); + + let mut rx_buffer = [0; 4096]; + let mut tx_buffer = [0; 4096]; + let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); + socket.set_timeout(Some(Duration::from_secs(10))); + + let hostname = "ecdsa-test.germancoding.com"; + + let mut remote = stack + .dns_query(hostname, smoltcp::wire::DnsQueryType::A) + .await + .unwrap(); + let remote_endpoint = (remote.pop().unwrap(), 443); + info!("connecting to {:?}...", remote_endpoint); + let r = socket.connect(remote_endpoint).await; + if let Err(e) = r { + warn!("connect error: {:?}", e); + return; + } + info!("TCP connected!"); + + let mut read_record_buffer = [0; 16384]; + let mut write_record_buffer = [0; 16384]; + let config = TlsConfig::new().with_server_name(hostname); + let mut tls = TlsConnection::new(socket, &mut read_record_buffer, &mut write_record_buffer); + + tls.open(TlsContext::new( + &config, + UnsecureProvider::new::(ChaCha8Rng::seed_from_u64(seed)), + )) + .await + .expect("error establishing TLS connection"); + + info!("TLS Established!"); + + let request = Request::get("/") + .host(hostname) + .content_type(ContentType::TextPlain) + .build(); + request.write(&mut tls).await.unwrap(); + + let mut rx_buf = [0; 4096]; + let response = Response::read(&mut tls, reqwless::request::Method::GET, &mut rx_buf) + .await + .unwrap(); + info!("{=[u8]:a}", rx_buf); } diff --git a/src/asynch/at_udp_socket.rs b/src/asynch/at_udp_socket.rs new file mode 100644 index 0000000..6dd777d --- /dev/null +++ b/src/asynch/at_udp_socket.rs @@ -0,0 +1,60 @@ +use embassy_net::{udp::UdpSocket, Ipv4Address}; +use embedded_io_async::{Read, Write}; + +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(); + debug!("READ {} bytes: {=[u8]:a}", len, &buf[..len]); + 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> 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(); + debug!("READ {} bytes: {=[u8]:a}", len, &buf[..len]); + 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 index fd658c6..5bc4ed0 100644 --- a/src/asynch/control.rs +++ b/src/asynch/control.rs @@ -1,44 +1,101 @@ use core::future::poll_fn; use core::task::Poll; -use atat::asynch::AtatClient; +use atat::{ + asynch::{AtatClient, SimpleClient}, + UrcChannel, UrcSubscription, +}; +use embassy_net::{ + udp::{PacketMetadata, UdpSocket}, + Ipv4Address, +}; use embassy_time::{with_timeout, Duration}; +use crate::command::gpio::{ + types::{GPIOId, GPIOValue}, + WriteGPIO, +}; use crate::command::network::SetNetworkHostName; -use crate::command::security::types::SecurityDataType; -use crate::command::security::SendSecurityDataImport; +use crate::command::system::{RebootDCE, ResetToFactoryDefaults}; use crate::command::wifi::types::{ Authentication, StatusId, WifiStationAction, WifiStationConfig, WifiStatus, WifiStatusVal, }; use crate::command::wifi::{ExecWifiStationAction, GetWifiStatus, SetWifiStationConfig}; use crate::command::OnOff; -use crate::command::{ - gpio::{ - types::{GPIOId, GPIOValue}, - WriteGPIO, - }, - security::PrepareSecurityDataImport, -}; use crate::error::Error; use super::state::LinkState; -use super::{state, AtHandle}; +use super::{at_udp_socket::AtUdpSocket, runner::URC_SUBSCRIBERS}; +use super::{state, UbloxUrc}; const CONFIG_ID: u8 = 0; -pub struct Control<'a, AT: AtatClient> { - state_ch: state::StateRunner<'a>, - at: AtHandle<'a, AT>, +const MAX_COMMAND_LEN: usize = 128; + +// TODO: Can this be made in a more intuitive way? +pub struct ControlResources { + rx_meta: [PacketMetadata; 1], + tx_meta: [PacketMetadata; 1], + socket_rx_buf: [u8; 32], + socket_tx_buf: [u8; 32], + at_buf: [u8; MAX_COMMAND_LEN], } -impl<'a, AT: AtatClient> Control<'a, AT> { - pub(crate) fn new(state_ch: state::StateRunner<'a>, at: AtHandle<'a, AT>) -> Self { - Self { state_ch, at } +impl ControlResources { + pub const fn new() -> Self { + Self { + rx_meta: [PacketMetadata::EMPTY; 1], + tx_meta: [PacketMetadata::EMPTY; 1], + socket_rx_buf: [0u8; 32], + socket_tx_buf: [0u8; 32], + at_buf: [0u8; MAX_COMMAND_LEN], + } + } +} + +pub struct Control<'a, 'r, const URC_CAPACITY: usize> { + state_ch: state::Runner<'a>, + at_client: SimpleClient<'r, AtUdpSocket<'r>, atat::DefaultDigester>, + _urc_subscription: UrcSubscription<'a, UbloxUrc, URC_CAPACITY, URC_SUBSCRIBERS>, +} + +impl<'a, 'r, const URC_CAPACITY: usize> Control<'a, 'r, URC_CAPACITY> { + pub(crate) fn new( + state_ch: state::Runner<'a>, + urc_channel: &'a UrcChannel, + resources: &'r mut ControlResources, + stack: &'r embassy_net::Stack, + ) -> Self { + let mut socket = UdpSocket::new( + stack, + &mut resources.rx_meta, + &mut resources.socket_rx_buf, + &mut resources.tx_meta, + &mut resources.socket_tx_buf, + ); + + info!("Socket bound!"); + socket + .bind((Ipv4Address::new(172, 30, 0, 252), AtUdpSocket::PPP_AT_PORT)) + .unwrap(); + + let at_client = SimpleClient::new( + AtUdpSocket(socket), + atat::AtDigester::::new(), + &mut resources.at_buf, + atat::Config::default(), + ); + + Self { + state_ch, + at_client, + _urc_subscription: urc_channel.subscribe().unwrap(), + } } pub async fn set_hostname(&mut self, hostname: &str) -> Result<(), Error> { - self.at - .send(SetNetworkHostName { + self.at_client + .send(&SetNetworkHostName { host_name: hostname, }) .await?; @@ -47,8 +104,8 @@ impl<'a, AT: AtatClient> Control<'a, AT> { async fn get_wifi_status(&mut self) -> Result { match self - .at - .send(GetWifiStatus { + .at_client + .send(&GetWifiStatus { status_id: StatusId::Status, }) .await? @@ -61,8 +118,8 @@ impl<'a, AT: AtatClient> Control<'a, AT> { async fn get_connected_ssid(&mut self) -> Result, Error> { match self - .at - .send(GetWifiStatus { + .at_client + .send(&GetWifiStatus { status_id: StatusId::SSID, }) .await? @@ -73,6 +130,13 @@ impl<'a, AT: AtatClient> Control<'a, AT> { } } + pub async fn factory_reset(&mut self) -> Result<(), Error> { + self.at_client.send(&ResetToFactoryDefaults).await?; + self.at_client.send(&RebootDCE).await?; + + Ok(()) + } + pub async fn join_open(&mut self, ssid: &str) -> Result<(), Error> { if matches!(self.get_wifi_status().await?, WifiStatusVal::Connected) { // Wifi already connected. Check if the SSID is the same @@ -84,15 +148,22 @@ impl<'a, AT: AtatClient> Control<'a, AT> { }; } - self.at - .send(SetWifiStationConfig { + self.at_client + .send(&ExecWifiStationAction { + config_id: CONFIG_ID, + action: WifiStationAction::Reset, + }) + .await?; + + self.at_client + .send(&SetWifiStationConfig { config_id: CONFIG_ID, config_param: WifiStationConfig::ActiveOnStartup(OnOff::Off), }) .await?; - self.at - .send(SetWifiStationConfig { + self.at_client + .send(&SetWifiStationConfig { config_id: CONFIG_ID, config_param: WifiStationConfig::SSID( heapless::String::try_from(ssid).map_err(|_| Error::Overflow)?, @@ -100,21 +171,21 @@ impl<'a, AT: AtatClient> Control<'a, AT> { }) .await?; - self.at - .send(SetWifiStationConfig { + self.at_client + .send(&SetWifiStationConfig { config_id: CONFIG_ID, config_param: WifiStationConfig::Authentication(Authentication::Open), }) .await?; - self.at - .send(ExecWifiStationAction { + self.at_client + .send(&ExecWifiStationAction { config_id: CONFIG_ID, action: WifiStationAction::Activate, }) .await?; - with_timeout(Duration::from_secs(10), self.wait_for_join(ssid)) + with_timeout(Duration::from_secs(25), self.wait_for_join(ssid)) .await .map_err(|_| Error::Timeout)??; @@ -132,22 +203,22 @@ impl<'a, AT: AtatClient> Control<'a, AT> { }; } - self.at - .send(ExecWifiStationAction { + self.at_client + .send(&ExecWifiStationAction { config_id: CONFIG_ID, action: WifiStationAction::Reset, }) .await?; - self.at - .send(SetWifiStationConfig { + self.at_client + .send(&SetWifiStationConfig { config_id: CONFIG_ID, config_param: WifiStationConfig::ActiveOnStartup(OnOff::Off), }) .await?; - self.at - .send(SetWifiStationConfig { + self.at_client + .send(&SetWifiStationConfig { config_id: CONFIG_ID, config_param: WifiStationConfig::SSID( heapless::String::try_from(ssid).map_err(|_| Error::Overflow)?, @@ -155,15 +226,15 @@ impl<'a, AT: AtatClient> Control<'a, AT> { }) .await?; - self.at - .send(SetWifiStationConfig { + self.at_client + .send(&SetWifiStationConfig { config_id: CONFIG_ID, config_param: WifiStationConfig::Authentication(Authentication::WpaWpa2Psk), }) .await?; - self.at - .send(SetWifiStationConfig { + self.at_client + .send(&SetWifiStationConfig { config_id: CONFIG_ID, config_param: WifiStationConfig::WpaPskOrPassphrase( heapless::String::try_from(passphrase).map_err(|_| Error::Overflow)?, @@ -171,8 +242,8 @@ impl<'a, AT: AtatClient> Control<'a, AT> { }) .await?; - self.at - .send(ExecWifiStationAction { + self.at_client + .send(&ExecWifiStationAction { config_id: CONFIG_ID, action: WifiStationAction::Activate, }) @@ -189,8 +260,8 @@ impl<'a, AT: AtatClient> Control<'a, AT> { match self.get_wifi_status().await? { WifiStatusVal::Disabled => {} WifiStatusVal::Disconnected | WifiStatusVal::Connected => { - self.at - .send(ExecWifiStationAction { + self.at_client + .send(&ExecWifiStationAction { config_id: CONFIG_ID, action: WifiStationAction::Deactivate, }) @@ -227,11 +298,12 @@ impl<'a, AT: AtatClient> Control<'a, AT> { } pub async fn gpio_set(&mut self, id: GPIOId, value: GPIOValue) -> Result<(), Error> { - self.at.send(WriteGPIO { id, value }).await?; + self.at_client.send(&WriteGPIO { id, value }).await?; Ok(()) } // FIXME: This could probably be improved + #[cfg(feature = "internal-network-stack")] pub async fn import_credentials( &mut self, data_type: SecurityDataType, @@ -243,8 +315,8 @@ impl<'a, AT: AtatClient> Control<'a, AT> { info!("Importing {:?} bytes as {:?}", data.len(), name); - self.at - .send(PrepareSecurityDataImport { + self.at_client + .send(&PrepareSecurityDataImport { data_type, data_size: data.len(), internal_name: name, @@ -253,8 +325,8 @@ impl<'a, AT: AtatClient> Control<'a, AT> { .await?; let import_data = self - .at - .send(SendSecurityDataImport { + .at_client + .send(&SendSecurityDataImport { data: atat::serde_bytes::Bytes::new(data), }) .await?; diff --git a/src/asynch/internal_stack.rs b/src/asynch/internal_stack.rs deleted file mode 100644 index 02cdbac..0000000 --- a/src/asynch/internal_stack.rs +++ /dev/null @@ -1,117 +0,0 @@ -// pub mod ublox_stack; - -use core::mem::MaybeUninit; - -use atat::{asynch::Client, AtatIngress}; -use embassy_sync::{blocking_mutex::raw::NoopRawMutex, mutex::Mutex}; -use embedded_hal::digital::OutputPin; -use embedded_io_async::{Read, Write}; - -use crate::command::custom_digest::EdmDigester; - -pub use super::resources::UbxResources as Resources; - -use super::{control::Control, runner::Runner, state, AtHandle, UbloxUrc}; - -pub struct InternalRunner< - 'a, - R: Read, - W: Write, - RST: OutputPin, - const INGRESS_BUF_SIZE: usize, - const URC_CAPACITY: usize, -> { - pub cellular_runner: Runner<'a, Client<'a, W, INGRESS_BUF_SIZE>, RST, URC_CAPACITY>, - pub ingress: atat::Ingress<'a, EdmDigester, UbloxUrc, INGRESS_BUF_SIZE, URC_CAPACITY, 2>, - pub reader: R, -} - -impl< - 'a, - R: Read, - W: Write, - RST: OutputPin, - const INGRESS_BUF_SIZE: usize, - const URC_CAPACITY: usize, - > InternalRunner<'a, R, W, RST, INGRESS_BUF_SIZE, URC_CAPACITY> -{ - pub async fn run(&mut self) -> ! { - self.cellular_runner.init().await.unwrap(); - - embassy_futures::join::join( - self.ingress.read_from(&mut self.reader), - self.cellular_runner.run(), - ) - .await; - core::unreachable!() - } -} - -pub fn new_internal< - 'a, - R: Read, - W: Write, - RST: OutputPin, - const CMD_BUF_SIZE: usize, - const INGRESS_BUF_SIZE: usize, - const URC_CAPACITY: usize, ->( - reader: R, - writer: W, - resources: &'a mut Resources, - reset: RST, -) -> ( - state::Device<'a, Client<'a, W, INGRESS_BUF_SIZE>, URC_CAPACITY>, - Control<'a, Client<'a, W, INGRESS_BUF_SIZE>>, - InternalRunner<'a, R, W, RST, INGRESS_BUF_SIZE, URC_CAPACITY>, -) { - // safety: this is a self-referential struct, however: - // - it can't move while the `'a` borrow is active. - // - when the borrow ends, the dangling references inside the MaybeUninit will never be used again. - let at_client_uninit: *mut MaybeUninit>> = - (&mut resources.at_client - as *mut MaybeUninit>>) - .cast(); - - unsafe { &mut *at_client_uninit }.write(Mutex::new(Client::new( - writer, - &resources.res_slot, - &mut resources.cmd_buf, - atat::Config::default(), - ))); - - let at_client = unsafe { (&*at_client_uninit).assume_init_ref() }; - - let (ch_runner, net_device) = state::new( - &mut resources.ch, - AtHandle(at_client), - resources.urc_channel.subscribe().unwrap(), - ); - - let control = Control::new(ch_runner.state_runner(), AtHandle(at_client)); - - let runner = Runner::new( - ch_runner, - AtHandle(at_client), - reset, - resources.urc_channel.subscribe().unwrap(), - ); - - // runner.init().await.unwrap(); - // control.init().await.unwrap(); - - let ingress = atat::Ingress::new( - EdmDigester, - &mut resources.ingress_buf, - &resources.res_slot, - &resources.urc_channel, - ); - - let runner = InternalRunner { - cellular_runner: runner, - ingress, - reader, - }; - - (net_device, control, runner) -} diff --git a/src/asynch/mod.rs b/src/asynch/mod.rs index de8313c..a6ef5fb 100644 --- a/src/asynch/mod.rs +++ b/src/asynch/mod.rs @@ -1,4 +1,6 @@ +mod at_udp_socket; pub mod control; +pub mod network; mod resources; pub mod runner; #[cfg(feature = "ublox-sockets")] @@ -6,18 +8,12 @@ pub mod ublox_stack; pub(crate) mod state; +pub use resources::Resources; +pub use runner::Runner; + #[cfg(feature = "internal-network-stack")] mod internal_stack; -#[cfg(feature = "internal-network-stack")] -pub use internal_stack::{new_internal, InternalRunner, Resources}; - -#[cfg(feature = "ppp")] -mod ppp; -#[cfg(feature = "ppp")] -pub use ppp::{new_ppp, PPPRunner, Resources}; - -use atat::asynch::AtatClient; -use embassy_sync::{blocking_mutex::raw::NoopRawMutex, mutex::Mutex}; +use embedded_io_async::{BufRead, Error as _, ErrorKind, Read, Write}; #[cfg(feature = "edm")] pub type UbloxUrc = crate::command::edm::urc::EdmEvent; @@ -25,24 +21,34 @@ pub type UbloxUrc = crate::command::edm::urc::EdmEvent; #[cfg(not(feature = "edm"))] pub type UbloxUrc = crate::command::Urc; -pub struct AtHandle<'d, AT: AtatClient>(&'d Mutex); +pub struct ReadWriteAdapter(pub R, pub W); + +impl embedded_io_async::ErrorType for ReadWriteAdapter { + type Error = ErrorKind; +} + +impl Read for ReadWriteAdapter { + async fn read(&mut self, buf: &mut [u8]) -> Result { + self.0.read(buf).await.map_err(|e| e.kind()) + } +} + +impl BufRead for ReadWriteAdapter { + async fn fill_buf(&mut self) -> Result<&[u8], Self::Error> { + self.0.fill_buf().await.map_err(|e| e.kind()) + } -impl<'d, AT: AtatClient> AtHandle<'d, AT> { - #[cfg(feature = "edm")] - async fn send(&mut self, cmd: Cmd) -> Result { - self.send_raw(crate::command::edm::EdmAtCmdWrapper(cmd)) - .await + fn consume(&mut self, amt: usize) { + self.0.consume(amt) } +} - #[cfg(not(feature = "edm"))] - async fn send(&mut self, cmd: Cmd) -> Result { - self.send_raw(cmd).await +impl Write for ReadWriteAdapter { + async fn write(&mut self, buf: &[u8]) -> Result { + self.1.write(buf).await.map_err(|e| e.kind()) } - async fn send_raw( - &mut self, - cmd: Cmd, - ) -> Result { - self.0.lock().await.send_retry::(&cmd).await + async fn flush(&mut self) -> Result<(), Self::Error> { + self.1.flush().await.map_err(|e| e.kind()) } } diff --git a/src/asynch/network.rs b/src/asynch/network.rs new file mode 100644 index 0000000..5b7bcae --- /dev/null +++ b/src/asynch/network.rs @@ -0,0 +1,292 @@ +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::{ + data_mode::{types::PeerConfigParameter, SetPeerConfiguration}, + general::SoftwareVersion, + 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 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(crate) async fn init(&mut self) -> Result<(), Error> { + // Initilize a new ublox device to a known state (set RS232 settings) + debug!("Initializing module"); + // Hard reset module + self.reset().await?; + + self.at_client.send(&SoftwareVersion).await?; + + if let Some(size) = C::TLS_IN_BUFFER_SIZE { + self.at_client + .send(&SetPeerConfiguration { + parameter: PeerConfigParameter::TlsInBuffer(size), + }) + .await?; + } + + if let Some(size) = C::TLS_OUT_BUFFER_SIZE { + self.at_client + .send(&SetPeerConfiguration { + parameter: PeerConfigParameter::TlsOutBuffer(size), + }) + .await?; + } + + Ok(()) + } + + pub async fn run(&mut self) -> ! { + loop { + let event = self.urc_subscription.next_message_pure().await; + + #[cfg(feature = "edm")] + let Some(event) = event.extract_urc() else { + continue; + }; + + self.handle_urc(event).await; + } + } + + async fn handle_urc(&mut self, event: Urc) { + debug!("GOT URC event"); + 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)); + con.activated = true; + }), + Urc::WifiLinkDisconnected(WifiLinkDisconnected { reason, .. }) => { + self.ch.update_connection_with(|con| match reason { + DisconnectReason::NetworkDisabled => { + con.wifi_state = WiFiState::Inactive; + } + DisconnectReason::SecurityProblems => { + error!("Wifi Security Problems"); + con.wifi_state = WiFiState::NotConnected; + } + _ => { + con.wifi_state = WiFiState::NotConnected; + } + }) + } + Urc::WifiAPUp(_) => todo!(), + Urc::WifiAPDown(_) => todo!(), + Urc::WifiAPStationConnected(_) => todo!(), + Urc::WifiAPStationDisconnected(_) => todo!(), + Urc::EthernetLinkUp(_) => todo!(), + Urc::EthernetLinkDown(_) => todo!(), + Urc::NetworkUp(NetworkUp { interface_id }) => { + drop(event); + self.network_status_callback(interface_id).await.ok(); + } + Urc::NetworkDown(NetworkDown { interface_id }) => { + drop(event); + self.network_status_callback(interface_id).await.ok(); + } + Urc::NetworkError(_) => todo!(), + _ => {} + } + } + + async fn network_status_callback(&mut self, interface_id: u8) -> Result<(), Error> { + let NetworkStatusResponse { + status: NetworkStatus::InterfaceType(InterfaceType::WifiStation), + .. + } = self + .at_client + .send(&GetNetworkStatus { + interface_id, + status: NetworkStatusParameter::InterfaceType, + }) + .await? + else { + return Err(Error::Network); + }; + + let NetworkStatusResponse { + status: NetworkStatus::IPv4Address(ipv4), + .. + } = self + .at_client + .send(&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(); + + let NetworkStatusResponse { + status: NetworkStatus::IPv6LinkLocalAddress(ipv6), + .. + } = self + .at_client + .send(&GetNetworkStatus { + interface_id, + status: NetworkStatusParameter::IPv6LinkLocalAddress, + }) + .await? + else { + return Err(Error::Network); + }; + + let ipv6_up = core::str::from_utf8(ipv6.as_slice()) + .ok() + .and_then(|s| Ipv6Addr::from_str(s).ok()) + .map(|ip| !ip.is_unspecified()) + .unwrap_or_default(); + + // Use `ipv4_up` & `ipv6_up` to determine link state + self.ch + .update_connection_with(|con| con.network_up = ipv4_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; + }; + + match event { + Urc::StartUp => return, + _ => {} + } + } + }; + + with_timeout(timeout, fut).await.map_err(|_| Error::Timeout) + } + + pub async fn reset(&mut self) -> Result<(), Error> { + warn!("Hard resetting Ublox Short Range"); + self.config.reset_pin().unwrap().set_low().ok(); + Timer::after(Duration::from_millis(100)).await; + self.config.reset_pin().unwrap().set_high().ok(); + + self.wait_startup(Duration::from_secs(4)).await?; + + #[cfg(feature = "edm")] + self.enter_edm(Duration::from_secs(4)).await?; + + Ok(()) + } + + pub async fn restart(&mut self, store: bool) -> Result<(), Error> { + warn!("Soft resetting Ublox Short Range"); + if store { + self.at_client.send(&StoreCurrentConfig).await?; + } + + self.at_client.send(&RebootDCE).await?; + + self.wait_startup(Duration::from_secs(10)).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(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)?; + + self.at_client + .send(crate::command::system::SetEcho { + on: crate::command::system::types::EchoOn::Off, + }) + .await?; + + Ok(()) + } +} diff --git a/src/asynch/ppp.rs b/src/asynch/ppp.rs deleted file mode 100644 index dfd3588..0000000 --- a/src/asynch/ppp.rs +++ /dev/null @@ -1,338 +0,0 @@ -use core::mem::MaybeUninit; - -use atat::{ - asynch::{AtatClient, Client, SimpleClient}, - AtatIngress, -}; -use embassy_futures::select::Either; -use embassy_net::{ - udp::{PacketMetadata, UdpSocket}, - IpEndpoint, Ipv4Address, -}; -use embassy_sync::{ - blocking_mutex::raw::NoopRawMutex, - mutex::Mutex, - pipe::{Reader, Writer}, -}; -use embassy_time::{Duration, Instant, Timer}; -use embedded_hal::digital::OutputPin; -use embedded_io_async::{BufRead, Read, Write}; - -use crate::command::{ - data_mode::{self, ChangeMode}, - system::{self, SetEcho}, -}; - -use super::{control::Control, resources::UbxResources, runner::Runner, state, AtHandle, UbloxUrc}; - -const PPP_AT_PORT: u16 = 23; -pub const SOCKET_BUF_SIZE: usize = 128; - -pub type Resources< - 'a, - const CMD_BUF_SIZE: usize, - const INGRESS_BUF_SIZE: usize, - const URC_CAPACITY: usize, -> = UbxResources< - Writer<'a, NoopRawMutex, SOCKET_BUF_SIZE>, - CMD_BUF_SIZE, - INGRESS_BUF_SIZE, - URC_CAPACITY, ->; - -pub fn new_ppp< - 'a, - RST: OutputPin, - const CMD_BUF_SIZE: usize, - const INGRESS_BUF_SIZE: usize, - const URC_CAPACITY: usize, ->( - resources: &'a mut Resources<'a, CMD_BUF_SIZE, INGRESS_BUF_SIZE, URC_CAPACITY>, - reset: RST, -) -> ( - embassy_net_ppp::Device<'a>, - Control<'a, Client<'a, Writer<'a, NoopRawMutex, SOCKET_BUF_SIZE>, INGRESS_BUF_SIZE>>, - PPPRunner<'a, RST, INGRESS_BUF_SIZE, URC_CAPACITY>, -) { - let ch_runner = state::new_ppp(&mut resources.ch); - let state_ch = ch_runner.state_runner(); - - let (control_rx_reader, control_rx_writer) = resources.control_rx.split(); - let (control_tx_reader, control_tx_writer) = resources.control_tx.split(); - - // safety: this is a self-referential struct, however: - // - it can't move while the `'a` borrow is active. - // - when the borrow ends, the dangling references inside the MaybeUninit will never be used again. - let at_client_uninit: *mut MaybeUninit< - Mutex< - NoopRawMutex, - Client<'a, Writer<'a, NoopRawMutex, SOCKET_BUF_SIZE>, INGRESS_BUF_SIZE>, - >, - > = (&mut resources.at_client - as *mut MaybeUninit< - Mutex< - NoopRawMutex, - Client<'static, Writer<'a, NoopRawMutex, SOCKET_BUF_SIZE>, INGRESS_BUF_SIZE>, - >, - >) - .cast(); - - unsafe { &mut *at_client_uninit }.write(Mutex::new(Client::new( - control_tx_writer, - &resources.res_slot, - &mut resources.cmd_buf, - atat::Config::default(), - ))); - - let at_client = unsafe { (&*at_client_uninit).assume_init_ref() }; - - let wifi_runner = Runner::new( - ch_runner, - AtHandle(at_client), - reset, - resources.urc_channel.subscribe().unwrap(), - ); - - let ingress = atat::Ingress::new( - atat::AtDigester::::new(), - &mut resources.ingress_buf, - &resources.res_slot, - &resources.urc_channel, - ); - - let control = Control::new(state_ch, AtHandle(at_client)); - - let (net_device, ppp_runner) = embassy_net_ppp::new(&mut resources.ppp_state); - - let runner = PPPRunner { - ppp_runner, - wifi_runner, - ingress, - control_rx_reader, - control_rx_writer, - control_tx_reader, - }; - - (net_device, control, runner) -} - -pub struct PPPRunner<'a, RST: OutputPin, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> { - pub ppp_runner: embassy_net_ppp::Runner<'a>, - pub wifi_runner: Runner< - 'a, - Client<'a, Writer<'a, NoopRawMutex, SOCKET_BUF_SIZE>, INGRESS_BUF_SIZE>, - RST, - URC_CAPACITY, - >, - pub ingress: - atat::Ingress<'a, atat::AtDigester, UbloxUrc, INGRESS_BUF_SIZE, URC_CAPACITY, 2>, - pub control_rx_reader: Reader<'a, NoopRawMutex, SOCKET_BUF_SIZE>, - pub control_rx_writer: Writer<'a, NoopRawMutex, SOCKET_BUF_SIZE>, - pub control_tx_reader: Reader<'a, NoopRawMutex, SOCKET_BUF_SIZE>, -} - -impl<'a, RST: OutputPin, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> - PPPRunner<'a, RST, INGRESS_BUF_SIZE, URC_CAPACITY> -{ - async fn configure(at_client: &mut A) -> Result<(), atat::Error> { - let _ = at_client - .send(&ChangeMode { - mode: data_mode::types::Mode::CommandMode, - }) - .await; - - at_client - .send(&SetEcho { - on: system::types::EchoOn::Off, - }) - .await?; - - // Initialize `ublox` module to desired baudrate - at_client - .send(&system::SetRS232Settings { - baud_rate: system::types::BaudRate::B115200, - flow_control: system::types::FlowControl::On, - data_bits: 8, - stop_bits: system::types::StopBits::One, - parity: system::types::Parity::None, - change_after_confirm: system::types::ChangeAfterConfirm::ChangeAfterOK, - }) - .await?; - - Ok(()) - } - - pub async fn run( - &mut self, - mut iface: RW, - stack: &embassy_net::Stack>, - ) -> ! { - // self.wifi_runner.init().await.unwrap(); - // Timer::after(Duration::from_secs(4)).await; - - loop { - // Reset modem - self.wifi_runner.reset().await; - - Timer::after(Duration::from_secs(1)).await; - - let control_fut = async { - stack.wait_config_up().await; - - let mut rx_meta = [PacketMetadata::EMPTY; 1]; - let mut tx_meta = [PacketMetadata::EMPTY; 1]; - let mut socket_rx_buf = [0u8; 32]; - let mut socket_tx_buf = [0u8; 32]; - let mut socket = UdpSocket::new( - stack, - &mut rx_meta, - &mut socket_rx_buf, - &mut tx_meta, - &mut socket_tx_buf, - ); - - let endpoint = stack.config_v4().unwrap(); - - info!("Socket bound!"); - socket - .bind((endpoint.address.address(), PPP_AT_PORT)) - .unwrap(); - - let mut tx_buf = [0u8; 32]; - let mut rx_buf = [0u8; 32]; - - loop { - match embassy_futures::select::select( - self.control_tx_reader.read(&mut tx_buf), - socket.recv_from(&mut rx_buf), - ) - .await - { - Either::First(n) => { - socket - .send_to( - &tx_buf[..n], - (Ipv4Address::new(172, 30, 0, 251), PPP_AT_PORT), - ) - .await - .unwrap(); - } - Either::Second(Ok((n, _))) => { - self.control_rx_writer - .write_all(&rx_buf[..n]) - .await - .unwrap(); - } - Either::Second(_) => {} - } - } - }; - - let ppp_fut = async { - let mut fails = 0; - let mut last_start = None; - - loop { - if let Some(last_start) = last_start { - Timer::at(last_start + Duration::from_secs(10)).await; - // Do not attempt to start too fast. - - // If was up stably for at least 1 min, reset fail counter. - if Instant::now() > last_start + Duration::from_secs(60) { - fails = 0; - } else { - fails += 1; - if fails == 10 { - warn!("modem: PPP failed too much, rebooting modem."); - break; - } - } - } - last_start = Some(Instant::now()); - - let mut buf = [0u8; 64]; - let mut at_client = SimpleClient::new( - &mut iface, - atat::AtDigester::::new(), - &mut buf, - atat::Config::default(), - ); - - if let Err(e) = Self::configure(&mut at_client).await { - warn!("modem: configure failed {:?}", e); - continue; - } - - Timer::after(Duration::from_secs(2)).await; - - // Send AT command `ATO3` to enter PPP mode - let res = at_client - .send(&ChangeMode { - mode: data_mode::types::Mode::PPPMode, - }) - .await; - - if let Err(e) = res { - warn!("ppp dial failed {:?}", e); - continue; - } - - drop(at_client); - - // Drain the UART - let _ = embassy_time::with_timeout(Duration::from_secs(2), async { - loop { - iface.read(&mut buf).await.ok(); - } - }) - .await; - - info!("RUNNING PPP"); - let config = embassy_net_ppp::Config { - username: b"", - password: b"", - }; - let res = self - .ppp_runner - .run(&mut iface, config, |ipv4| { - let Some(addr) = ipv4.address else { - warn!("PPP did not provide an IP address."); - return; - }; - let mut dns_servers = heapless::Vec::new(); - for s in ipv4.dns_servers.iter().flatten() { - let _ = - dns_servers.push(embassy_net::Ipv4Address::from_bytes(&s.0)); - } - let config = - embassy_net::ConfigV4::Static(embassy_net::StaticConfigV4 { - address: embassy_net::Ipv4Cidr::new( - embassy_net::Ipv4Address::from_bytes(&addr.0), - 0, - ), - gateway: None, - dns_servers, - }); - - stack.set_config_v4(config); - }) - .await; - - info!("ppp failed"); - } - }; - - let ingress_fut = async { - self.ingress.read_from(&mut self.control_rx_reader).await; - }; - - embassy_futures::select::select4( - ppp_fut, - ingress_fut, - control_fut, - self.wifi_runner.run(), - ) - .await; - } - } -} diff --git a/src/asynch/resources.rs b/src/asynch/resources.rs index d55fb2f..90cbe50 100644 --- a/src/asynch/resources.rs +++ b/src/asynch/resources.rs @@ -1,13 +1,8 @@ -use core::mem::MaybeUninit; +use atat::{ResponseSlot, UrcChannel}; -use atat::{asynch::Client, ResponseSlot, UrcChannel}; -use embassy_sync::{blocking_mutex::raw::NoopRawMutex, mutex::Mutex}; -use embedded_io_async::Write; +use super::{runner::URC_SUBSCRIBERS, state, UbloxUrc}; -use super::{state, UbloxUrc}; - -pub struct UbxResources< - W: Write, +pub struct Resources< const CMD_BUF_SIZE: usize, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize, @@ -15,27 +10,21 @@ pub struct UbxResources< pub(crate) ch: state::State, pub(crate) res_slot: ResponseSlot, - pub(crate) urc_channel: UrcChannel, + pub(crate) urc_channel: UrcChannel, pub(crate) cmd_buf: [u8; CMD_BUF_SIZE], pub(crate) ingress_buf: [u8; INGRESS_BUF_SIZE], +} - pub(crate) at_client: MaybeUninit>>, - - #[cfg(feature = "ppp")] - pub(crate) ppp_state: embassy_net_ppp::State<2, 2>, - - #[cfg(feature = "ppp")] - pub(crate) control_rx: embassy_sync::pipe::Pipe, - #[cfg(feature = "ppp")] - pub(crate) control_tx: embassy_sync::pipe::Pipe, +impl Default + for Resources +{ + fn default() -> Self { + Self::new() + } } -impl< - W: Write, - const CMD_BUF_SIZE: usize, - const INGRESS_BUF_SIZE: usize, - const URC_CAPACITY: usize, - > UbxResources +impl + Resources { pub fn new() -> Self { Self { @@ -45,16 +34,6 @@ impl< urc_channel: UrcChannel::new(), cmd_buf: [0; CMD_BUF_SIZE], ingress_buf: [0; INGRESS_BUF_SIZE], - - at_client: MaybeUninit::uninit(), - - #[cfg(feature = "ppp")] - ppp_state: embassy_net_ppp::State::new(), - - #[cfg(feature = "ppp")] - control_rx: embassy_sync::pipe::Pipe::new(), - #[cfg(feature = "ppp")] - control_tx: embassy_sync::pipe::Pipe::new(), } } } diff --git a/src/asynch/runner.rs b/src/asynch/runner.rs index fb0be9d..335d809 100644 --- a/src/asynch/runner.rs +++ b/src/asynch/runner.rs @@ -1,365 +1,299 @@ -use core::str::FromStr; - use super::{ - state::{self, LinkState}, - UbloxUrc, + control::{Control, ControlResources}, + network::NetDevice, + state, Resources, UbloxUrc, }; #[cfg(feature = "edm")] use crate::command::edm::SwitchToEdmCommand; use crate::{ + asynch::at_udp_socket::AtUdpSocket, command::{ - general::SoftwareVersion, - network::{ - responses::NetworkStatusResponse, - types::{InterfaceType, NetworkStatus, NetworkStatusParameter}, - urc::{NetworkDown, NetworkUp}, - GetNetworkStatus, - }, - system::{ - types::{BaudRate, ChangeAfterConfirm, FlowControl, Parity, StopBits}, - RebootDCE, SetRS232Settings, StoreCurrentConfig, - }, - wifi::{ - types::DisconnectReason, - urc::{WifiLinkConnected, WifiLinkDisconnected}, - }, + data_mode::{self, ChangeMode}, Urc, }, - connection::{WiFiState, WifiConnection}, - error::Error, - network::WifiNetwork, + WifiConfig, +}; +use atat::{ + asynch::{AtatClient, SimpleClient}, + AtatIngress as _, UrcChannel, }; -use atat::{asynch::AtatClient, UrcSubscription}; -use embassy_time::{with_timeout, Duration, Timer}; -use embedded_hal::digital::OutputPin; -use no_std_net::{Ipv4Addr, Ipv6Addr}; +use embassy_futures::select::Either; +use embassy_net::{ + udp::{PacketMetadata, UdpSocket}, + Ipv4Address, +}; +use embassy_time::{Duration, Instant, Timer}; +use embedded_io_async::{BufRead, Read, Write}; -use super::AtHandle; +pub(crate) const URC_SUBSCRIBERS: usize = 3; /// Background runner for the Ublox Module. /// /// You must call `.run()` in a background task for the Ublox Module to operate. -pub struct Runner<'d, AT: AtatClient, RST: OutputPin, const URC_CAPACITY: usize> { - ch: state::Runner<'d>, - at: AtHandle<'d, AT>, - reset: RST, - wifi_connection: Option, - urc_subscription: UrcSubscription<'d, UbloxUrc, URC_CAPACITY, 2>, +pub struct Runner<'a, R, W, C, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> { + iface: (R, W), + + ch: state::Runner<'a>, + config: C, + + pub urc_channel: &'a UrcChannel, + + pub ingress: atat::Ingress< + 'a, + atat::AtDigester, + Urc, + INGRESS_BUF_SIZE, + URC_CAPACITY, + URC_SUBSCRIBERS, + >, + pub cmd_buf: &'a mut [u8], + pub res_slot: &'a atat::ResponseSlot, + + #[cfg(feature = "ppp")] + ppp_runner: Option>, } -impl< - 'd, - AT: AtatClient, - // AT: AtatClient + atat::UartExt, - RST: OutputPin, - const URC_CAPACITY: usize, - > Runner<'d, AT, RST, URC_CAPACITY> +impl<'a, R, W, C, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> + Runner<'a, R, W, C, INGRESS_BUF_SIZE, URC_CAPACITY> +where + R: BufRead + Read, + W: Write, + C: WifiConfig<'a> + 'a, { - pub(crate) fn new( - ch: state::Runner<'d>, - at: AtHandle<'d, AT>, - reset: RST, - urc_subscription: UrcSubscription<'d, UbloxUrc, URC_CAPACITY, 2>, + pub fn new( + iface: (R, W), + resources: &'a mut Resources, + config: C, ) -> Self { + let ch_runner = state::Runner::new(&mut resources.ch); + + let ingress = atat::Ingress::new( + atat::AtDigester::new(), + &mut resources.ingress_buf, + &resources.res_slot, + &resources.urc_channel, + ); + Self { - ch, - at, - reset, - wifi_connection: None, - urc_subscription, + iface, + + ch: ch_runner, + config, + urc_channel: &resources.urc_channel, + + ingress, + cmd_buf: &mut resources.cmd_buf, + res_slot: &resources.res_slot, + + #[cfg(feature = "ppp")] + ppp_runner: None, } } - pub(crate) async fn init(&mut self) -> Result<(), Error> { - // Initilize a new ublox device to a known state (set RS232 settings) - debug!("Initializing module"); - // Hard reset module - self.reset().await?; - - // ## 2.2.6.1 AT request serial settings (EDM mode) - // - // The AT+UMRS command to change serial settings does not work exactly - // the same as in command mode. When executed in the extended data mode, - // it is not possible to change the settings directly using the - // parameter. Instead, the - // parameter must be set to 0 and the serial settings will take effect - // when the module is reset. - self.at - .send(SetRS232Settings { - baud_rate: BaudRate::B115200, - flow_control: FlowControl::On, - data_bits: 8, - stop_bits: StopBits::One, - parity: Parity::None, - change_after_confirm: ChangeAfterConfirm::StoreAndReset, - }) - .await?; - - self.restart(true).await?; - - self.at.send(SoftwareVersion).await?; - - // Move to control - // if let Some(size) = self.config.tls_in_buffer_size { - // self.at - // .send(SetPeerConfiguration { - // parameter: PeerConfigParameter::TlsInBuffer(size), - // }) - // .await?; - // } - - // if let Some(size) = self.config.tls_out_buffer_size { - // self.at - // .send(SetPeerConfiguration { - // parameter: PeerConfigParameter::TlsOutBuffer(size), - // }) - // .await?; - // } - - Ok(()) + pub fn control<'r, D: embassy_net::driver::Driver>( + &self, + resources: &'r mut ControlResources, + stack: &'r embassy_net::Stack, + ) -> Control<'a, 'r, URC_CAPACITY> { + Control::new(self.ch.clone(), &self.urc_channel, resources, stack) } - 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; - }; - - match event { - Urc::StartUp => return, - _ => {} - } - } - }; + #[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 + } - with_timeout(timeout, fut).await.map_err(|_| Error::Timeout) + #[cfg(feature = "internal-network-stack")] + pub fn internal_stack(&mut self) -> state::Device { + state::Device { + shared: &self.ch.shared, + urc_subscription: self.urc_channel.subscribe().unwrap(), + } } - pub async fn reset(&mut self) -> Result<(), Error> { - warn!("Hard resetting Ublox Short Range"); - self.reset.set_low().ok(); - Timer::after(Duration::from_millis(100)).await; - self.reset.set_high().ok(); + pub async fn run(mut self, stack: &embassy_net::Stack) -> ! { + #[cfg(feature = "ppp")] + let mut ppp_runner = self.ppp_runner.take().unwrap(); - self.wait_startup(Duration::from_secs(4)).await?; + let at_config = atat::Config::default(); + loop { + // Run the cellular device from full power down to the + // `DataEstablished` state, handling power on, module configuration, + // network registration & operator selection and PDP context + // activation along the way. + // + // This is all done directly on the serial line, before setting up + // virtual channels through multiplexing. + { + let at_client = atat::asynch::Client::new( + &mut self.iface.1, + self.res_slot, + self.cmd_buf, + at_config, + ); + let mut wifi_device = + NetDevice::new(&self.ch, &mut self.config, at_client, self.urc_channel); + + // Clean up and start from completely powered off state. Ignore URCs in the process. + self.ingress.clear(); + + match embassy_futures::select::select( + self.ingress.read_from(&mut self.iface.0), + wifi_device.init(), + ) + .await + { + Either::First(_) => { + // This has return type never (`-> !`) + unreachable!() + } + Either::Second(Err(_)) => { + // Reboot the wifi module and try again! + continue; + } + Either::Second(Ok(_)) => { + // All good! We are now ready to start communication services! + } + } + } - #[cfg(feature = "edm")] - self.enter_edm(Duration::from_secs(4)).await?; + #[cfg(feature = "ppp")] + let ppp_fut = async { + let mut iface = super::ReadWriteAdapter(&mut self.iface.0, &mut self.iface.1); - Ok(()) - } + let mut fails = 0; + let mut last_start = None; - pub async fn restart(&mut self, store: bool) -> Result<(), Error> { - warn!("Soft resetting Ublox Short Range"); - if store { - self.at.send(StoreCurrentConfig).await?; - } + loop { + if let Some(last_start) = last_start { + Timer::at(last_start + Duration::from_secs(10)).await; + // Do not attempt to start too fast. - self.at.send(RebootDCE).await?; + // If was up stably for at least 1 min, reset fail counter. + if Instant::now() > last_start + Duration::from_secs(60) { + fails = 0; + } else { + fails += 1; + if fails == 10 { + warn!("modem: PPP failed too much, rebooting modem."); + break; + } + } + } + last_start = Some(Instant::now()); + + { + let mut buf = [0u8; 64]; + + let mut at_client = SimpleClient::new( + &mut iface, + atat::AtDigester::::new(), + &mut buf, + at_config, + ); + + // Send AT command `ATO3` to enter PPP mode + let res = at_client + .send(&ChangeMode { + mode: data_mode::types::Mode::PPPMode, + }) + .await; + + if let Err(e) = res { + warn!("ppp dial failed {:?}", e); + continue; + } - self.wait_startup(Duration::from_secs(10)).await?; + drop(at_client); - info!("Module started again"); - #[cfg(feature = "edm")] - self.enter_edm(Duration::from_secs(4)).await?; + // Drain the UART + let _ = embassy_time::with_timeout(Duration::from_secs(2), async { + loop { + iface.read(&mut buf).await.ok(); + } + }) + .await; - Ok(()) - } + Timer::after(Duration::from_millis(100)).await; + } - #[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.send(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; + info!("RUNNING PPP"); + let res = ppp_runner + .run(&mut iface, C::PPP_CONFIG, |ipv4| { + let Some(addr) = ipv4.address else { + warn!("PPP did not provide an IP address."); + return; + }; + let mut dns_servers = heapless::Vec::new(); + for s in ipv4.dns_servers.iter().flatten() { + let _ = + dns_servers.push(embassy_net::Ipv4Address::from_bytes(&s.0)); + } + let config = + embassy_net::ConfigV4::Static(embassy_net::StaticConfigV4 { + address: embassy_net::Ipv4Cidr::new( + embassy_net::Ipv4Address::from_bytes(&addr.0), + 0, + ), + gateway: None, + dns_servers, + }); + + stack.set_config_v4(config); + }) + .await; + + info!("ppp failed: {:?}", res); } - Timer::after(Duration::from_millis(10)).await; - } - }; + }; - with_timeout(timeout, fut) - .await - .map_err(|_| Error::Timeout)?; + let network_fut = async { + stack.wait_config_up().await; - self.at - .send(crate::command::system::SetEcho { - on: crate::command::system::types::EchoOn::Off, - }) - .await?; + 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, + ); - Ok(()) - } + let endpoint = stack.config_v4().unwrap(); - pub async fn is_link_up(&mut self) -> Result { - // Determine link state - let link_state = match self.wifi_connection { - Some(ref conn) - if conn.network_up && matches!(conn.wifi_state, WiFiState::Connected) => - { - LinkState::Up - } - _ => LinkState::Down, - }; + info!("Socket bound!"); + socket + .bind((Ipv4Address::new(172, 30, 0, 252), AtUdpSocket::PPP_AT_PORT)) + .unwrap(); - self.ch.set_link_state(link_state); + let at_socket = AtUdpSocket(socket); - Ok(link_state == LinkState::Up) - } + let at_client = + atat::asynch::Client::new(&at_socket, self.res_slot, self.cmd_buf, at_config); - pub async fn run(&mut self) -> ! { - loop { - let wait_link_up = { - let event = self.urc_subscription.next_message_pure().await; - - #[cfg(feature = "edm")] - let Some(event) = event.extract_urc() else { - continue; - }; - - match event { - Urc::StartUp => { - error!("AT startup event?! Device restarted unintentionally!"); - false - } - Urc::WifiLinkConnected(WifiLinkConnected { - connection_id: _, - bssid, - channel, - }) => { - if let Some(ref mut con) = self.wifi_connection { - con.wifi_state = WiFiState::Connected; - con.network.bssid = bssid; - con.network.channel = channel; - } else { - debug!("[URC] Active network config discovered"); - self.wifi_connection.replace( - WifiConnection::new( - WifiNetwork::new_station(bssid, channel), - WiFiState::Connected, - 255, - ) - .activate(), - ); - } - true - } - Urc::WifiLinkDisconnected(WifiLinkDisconnected { reason, .. }) => { - if let Some(ref mut con) = self.wifi_connection { - match reason { - DisconnectReason::NetworkDisabled => { - con.wifi_state = WiFiState::Inactive; - } - DisconnectReason::SecurityProblems => { - error!("Wifi Security Problems"); - con.wifi_state = WiFiState::NotConnected; - } - _ => { - con.wifi_state = WiFiState::NotConnected; - } - } - } + let mut wifi_device = + NetDevice::new(&self.ch, &mut self.config, at_client, self.urc_channel); - true - } - Urc::WifiAPUp(_) => todo!(), - Urc::WifiAPDown(_) => todo!(), - Urc::WifiAPStationConnected(_) => todo!(), - Urc::WifiAPStationDisconnected(_) => todo!(), - Urc::EthernetLinkUp(_) => todo!(), - Urc::EthernetLinkDown(_) => todo!(), - Urc::NetworkUp(NetworkUp { interface_id }) => { - drop(event); - self.network_status_callback(interface_id).await.ok(); - true - } - Urc::NetworkDown(NetworkDown { interface_id }) => { - drop(event); - self.network_status_callback(interface_id).await.ok(); - true - } - Urc::NetworkError(_) => todo!(), - _ => false, - } + embassy_futures::join::join(self.ingress.read_from(&at_socket), wifi_device.run()) + .await; }; - if wait_link_up { - self.is_link_up().await.unwrap(); + match embassy_futures::select::select(ppp_fut, network_fut).await { + Either::First(_) => { + warn!("Breaking to reboot module from PPP"); + } + Either::Second(_) => { + warn!("Breaking to reboot module from network runner"); + } } } } - - async fn network_status_callback(&mut self, interface_id: u8) -> Result<(), Error> { - let NetworkStatusResponse { - status: NetworkStatus::InterfaceType(InterfaceType::WifiStation), - .. - } = self - .at - .send(GetNetworkStatus { - interface_id, - status: NetworkStatusParameter::InterfaceType, - }) - .await? - else { - return Err(Error::Network); - }; - - let NetworkStatusResponse { - status: NetworkStatus::Gateway(ipv4), - .. - } = self - .at - .send(GetNetworkStatus { - interface_id, - status: NetworkStatusParameter::Gateway, - }) - .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(); - - let NetworkStatusResponse { - status: NetworkStatus::IPv6LinkLocalAddress(ipv6), - .. - } = self - .at - .send(GetNetworkStatus { - interface_id, - status: NetworkStatusParameter::IPv6LinkLocalAddress, - }) - .await? - else { - return Err(Error::Network); - }; - - let ipv6_up = core::str::from_utf8(ipv6.as_slice()) - .ok() - .and_then(|s| Ipv6Addr::from_str(s).ok()) - .map(|ip| !ip.is_unspecified()) - .unwrap_or_default(); - - // Use `ipv4_up` & `ipv6_up` to determine link state - if let Some(ref mut con) = self.wifi_connection { - con.network_up = ipv4_up && ipv6_up; - } - - Ok(()) - } } diff --git a/src/asynch/state.rs b/src/asynch/state.rs index 816e1ac..8ec431e 100644 --- a/src/asynch/state.rs +++ b/src/asynch/state.rs @@ -1,15 +1,15 @@ #![allow(dead_code)] use core::cell::RefCell; -use core::mem::MaybeUninit; -use core::task::Context; +use core::future::poll_fn; +use core::task::{Context, Poll}; -use atat::asynch::AtatClient; -use atat::UrcSubscription; 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))] @@ -20,43 +20,40 @@ pub enum LinkState { Up, } -use super::{AtHandle, UbloxUrc}; - pub struct State { - inner: MaybeUninit, + shared: Mutex>, } impl State { pub const fn new() -> Self { Self { - inner: MaybeUninit::uninit(), + shared: Mutex::new(RefCell::new(Shared { + link_state: LinkState::Down, + wifi_connection: WifiConnection::new(), + state_waker: WakerRegistration::new(), + connection_waker: WakerRegistration::new(), + })), } } } -struct StateInner { - shared: Mutex>, -} - /// State of the LinkState pub struct Shared { link_state: LinkState, - waker: WakerRegistration, + wifi_connection: WifiConnection, + state_waker: WakerRegistration, + connection_waker: WakerRegistration, } +#[derive(Clone)] pub struct Runner<'d> { shared: &'d Mutex>, } -#[derive(Clone, Copy)] -pub struct StateRunner<'d> { - shared: &'d Mutex>, -} - impl<'d> Runner<'d> { - pub fn state_runner(&self) -> StateRunner<'d> { - StateRunner { - shared: self.shared, + pub fn new(state: &'d mut State) -> Self { + Self { + shared: &state.shared, } } @@ -64,96 +61,62 @@ impl<'d> Runner<'d> { self.shared.lock(|s| { let s = &mut *s.borrow_mut(); s.link_state = state; - s.waker.wake(); + s.state_waker.wake(); }); } -} -impl<'d> StateRunner<'d> { - pub fn set_link_state(&self, state: LinkState) { + pub fn link_state(&mut self, cx: &mut Context) -> LinkState { self.shared.lock(|s| { let s = &mut *s.borrow_mut(); - s.link_state = state; - s.waker.wake(); - }); + s.state_waker.register(cx.waker()); + s.link_state + }) } - pub fn link_state(&mut self, cx: &mut Context) -> LinkState { + pub fn update_connection_with(&self, f: impl FnOnce(&mut WifiConnection)) { self.shared.lock(|s| { let s = &mut *s.borrow_mut(); - s.waker.register(cx.waker()); - s.link_state + f(&mut s.wifi_connection); + info!( + "Connection status changed! Connected: {:?}", + s.wifi_connection.is_connected() + ); + + if s.wifi_connection.network_up + && matches!(s.wifi_connection.wifi_state, WiFiState::Connected) + { + s.link_state = LinkState::Up; + } else { + s.link_state = LinkState::Down; + } + + s.state_waker.wake(); + s.connection_waker.wake(); }) } -} -pub fn new<'d, AT: AtatClient, const URC_CAPACITY: usize>( - state: &'d mut State, - at: AtHandle<'d, AT>, - urc_subscription: UrcSubscription<'d, UbloxUrc, URC_CAPACITY, 2>, -) -> (Runner<'d>, Device<'d, AT, URC_CAPACITY>) { - // safety: this is a self-referential struct, however: - // - it can't move while the `'d` borrow is active. - // - when the borrow ends, the dangling references inside the MaybeUninit will never be used again. - let state_uninit: *mut MaybeUninit = - (&mut state.inner as *mut MaybeUninit).cast(); - - let state = unsafe { &mut *state_uninit }.write(StateInner { - shared: Mutex::new(RefCell::new(Shared { - link_state: LinkState::Down, - waker: WakerRegistration::new(), - })), - }); - - ( - Runner { - shared: &state.shared, - }, - Device { - shared: TestShared { - inner: &state.shared, - }, - urc_subscription, - at, - }, - ) -} - -pub fn new_ppp<'d>(state: &'d mut State) -> Runner<'d> { - // safety: this is a self-referential struct, however: - // - it can't move while the `'d` borrow is active. - // - when the borrow ends, the dangling references inside the MaybeUninit will never be used again. - let state_uninit: *mut MaybeUninit = - (&mut state.inner as *mut MaybeUninit).cast(); - - let state = unsafe { &mut *state_uninit }.write(StateInner { - shared: Mutex::new(RefCell::new(Shared { - link_state: LinkState::Down, - waker: WakerRegistration::new(), - })), - }); - - Runner { - shared: &state.shared, + pub 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() + }) } -} - -pub struct TestShared<'d> { - inner: &'d Mutex>, -} - -pub struct Device<'d, AT: AtatClient, const URC_CAPACITY: usize> { - pub(crate) shared: TestShared<'d>, - pub(crate) at: AtHandle<'d, AT>, - pub(crate) urc_subscription: UrcSubscription<'d, UbloxUrc, URC_CAPACITY, 2>, -} -impl<'d> TestShared<'d> { - pub fn link_state(&mut self, cx: &mut Context) -> LinkState { - self.inner.lock(|s| { - let s = &mut *s.borrow_mut(); - s.waker.register(cx.waker()); - s.link_state + pub async fn wait_connection_change(&mut self) -> bool { + let old_state = self + .shared + .lock(|s| s.borrow().wifi_connection.is_connected()); + + poll_fn(|cx| { + let current_state = self.is_connected(Some(cx)); + if current_state != old_state { + return Poll::Ready(current_state); + } + Poll::Pending }) + .await } } diff --git a/src/asynch/ublox_stack/tcp.rs b/src/asynch/ublox_stack/tcp.rs index aef9f89..6283ff6 100644 --- a/src/asynch/ublox_stack/tcp.rs +++ b/src/asynch/ublox_stack/tcp.rs @@ -750,7 +750,7 @@ pub mod client { } } - impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_io::ErrorType + 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; diff --git a/src/asynch/ublox_stack/tls.rs b/src/asynch/ublox_stack/tls.rs index 55434d2..7caa78a 100644 --- a/src/asynch/ublox_stack/tls.rs +++ b/src/asynch/ublox_stack/tls.rs @@ -406,7 +406,7 @@ pub mod client { } } - impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_io::ErrorType + 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; diff --git a/src/command/custom_digest.rs b/src/command/custom_digest.rs index d591a08..1db8831 100644 --- a/src/command/custom_digest.rs +++ b/src/command/custom_digest.rs @@ -112,20 +112,6 @@ impl Digester for EdmDigester { // struct MockWriter; -// impl embedded_io::Io for MockWriter { -// type Error = (); -// } - -// impl embedded_io::blocking::Write for MockWriter { -// fn write(&mut self, buf: &[u8]) -> Result { -// Ok(buf.len()) -// } - -// fn flush(&mut self) -> Result<(), Self::Error> { -// Ok(()) -// } -// } - // /// Removed functionality used to change OK responses to empty responses. // #[test] // fn ok_response<'a>() { diff --git a/src/command/network/mod.rs b/src/command/network/mod.rs index c8e401b..81d4c8a 100644 --- a/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 = 3000)] +#[at_cmd("+UNSTAT", NetworkStatusResponse, attempts = 3, timeout_ms = 1000)] pub struct GetNetworkStatus { #[at_arg(position = 0)] pub interface_id: u8, diff --git a/src/command/system/mod.rs b/src/command/system/mod.rs index ec1ac13..d3403a0 100644 --- a/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 /// diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..8589684 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,17 @@ +use embedded_hal::digital::OutputPin; + +pub trait WifiConfig<'a> { + type ResetPin: OutputPin; + + const FLOW_CONTROL: bool = false; + + const TLS_IN_BUFFER_SIZE: Option = None; + 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 + } +} diff --git a/src/connection.rs b/src/connection.rs index c270887..8136e8d 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -14,7 +14,7 @@ pub struct WifiConnection { /// Keeps track of connection state on module pub wifi_state: WiFiState, pub network_up: bool, - pub network: WifiNetwork, + pub network: Option, /// Number from 0-9. 255 used for unknown pub config_id: u8, /// Keeps track of activation of the config by driver @@ -22,30 +22,28 @@ pub struct WifiConnection { } impl WifiConnection { - pub(crate) fn new(network: WifiNetwork, wifi_state: WiFiState, config_id: u8) -> Self { + pub(crate) const fn new() -> Self { WifiConnection { - wifi_state, + wifi_state: WiFiState::Inactive, network_up: false, - network, - config_id, + network: None, + config_id: 255, activated: false, } } pub fn is_station(&self) -> bool { - self.network.mode == WifiMode::Station + match self.network { + Some(ref n) => n.mode == WifiMode::Station, + _ => false, + } } 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; + pub fn is_connected(&self) -> bool { + self.network_up } } diff --git a/src/lib.rs b/src/lib.rs index 180e4fb..b7375c2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,10 +4,21 @@ #[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 chip 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 peer_builder; @@ -19,16 +30,5 @@ pub use atat; pub mod command; pub mod error; // pub mod wifi; +pub use config::WifiConfig; pub use peer_builder::SecurityCredentials; - -// TODO: -// - UDP stack -// - Secure sockets -// - Network scan -// - AP Mode (control) -// - TCP listener stack -// - -// -// FIXME: -// - PWR/Restart stuff doesn't fully work -// - From 967d75fffa34065749b0f2f8bdb272052d2944f6 Mon Sep 17 00:00:00 2001 From: Mathias Date: Mon, 17 Jun 2024 14:51:50 +0200 Subject: [PATCH 3/7] Bump embassy-sync to 0.6 --- Cargo.toml | 2 +- examples/rpi-pico/Cargo.toml | 3 +- examples/rpi-pico/src/bin/embassy-perf.rs | 65 +++++++++------ .../rpi-pico/src/bin/embassy-smoltcp-ppp.rs | 81 ++++++++++++++----- 4 files changed, 105 insertions(+), 46 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c8b21f0..df7be4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ defmt = { version = "^0.3", optional = true } embedded-hal = "1.0" embassy-time = "0.3" -embassy-sync = "0.5" +embassy-sync = "0.6" embassy-futures = "0.1" embedded-nal-async = { version = "0.7" } diff --git a/examples/rpi-pico/Cargo.toml b/examples/rpi-pico/Cargo.toml index a07f340..4a9d2a1 100644 --- a/examples/rpi-pico/Cargo.toml +++ b/examples/rpi-pico/Cargo.toml @@ -17,7 +17,7 @@ embassy-time = { version = "0.3", features = [ "defmt", "defmt-timestamp-uptime", ] } -embassy-sync = { version = "0.5" } +embassy-sync = { version = "0.6" } embassy-rp = { version = "0.1.0", features = [ "defmt", "unstable-pac", @@ -80,6 +80,7 @@ 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 diff --git a/examples/rpi-pico/src/bin/embassy-perf.rs b/examples/rpi-pico/src/bin/embassy-perf.rs index f990655..0376907 100644 --- a/examples/rpi-pico/src/bin/embassy-perf.rs +++ b/examples/rpi-pico/src/bin/embassy-perf.rs @@ -61,40 +61,57 @@ async fn main(spawner: Spawner) { let p = embassy_rp::init(Default::default()); - let rst = Output::new(p.PIN_26, Level::High); - - let (tx_pin, rx_pin, rts_pin, cts_pin, uart) = - (p.PIN_24, p.PIN_25, p.PIN_23, p.PIN_22, p.UART1); - - let tx_buf = &mut make_static!([0u8; 64])[..]; - let rx_buf = &mut make_static!([0u8; 64])[..]; - let mut config = uart::Config::default(); - config.baudrate = 115200; - let uart = uart::BufferedUart::new_with_rtscts( - uart, Irqs, tx_pin, rx_pin, rts_pin, cts_pin, tx_buf, rx_buf, config, + 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(), ); - let (rx, tx) = uart.split(); - let buffers = &*make_static!(atat::Buffers::new()); - let (ingress, client) = buffers.split( - common::TxWrap(tx), - EdmDigester::default(), - atat::Config::new(), + static RESOURCES: StaticCell> = + StaticCell::new(); + + let mut runner = Runner::new( + wifi_uart.split(), + RESOURCES.init(Resources::new()), + WifiConfig { rst_pin }, ); - defmt::unwrap!(spawner.spawn(ingress_task(ingress, rx))); - let state = make_static!(State::new(client)); - let (net_device, mut control, runner) = new(state, &buffers.urc_channel, rst).await; + static PPP_STATE: StaticCell> = StaticCell::new(); + let net_device = runner.ppp_stack(PPP_STATE.init(embassy_net_ppp::State::new())); - defmt::unwrap!(spawner.spawn(wifi_task(runner))); + // Generate random seed + let seed = 0x0123_4567_89ab_cdef; // chosen by fair dice roll. guarenteed to be random. // Init network stack - let stack = &*make_static!(UbloxStack::new( + static STACK: StaticCell>> = StaticCell::new(); + static STACK_RESOURCES: StaticCell> = StaticCell::new(); + + let stack = &*STACK.init(Stack::new( net_device, - make_static!(StackResources::<4>::new()), + embassy_net::Config::default(), + STACK_RESOURCES.init(StackResources::new()), + seed, )); - defmt::unwrap!(spawner.spawn(net_task(stack))); + 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 { diff --git a/examples/rpi-pico/src/bin/embassy-smoltcp-ppp.rs b/examples/rpi-pico/src/bin/embassy-smoltcp-ppp.rs index 2991638..21c9416 100644 --- a/examples/rpi-pico/src/bin/embassy-smoltcp-ppp.rs +++ b/examples/rpi-pico/src/bin/embassy-smoltcp-ppp.rs @@ -1,6 +1,7 @@ #![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"); @@ -9,16 +10,16 @@ use defmt::*; use embassy_executor::Spawner; use embassy_net::tcp::TcpSocket; use embassy_net::{Ipv4Address, Stack, StackResources}; -use embassy_rp::gpio::{AnyPin, Level, Output, Pin}; +use embassy_rp::gpio::{AnyPin, Level, Output, OutputOpenDrain, Pin}; use embassy_rp::peripherals::UART1; -use embassy_rp::uart::{BufferedInterruptHandler, BufferedUart}; +use embassy_rp::uart::{BufferedInterruptHandler, BufferedUart, BufferedUartRx, BufferedUartTx}; use embassy_rp::{bind_interrupts, uart}; use embassy_time::{Duration, Timer}; -use embedded_tls::Aes128GcmSha256; 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; @@ -26,13 +27,31 @@ use reqwless::request::Request; use reqwless::request::RequestBuilder as _; use reqwless::response::Response; use static_cell::StaticCell; -use ublox_short_range::asynch::{PPPRunner, Resources}; +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 @@ -40,11 +59,17 @@ async fn net_task(stack: &'static Stack>) -> ! #[embassy_executor::task] async fn ppp_task( - mut runner: PPPRunner<'static, Output<'static>, INGRESS_BUF_SIZE, URC_CAPACITY>, - interface: BufferedUart<'static, UART1>, + mut runner: Runner< + 'static, + BufferedUartRx<'static, UART1>, + BufferedUartTx<'static, UART1>, + WifiConfig, + INGRESS_BUF_SIZE, + URC_CAPACITY, + >, stack: &'static embassy_net::Stack>, ) -> ! { - runner.run(interface, stack).await + runner.run(stack).await } bind_interrupts!(struct Irqs { @@ -55,7 +80,7 @@ bind_interrupts!(struct Irqs { async fn main(spawner: Spawner) { let p = embassy_rp::init(Default::default()); - let rst = Output::new(p.PIN_26.degrade(), Level::High); + 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(); @@ -74,15 +99,21 @@ async fn main(spawner: Spawner) { static RESOURCES: StaticCell> = StaticCell::new(); - let (net_device, mut control, runner) = - ublox_short_range::asynch::new_ppp(RESOURCES.init(Resources::new()), rst); + 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. guarenteed to be random. // Init network stack static STACK: StaticCell>> = StaticCell::new(); - static STACK_RESOURCES: StaticCell> = StaticCell::new(); + static STACK_RESOURCES: StaticCell> = StaticCell::new(); let stack = &*STACK.init(Stack::new( net_device, @@ -91,19 +122,19 @@ async fn main(spawner: Spawner) { 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, wifi_uart, &stack)).unwrap(); + spawner.spawn(ppp_task(runner, &stack)).unwrap(); stack.wait_config_up().await; Timer::after(Duration::from_secs(1)).await; - control - .set_hostname("Factbird-duo-wifi-test") - .await - .unwrap(); + control.set_hostname("Ublox-wifi-test").await.ok(); - control.join_open("Test").await; + control.join_wpa2("MyAccessPoint", "12345678").await; info!("We have network!"); @@ -129,7 +160,9 @@ async fn main(spawner: Spawner) { let mut read_record_buffer = [0; 16384]; let mut write_record_buffer = [0; 16384]; - let config = TlsConfig::new().with_server_name(hostname); + 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( @@ -147,9 +180,17 @@ async fn main(spawner: Spawner) { .build(); request.write(&mut tls).await.unwrap(); - let mut rx_buf = [0; 4096]; + 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(); - info!("{=[u8]:a}", rx_buf); + let len = response + .body() + .reader() + .read_to_end(&mut body_buf) + .await + .unwrap(); + + info!("{=[u8]:a}", &body_buf[..len]); } From bfdc2482efc711a21298852239e204f813bd6357 Mon Sep 17 00:00:00 2001 From: Mathias Date: Mon, 1 Jul 2024 20:10:59 +0200 Subject: [PATCH 4/7] Fix internal-network-stack compiling --- .vscode/settings.json | 1 + Cargo.toml | 2 +- Design_diagram.drawio | 1 - Design_diagram.png | Bin 135242 -> 0 bytes README.md | 26 +- rust-toolchain.toml | 2 +- src/asynch/mod.rs | 6 +- src/asynch/ublox_stack/device.rs | 11 + src/asynch/ublox_stack/dns.rs | 5 +- src/asynch/ublox_stack/mod.rs | 105 ++-- src/{ => asynch/ublox_stack}/peer_builder.rs | 0 src/asynch/ublox_stack/tcp.rs | 41 +- src/asynch/ublox_stack/tls.rs | 43 +- src/asynch/ublox_stack/udp.rs | 5 +- src/config.rs | 2 + src/connection.rs | 13 +- src/error.rs | 8 +- src/fmt.rs | 54 +- src/lib.rs | 3 - src/network.rs | 2 +- src/wifi/ap.rs | 227 ------- src/wifi/mod.rs | 86 --- src/wifi/options.rs | 137 ----- src/wifi/supplicant.rs | 603 ------------------- 24 files changed, 182 insertions(+), 1201 deletions(-) delete mode 100644 Design_diagram.drawio delete mode 100644 Design_diagram.png create mode 100644 src/asynch/ublox_stack/device.rs rename src/{ => asynch/ublox_stack}/peer_builder.rs (100%) delete mode 100644 src/wifi/ap.rs delete mode 100644 src/wifi/mod.rs delete mode 100644 src/wifi/options.rs delete mode 100644 src/wifi/supplicant.rs diff --git a/.vscode/settings.json b/.vscode/settings.json index b297366..1b2bf72 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,6 +8,7 @@ "rust-analyzer.linkedProjects": [], "rust-analyzer.cargo.features": [ "odin_w2xx", + // "internal-network-stack" "ppp" ], "rust-analyzer.server.extraEnv": { diff --git a/Cargo.toml b/Cargo.toml index df7be4c..5d31af1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ name = "ublox_short_range" doctest = false [dependencies] -atat = { version = "0.22", features = ["derive", "bytes"] } +atat = { version = "0.23", features = ["derive", "bytes"] } heapless = { version = "^0.8", features = ["serde"] } no-std-net = { version = "0.6", features = ["serde"] } 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/rust-toolchain.toml b/rust-toolchain.toml index b4220cb..1dca89f 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] -channel = "1.75" +channel = "1.79" components = [ "rust-src", "rustfmt", "llvm-tools" ] targets = [ "thumbv6m-none-eabi", diff --git a/src/asynch/mod.rs b/src/asynch/mod.rs index a6ef5fb..49ace50 100644 --- a/src/asynch/mod.rs +++ b/src/asynch/mod.rs @@ -1,18 +1,18 @@ +#[cfg(feature = "ppp")] mod at_udp_socket; pub mod control; pub mod network; mod resources; pub mod runner; -#[cfg(feature = "ublox-sockets")] +#[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 = "internal-network-stack")] -mod internal_stack; use embedded_io_async::{BufRead, Error as _, ErrorKind, Read, Write}; #[cfg(feature = "edm")] 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 index ab28ee2..b3aed1e 100644 --- a/src/asynch/ublox_stack/dns.rs +++ b/src/asynch/ublox_stack/dns.rs @@ -1,6 +1,5 @@ use core::{cell::RefCell, future::poll_fn, task::Poll}; -use atat::asynch::AtatClient; use embassy_sync::waitqueue::WakerRegistration; use embedded_nal_async::AddrType; use no_std_net::IpAddr; @@ -115,8 +114,8 @@ pub struct DnsSocket<'a> { impl<'a> DnsSocket<'a> { /// Create a new DNS socket using the provided stack. - pub fn new( - stack: &'a UbloxStack, + pub fn new( + stack: &'a UbloxStack, ) -> Self { Self { stack: &stack.socket, diff --git a/src/asynch/ublox_stack/mod.rs b/src/asynch/ublox_stack/mod.rs index b5f3b8d..06d9533 100644 --- a/src/asynch/ublox_stack/mod.rs +++ b/src/asynch/ublox_stack/mod.rs @@ -5,37 +5,37 @@ 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::asynch::state::Device; use crate::command::data_mode::responses::ConnectPeerResponse; use crate::command::data_mode::urc::PeerDisconnected; use crate::command::data_mode::{ClosePeerConnection, ConnectPeer}; use crate::command::edm::types::{DataEvent, Protocol}; use crate::command::edm::urc::EdmEvent; -use crate::command::edm::EdmDataCommand; +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 crate::peer_builder::{PeerUrlBuilder, SecurityCredentials}; +use peer_builder::{PeerUrlBuilder, SecurityCredentials}; use self::dns::{DnsSocket, DnsState, DnsTable}; -use super::state::{self, LinkState}; -use super::AtHandle; +use super::control::ProxyClient; -use atat::asynch::AtatClient; use embassy_futures::select; use embassy_sync::waitqueue::WakerRegistration; use embassy_time::{Duration, Ticker}; use embedded_nal_async::SocketAddr; -use futures::pin_mut; use no_std_net::IpAddr; use portable_atomic::{AtomicBool, AtomicU8, Ordering}; use ublox_sockets::{ @@ -68,15 +68,14 @@ impl StackResources { } } -pub struct UbloxStack { +pub struct UbloxStack { socket: RefCell, - device: RefCell>, + device: Device<'static, INGRESS_BUF_SIZE, URC_CAPACITY>, last_tx_socket: AtomicU8, should_tx: AtomicBool, - link_up: AtomicBool, } -struct SocketStack { +pub(crate) struct SocketStack { sockets: SocketSet<'static>, waker: WakerRegistration, dns_table: DnsTable, @@ -84,9 +83,11 @@ struct SocketStack { credential_map: heapless::FnvIndexMap, } -impl UbloxStack { +impl + UbloxStack +{ pub fn new( - device: state::Device<'static, AT, URC_CAPACITY>, + device: Device<'static, INGRESS_BUF_SIZE, URC_CAPACITY>, resources: &'static mut StackResources, ) -> Self { let sockets = SocketSet::new(&mut resources.sockets[..]); @@ -101,9 +102,8 @@ impl UbloxStack UbloxStack ! { 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) { @@ -126,46 +134,21 @@ impl UbloxStack Poll::Ready(LinkState::Down), - (false, LinkState::Up) => Poll::Ready(LinkState::Up), - _ => Poll::Pending, - }, - ), ) .await { - select::Either4::First(event) => { + select::Either3::First(event) => { Self::socket_rx(event, &self.socket); } - select::Either4::Second(_) | select::Either4::Third(_) => { + select::Either3::Second(_) | select::Either3::Third(_) => { if let Some(ev) = self.tx_event(&mut tx_buf) { - Self::socket_tx(ev, &self.socket, at).await; - } - } - select::Either4::Fourth(new_state) => { - // Update link up - let old_link_up = self.link_up.load(Ordering::Relaxed); - let new_link_up = new_state == LinkState::Up; - self.link_up.store(new_link_up, Ordering::Relaxed); - - // Print when changed - if old_link_up != new_link_up { - info!("link_up = {:?}", new_link_up); + Self::socket_tx(ev, &self.socket, &at_client).await; } } } @@ -325,13 +308,14 @@ impl UbloxStack UbloxStack( ev: TxEvent<'data>, socket: &RefCell, - at: &mut AtHandle<'_, AT>, + at_client: &RefCell>, ) { + use atat::asynch::AtatClient; + + let mut at = at_client.borrow_mut(); match ev { TxEvent::Connect { socket_handle, url } => { - match at.send_edm(ConnectPeer { url: &url }).await { + match at.send(&EdmAtCmdWrapper(ConnectPeer { url: &url })).await { Ok(ConnectPeerResponse { peer_handle }) => { let mut s = socket.borrow_mut(); let tcp = s @@ -436,7 +423,7 @@ impl UbloxStack { warn!("Sending {} bytes on {}", data.len(), edm_channel); - at.send(EdmDataCommand { + at.send(&EdmDataCommand { channel: edm_channel, data, }) @@ -444,14 +431,16 @@ impl UbloxStack { - at.send_edm(ClosePeerConnection { peer_handle }).await.ok(); + at.send(&EdmAtCmdWrapper(ClosePeerConnection { peer_handle })) + .await + .ok(); } TxEvent::Dns { hostname } => { match at - .send_edm(Ping { + .send(&EdmAtCmdWrapper(Ping { hostname: &hostname, retry_num: 1, - }) + })) .await { Ok(_) => {} diff --git a/src/peer_builder.rs b/src/asynch/ublox_stack/peer_builder.rs similarity index 100% rename from src/peer_builder.rs rename to src/asynch/ublox_stack/peer_builder.rs diff --git a/src/asynch/ublox_stack/tcp.rs b/src/asynch/ublox_stack/tcp.rs index 6283ff6..8706313 100644 --- a/src/asynch/ublox_stack/tcp.rs +++ b/src/asynch/ublox_stack/tcp.rs @@ -3,7 +3,6 @@ use core::future::poll_fn; use core::mem; use core::task::Poll; -use atat::asynch::AtatClient; use embassy_time::Duration; use embedded_nal_async::SocketAddr; use ublox_sockets::{tcp, SocketHandle, TcpState}; @@ -123,8 +122,8 @@ impl<'a> TcpWriter<'a> { impl<'a> TcpSocket<'a> { /// Create a new TCP socket on the given stack, with the given buffers. - pub fn new( - stack: &'a UbloxStack, + pub fn new( + stack: &'a UbloxStack, rx_buffer: &'a mut [u8], tx_buffer: &'a mut [u8], ) -> Self { @@ -331,7 +330,7 @@ impl<'a> TcpSocket<'a> { self.io.with(|s| s.may_send()) } - /// return whether the recieve half of the full-duplex connection is open. + /// 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. @@ -481,7 +480,7 @@ impl<'d> TcpIo<'d> { s.register_recv_waker(cx.waker()); Poll::Pending } else { - // if we can't receive because the recieve half of the duplex connection is closed then return an error + // 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 { @@ -631,24 +630,25 @@ pub mod client { /// 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, - AT: AtatClient + 'static, - const N: usize, + 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) stack: &'d UbloxStack, pub(crate) state: &'d TcpClientState, } impl< 'd, - AT: AtatClient, - const N: usize, + 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, AT, N, URC_CAPACITY, TX_SZ, RX_SZ> + > 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; @@ -671,16 +671,16 @@ pub mod client { impl< 'd, - AT: AtatClient, - const N: usize, + const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize, + const N: usize, const TX_SZ: usize, const RX_SZ: usize, - > TcpClient<'d, AT, N, URC_CAPACITY, TX_SZ, RX_SZ> + > TcpClient<'d, INGRESS_BUF_SIZE, URC_CAPACITY, N, TX_SZ, RX_SZ> { /// Create a new `TcpClient`. pub fn new( - stack: &'d UbloxStack, + stack: &'d UbloxStack, state: &'d TcpClientState, ) -> Self { Self { stack, state } @@ -689,12 +689,13 @@ pub mod client { impl< 'd, - AT: AtatClient, - const N: usize, + 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, AT, N, URC_CAPACITY, TX_SZ, RX_SZ> + > 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; @@ -724,8 +725,8 @@ pub mod client { 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, + fn new( + stack: &'d UbloxStack, state: &'d TcpClientState, ) -> Result { let mut bufs = state.pool.alloc().ok_or(Error::ConnectionReset)?; diff --git a/src/asynch/ublox_stack/tls.rs b/src/asynch/ublox_stack/tls.rs index 7caa78a..327f811 100644 --- a/src/asynch/ublox_stack/tls.rs +++ b/src/asynch/ublox_stack/tls.rs @@ -1,9 +1,8 @@ -use atat::asynch::AtatClient; use embassy_time::Duration; use no_std_net::SocketAddr; use ublox_sockets::TcpState as State; -use crate::peer_builder::SecurityCredentials; +use super::peer_builder::SecurityCredentials; use super::{ tcp::{ConnectError, Error, TcpIo, TcpReader, TcpSocket, TcpWriter}, @@ -16,8 +15,8 @@ pub struct TlsSocket<'a> { impl<'a> TlsSocket<'a> { /// Create a new TCP socket on the given stack, with the given buffers. - pub fn new( - stack: &'a UbloxStack, + pub fn new( + stack: &'a UbloxStack, rx_buffer: &'a mut [u8], tx_buffer: &'a mut [u8], credentials: SecurityCredentials, @@ -28,7 +27,7 @@ impl<'a> TlsSocket<'a> { let s = &mut *stack.borrow_mut(); info!("Associating credentials {} with {}", credentials, handle); - s.credential_map.insert(handle, credentials); + s.credential_map.insert(handle, credentials).unwrap(); Self { inner: tcp_socket } } @@ -204,7 +203,7 @@ impl<'a> TlsSocket<'a> { self.inner.may_send() } - /// return whether the recieve half of the full-duplex connection is open. + /// 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. @@ -275,25 +274,26 @@ pub mod client { /// 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, - AT: AtatClient + 'static, - const N: usize, + 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) stack: &'d UbloxStack, pub(crate) state: &'d TcpClientState, pub(crate) credentials: SecurityCredentials, } impl< 'd, - AT: AtatClient, - const N: usize, + 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, AT, N, URC_CAPACITY, TX_SZ, RX_SZ> + > 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; @@ -316,16 +316,16 @@ pub mod client { impl< 'd, - AT: AtatClient, - const N: usize, + const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize, + const N: usize, const TX_SZ: usize, const RX_SZ: usize, - > TlsClient<'d, AT, N, URC_CAPACITY, TX_SZ, RX_SZ> + > TlsClient<'d, INGRESS_BUF_SIZE, URC_CAPACITY, N, TX_SZ, RX_SZ> { /// Create a new `TlsClient`. pub fn new( - stack: &'d UbloxStack, + stack: &'d UbloxStack, state: &'d TcpClientState, credentials: SecurityCredentials, ) -> Self { @@ -339,12 +339,13 @@ pub mod client { impl< 'd, - AT: AtatClient, - const N: usize, + 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, AT, N, URC_CAPACITY, TX_SZ, RX_SZ> + > 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; @@ -374,8 +375,8 @@ pub mod client { 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, + fn new( + stack: &'d UbloxStack, state: &'d TcpClientState, credentials: SecurityCredentials, ) -> Result { diff --git a/src/asynch/ublox_stack/udp.rs b/src/asynch/ublox_stack/udp.rs index 8348b3d..f561f69 100644 --- a/src/asynch/ublox_stack/udp.rs +++ b/src/asynch/ublox_stack/udp.rs @@ -3,7 +3,6 @@ use core::cell::RefCell; use core::mem; -use atat::asynch::AtatClient; use embedded_nal_async::SocketAddr; use ublox_sockets::{udp, SocketHandle, UdpState}; @@ -45,8 +44,8 @@ pub struct UdpSocket<'a> { impl<'a> UdpSocket<'a> { /// Create a new UDP socket using the provided stack and buffers. - pub fn new( - stack: &'a UbloxStack, + pub fn new( + stack: &'a UbloxStack, rx_buffer: &'a mut [u8], tx_buffer: &'a mut [u8], ) -> Self { diff --git a/src/config.rs b/src/config.rs index 8589684..44fce23 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,7 +5,9 @@ pub trait WifiConfig<'a> { const FLOW_CONTROL: bool = false; + #[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")] diff --git a/src/connection.rs b/src/connection.rs index 8136e8d..a309f11 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -6,19 +6,14 @@ pub enum WiFiState { Inactive, /// Searching for Wifi NotConnected, + SecurityProblems, Connected, } -// Fold into wifi connectivity pub struct WifiConnection { - /// Keeps track of connection state on module pub wifi_state: WiFiState, pub network_up: bool, pub network: Option, - /// Number from 0-9. 255 used for unknown - pub config_id: u8, - /// Keeps track of activation of the config by driver - pub activated: bool, } impl WifiConnection { @@ -27,11 +22,10 @@ impl WifiConnection { wifi_state: WiFiState::Inactive, network_up: false, network: None, - config_id: 255, - activated: false, } } + #[allow(dead_code)] pub fn is_station(&self) -> bool { match self.network { Some(ref n) => n.mode == WifiMode::Station, @@ -39,11 +33,12 @@ impl WifiConnection { } } + #[allow(dead_code)] pub fn is_access_point(&self) -> bool { !self.is_station() } pub fn is_connected(&self) -> bool { - self.network_up + self.network_up && self.wifi_state == WiFiState::Connected } } diff --git a/src/error.rs b/src/error.rs index c770c9b..ea7bf9d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,4 @@ -#[cfg(feature = "ublox-sockets")] +#[cfg(feature = "internal-network-stack")] pub use ublox_sockets::Error as SocketError; #[derive(Debug)] @@ -18,7 +18,7 @@ pub enum Error { // NetworkState(crate::wifi::connection::NetworkState), NoWifiSetup, // WifiState(crate::wifi::connection::WiFiState), - #[cfg(feature = "ublox-sockets")] + #[cfg(feature = "internal-network-stack")] Socket(ublox_sockets::Error), AT(atat::Error), Busy, @@ -42,7 +42,7 @@ impl From for Error { } } -#[cfg(feature = "ublox-sockets")] +#[cfg(feature = "internal-network-stack")] impl From for Error { fn from(e: ublox_sockets::Error) -> Self { Error::Socket(e) @@ -97,7 +97,7 @@ pub enum WifiError { #[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 index 81c9940..35b929f 100644 --- a/src/fmt.rs +++ b/src/fmt.rs @@ -1,11 +1,12 @@ #![macro_use] -#![allow(unused_macros)] +#![allow(unused)] -use core::fmt::Debug; +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)*) => { { @@ -17,6 +18,7 @@ macro_rules! assert { }; } +#[collapse_debuginfo(yes)] macro_rules! assert_eq { ($($x:tt)*) => { { @@ -28,6 +30,7 @@ macro_rules! assert_eq { }; } +#[collapse_debuginfo(yes)] macro_rules! assert_ne { ($($x:tt)*) => { { @@ -39,39 +42,43 @@ macro_rules! assert_ne { }; } +#[collapse_debuginfo(yes)] macro_rules! debug_assert { ($($x:tt)*) => { { #[cfg(not(feature = "defmt"))] ::core::debug_assert!($($x)*); #[cfg(feature = "defmt")] - ::debug_assert!($($x)*); + ::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")] - ::debug_assert_eq!($($x)*); + ::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")] - ::debug_assert_ne!($($x)*); + ::defmt::debug_assert_ne!($($x)*); } }; } +#[collapse_debuginfo(yes)] macro_rules! todo { ($($x:tt)*) => { { @@ -84,6 +91,7 @@ macro_rules! todo { } #[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] macro_rules! unreachable { ($($x:tt)*) => { ::core::unreachable!($($x)*) @@ -91,12 +99,14 @@ macro_rules! unreachable { } #[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] macro_rules! unreachable { ($($x:tt)*) => { ::defmt::unreachable!($($x)*) }; } +#[collapse_debuginfo(yes)] macro_rules! panic { ($($x:tt)*) => { { @@ -108,6 +118,7 @@ macro_rules! panic { }; } +#[collapse_debuginfo(yes)] macro_rules! trace { ($s:literal $(, $x:expr)* $(,)?) => { { @@ -121,6 +132,7 @@ macro_rules! trace { }; } +#[collapse_debuginfo(yes)] macro_rules! debug { ($s:literal $(, $x:expr)* $(,)?) => { { @@ -134,6 +146,7 @@ macro_rules! debug { }; } +#[collapse_debuginfo(yes)] macro_rules! info { ($s:literal $(, $x:expr)* $(,)?) => { { @@ -147,6 +160,7 @@ macro_rules! info { }; } +#[collapse_debuginfo(yes)] macro_rules! warn { ($s:literal $(, $x:expr)* $(,)?) => { { @@ -160,6 +174,7 @@ macro_rules! warn { }; } +#[collapse_debuginfo(yes)] macro_rules! error { ($s:literal $(, $x:expr)* $(,)?) => { { @@ -174,6 +189,7 @@ macro_rules! error { } #[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] macro_rules! unwrap { ($($x:tt)*) => { ::defmt::unwrap!($($x)*) @@ -181,6 +197,7 @@ macro_rules! unwrap { } #[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] macro_rules! unwrap { ($arg:expr) => { match $crate::fmt::Try::into_result($arg) { @@ -228,3 +245,30 @@ impl Try for Result { self } } + +pub(crate) struct Bytes<'a>(pub &'a [u8]); + +impl<'a> Debug for Bytes<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +impl<'a> Display for Bytes<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +impl<'a> LowerHex for Bytes<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +#[cfg(feature = "defmt")] +impl<'a> defmt::Format for Bytes<'a> { + fn format(&self, fmt: defmt::Formatter) { + defmt::write!(fmt, "{:02x}", self.0) + } +} diff --git a/src/lib.rs b/src/lib.rs index b7375c2..b8e84fa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,7 +21,6 @@ pub mod asynch; mod config; mod connection; mod network; -mod peer_builder; mod hex; @@ -29,6 +28,4 @@ pub use atat; pub mod command; pub mod error; -// pub mod wifi; pub use config::WifiConfig; -pub use peer_builder::SecurityCredentials; diff --git a/src/network.rs b/src/network.rs index e980dc9..eafa25d 100644 --- a/src/network.rs +++ b/src/network.rs @@ -6,7 +6,7 @@ use heapless::String; use core::convert::TryFrom; -#[derive(PartialEq, Debug)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum WifiMode { Station, AccessPoint, diff --git a/src/wifi/ap.rs b/src/wifi/ap.rs deleted file mode 100644 index 5995140..0000000 --- a/src/wifi/ap.rs +++ /dev/null @@ -1,227 +0,0 @@ -use crate::{ - blocking::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 UbloxClient -where - C: 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, - ap_config_id as u8, - ) - .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/src/wifi/mod.rs b/src/wifi/mod.rs deleted file mode 100644 index 15f6b48..0000000 --- a/src/wifi/mod.rs +++ /dev/null @@ -1,86 +0,0 @@ -pub use ublox_sockets::{PeerHandle, SocketHandle}; - -use crate::command::edm::types::ChannelId; - -pub mod ap; -pub mod connection; -pub mod network; -pub mod options; -pub mod supplicant; - -pub mod peer_builder; - -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 struct SocketMap { - channel_map: heapless::FnvIndexMap, - peer_map: heapless::FnvIndexMap, -} - -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<(), ()> { - trace!("[SOCK_MAP] {:?} tied to {:?}", socket_handle, channel_id); - self.channel_map - .insert(channel_id, socket_handle) - .map_err(drop)?; - Ok(()) - } - - pub fn remove_channel(&mut self, channel_id: &ChannelId) -> Result<(), ()> { - trace!("[SOCK_MAP] {:?} removed", channel_id); - self.channel_map.remove(channel_id).ok_or(())?; - Ok(()) - } - - 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<(), ()> { - trace!("[SOCK_MAP] {:?} tied to {:?}", socket_handle, peer); - self.peer_map.insert(peer, socket_handle).map_err(drop)?; - Ok(()) - } - - pub fn remove_peer(&mut self, peer: &PeerHandle) -> Result<(), ()> { - trace!("[SOCK_MAP] {:?} removed", peer); - self.peer_map.remove(peer).ok_or(())?; - Ok(()) - } - - 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/src/wifi/options.rs b/src/wifi/options.rs deleted file mode 100644 index aac9dd3..0000000 --- a/src/wifi/options.rs +++ /dev/null @@ -1,137 +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)] -#[cfg_attr(feature = "defmt", derive(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/src/wifi/supplicant.rs b/src/wifi/supplicant.rs deleted file mode 100644 index dabd5bf..0000000 --- a/src/wifi/supplicant.rs +++ /dev/null @@ -1,603 +0,0 @@ -use heapless::Vec; - -use crate::{ - command::{ - edm::EdmAtCmdWrapper, - system::RebootDCE, - wifi::{ - responses::GetWifiStationConfigResponse, - types::{ - Authentication, IPv4Mode, WifiStationAction, WifiStationConfig, - WifiStationConfigParameter, WifiStationConfigR, - }, - ExecWifiStationAction, GetWifiStationConfig, SetWifiStationConfig, WifiScan, - }, - }, - error::{Error, WifiConnectionError, WifiError}, -}; - -use super::{ - connection::{WiFiState, WifiConnection}, - network::WifiNetwork, - options::ConnectionOptions, -}; - -use debug; - -/// Supplicant is used to -/// -/// -/// ``` -/// // Add, activate and remove network -/// let network = ConnectionOptions::new().ssid("my-ssid").password("hunter2"); -/// let config_id: u8 = 0; -/// let mut supplicant = ublox.supplicant::().unwrap(); -/// -/// supplicant.upsert_connection(config_id, network).unwrap(); -/// supplicant.activate(config_id).unwrap(); -/// while ublox.connected_to_network().is_err() { -/// ublox.spin().ok(); -/// } -/// // Now connected to a wifi network. -/// -/// let mut supplicant = ublox.supplicant::().unwrap(); -/// supplicant.deactivate(0).unwrap(); -/// // Connection has to be down before removal -/// while ublox.supplicant::().unwrap().get_active_config().is_some() { -/// ublox.spin().ok(); -/// } -/// let mut supplicant = ublox.supplicant::().unwrap(); -/// supplicant.remove_connection(0) -/// -/// -pub struct Supplicant<'a, C, const N: usize> { - pub(crate) client: &'a mut C, - pub(crate) wifi_connection: &'a mut Option, - pub(crate) active_on_startup: &'a mut Option, -} - -impl<'a, C, const N: usize> Supplicant<'a, C, N> -where - C: atat::blocking::AtatClient, -{ - fn send_at(&mut self, req: &A) -> Result { - self.client.send(req).map_err(|e| { - error!("{:?}: {=[u8]:a}", e, req.as_bytes()); - e.into() - }) - } - - // pub fn load(&mut self) { - // for config_id in 0..N as u8 { - // self.client - // .send(&EdmAtCmdWrapper(ExecWifiStationAction { - // config_id, - // action: WifiStationAction::Load, - // })) - // .ok(); - // } - // } - pub(crate) fn init(&mut self) -> Result<(), Error> { - debug!("[SUP] init"); - for config_id in 0..N as u8 { - let load = self.client.send(&EdmAtCmdWrapper(ExecWifiStationAction { - config_id, - action: WifiStationAction::Load, - })); - - let GetWifiStationConfigResponse { parameter, .. } = - self.send_at(&EdmAtCmdWrapper(GetWifiStationConfig { - config_id, - parameter: Some(WifiStationConfigParameter::ActiveOnStartup), - }))?; - - if parameter == WifiStationConfigR::ActiveOnStartup(true.into()) { - debug!("[SUP] Config {:?} is active on startup", config_id); - if *self.active_on_startup == None || *self.active_on_startup == Some(config_id) { - *self.active_on_startup = Some(config_id); - // Update wifi connection - if self.wifi_connection.is_none() { - let con = self.get_connection(config_id)?.unwrap_or_default(); - - self.wifi_connection.replace( - WifiConnection::new( - WifiNetwork { - bssid: atat::heapless_bytes::Bytes::new(), - op_mode: - crate::command::wifi::types::OperationMode::Infrastructure, - ssid: con.ssid, - channel: 0, - rssi: 1, - authentication_suites: 0, - unicast_ciphers: 0, - group_ciphers: 0, - mode: super::network::WifiMode::Station, - }, - WiFiState::NotConnected, - config_id, - ) - .activate(), - ); - } else if let Some(ref mut con) = self.wifi_connection { - if con.config_id == 255 { - con.config_id = config_id; - } - } - // One could argue that an excisting connection should be verified, - // but should this be the case, the module is already having unexpected behaviour - } else { - // This causes unexpected behaviour - error!("Two configs are active on startup!"); - return Err(Error::Supplicant); - } - } else if load.is_err() { - //Handle shadow store bug - //TODO: Check if the ssid is set, if so the credential has to be cleared, as it is not there actually. - - let GetWifiStationConfigResponse { parameter, .. } = - self.send_at(&EdmAtCmdWrapper(GetWifiStationConfig { - config_id, - parameter: Some(WifiStationConfigParameter::SSID), - }))?; - - if let WifiStationConfigR::SSID(ssid) = parameter { - if !ssid.is_empty() { - error!("Shadow store bug!"); - // This should fix the issue - self.remove_connection(config_id) - .map_err(|_| Error::Supplicant)?; - self.send_at(&EdmAtCmdWrapper(RebootDCE)).ok(); - return Err(Error::ShadowStoreBug); - } - } - } - } - Ok(()) - } - - pub fn get_connection(&mut self, config_id: u8) -> Result, Error> { - debug!("[SUP] Get connection: {:?}", config_id); - let GetWifiStationConfigResponse { - parameter: ip_mode, .. - } = self.send_at(&EdmAtCmdWrapper(GetWifiStationConfig { - config_id, - parameter: Some(WifiStationConfigParameter::IPv4Mode), - }))?; - - let mut options = ConnectionOptions { - ssid: heapless::String::new(), - password: None, - ip: None, - subnet: None, - gateway: None, - }; - - let GetWifiStationConfigResponse { parameter, .. } = - self.send_at(&EdmAtCmdWrapper(GetWifiStationConfig { - config_id, - parameter: Some(WifiStationConfigParameter::SSID), - }))?; - - if let WifiStationConfigR::SSID(ssid) = parameter { - if ssid.is_empty() { - return Ok(None); - } - options.ssid = ssid; - } - - let GetWifiStationConfigResponse { parameter, .. } = - self.send_at(&EdmAtCmdWrapper(GetWifiStationConfig { - config_id, - parameter: Some(WifiStationConfigParameter::Authentication), - }))?; - - if let WifiStationConfigR::Authentication(auth) = parameter { - if !matches!(auth, Authentication::Open) { - options.password = Some(heapless::String::from("***")); - } - } - - if let WifiStationConfigR::IPv4Mode(IPv4Mode::Static) = ip_mode { - let GetWifiStationConfigResponse { parameter, .. } = - self.send_at(&EdmAtCmdWrapper(GetWifiStationConfig { - config_id, - parameter: Some(WifiStationConfigParameter::IPv4Address), - }))?; - - if let WifiStationConfigR::IPv4Address(ip) = parameter { - options.ip = Some(ip); - } - - let GetWifiStationConfigResponse { parameter, .. } = - self.send_at(&EdmAtCmdWrapper(GetWifiStationConfig { - config_id, - parameter: Some(WifiStationConfigParameter::SubnetMask), - }))?; - - if let WifiStationConfigR::SubnetMask(subnet) = parameter { - options.subnet = Some(subnet); - } - - let GetWifiStationConfigResponse { parameter, .. } = - self.send_at(&EdmAtCmdWrapper(GetWifiStationConfig { - config_id, - parameter: Some(WifiStationConfigParameter::DefaultGateway), - }))?; - - if let WifiStationConfigR::DefaultGateway(gateway) = parameter { - options.gateway = Some(gateway); - } - } - - Ok(Some(options)) - } - - /// Get id of active config - pub fn get_active_config_id(&self) -> Option { - debug!("[SUP] Get active config id"); - if let Some(ref wifi) = self.wifi_connection { - if wifi.wifi_state != WiFiState::Inactive { - debug!("[SUP] Active: {:?}", wifi.config_id); - return Some(wifi.config_id); - } - } - None - } - - /// Get id of active config - pub fn has_active_config_id(&self) -> bool { - if let Some(ref wifi) = self.wifi_connection { - if wifi.wifi_state != WiFiState::Inactive { - return true; - } - } - false - } - - /// List connections stored in module - /// - /// Sorted by config ID - pub fn list_connections(&mut self) -> Result, Error> { - debug!("[SUP] list connections"); - Ok((0..N as u8) - .filter_map(|config_id| { - self.get_connection(config_id) - .unwrap() - .map(|c| (config_id, c)) - }) - .collect()) - } - - /// Attempts to remove a stored wireless network - /// - /// Removing the active connection is not possible. Deactivate the network first. - pub fn remove_connection(&mut self, config_id: u8) -> Result<(), WifiConnectionError> { - // self.deactivate(config_id)?; - debug!("[SUP] Remove connection: {:?}", config_id); - // check for active - if self.is_config_in_use(config_id) { - error!("Config id is active!"); - return Err(WifiConnectionError::Illegal); - } - - self.send_at(&EdmAtCmdWrapper(ExecWifiStationAction { - config_id, - action: WifiStationAction::Reset, - }))?; - - self.send_at(&EdmAtCmdWrapper(ExecWifiStationAction { - config_id, - action: WifiStationAction::Store, - }))?; - - if Some(config_id) == self.get_active_on_startup() { - self.unset_active_on_startup()?; - } - // debug!("[SUP] Remove config: {:?}", config_id); - - Ok(()) - } - - /// Attempts to store a wireless network with the given connection options. - /// - /// Replacing the currently active network is not possible. - pub fn insert_connection( - &mut self, - config_id: u8, - options: &ConnectionOptions, - ) -> Result<(), WifiConnectionError> { - debug!("[SUP] Insert config: {:?}", config_id); - // Network part - // Reset network config slot - - // check for active - if self.is_config_in_use(config_id) { - error!("Config id is active!"); - return Err(WifiConnectionError::Illegal); - } - - self.send_at(&EdmAtCmdWrapper(ExecWifiStationAction { - config_id, - action: WifiStationAction::Reset, - }))?; - if Some(config_id) == self.get_active_on_startup() { - self.unset_active_on_startup()?; - } - - // Disable DHCP Client (static IP address will be used) - if options.ip.is_some() || options.subnet.is_some() || options.gateway.is_some() { - self.send_at(&EdmAtCmdWrapper(SetWifiStationConfig { - config_id, - config_param: WifiStationConfig::IPv4Mode(IPv4Mode::Static), - }))?; - } - - // Network IP address - if let Some(ip) = options.ip { - self.send_at(&EdmAtCmdWrapper(SetWifiStationConfig { - config_id, - config_param: WifiStationConfig::IPv4Address(ip), - }))?; - } - // Network Subnet mask - if let Some(subnet) = options.subnet { - self.send_at(&EdmAtCmdWrapper(SetWifiStationConfig { - config_id, - config_param: WifiStationConfig::SubnetMask(subnet), - }))?; - } - // Network Default gateway - if let Some(gateway) = options.gateway { - self.send_at(&EdmAtCmdWrapper(SetWifiStationConfig { - config_id, - config_param: WifiStationConfig::DefaultGateway(gateway), - }))?; - } - - // Wifi part - // Set the Network SSID to connect to - self.send_at(&EdmAtCmdWrapper(SetWifiStationConfig { - config_id, - config_param: WifiStationConfig::SSID(options.ssid.clone()), - }))?; - - if let Some(pass) = options.password.clone() { - // Use WPA2 as authentication type - self.send_at(&EdmAtCmdWrapper(SetWifiStationConfig { - config_id, - config_param: WifiStationConfig::Authentication(Authentication::WpaWpa2Psk), - }))?; - - // Input passphrase - self.send_at(&EdmAtCmdWrapper(SetWifiStationConfig { - config_id, - config_param: WifiStationConfig::WpaPskOrPassphrase(pass), - }))?; - } else { - self.send_at(&EdmAtCmdWrapper(SetWifiStationConfig { - config_id, - config_param: WifiStationConfig::Authentication(Authentication::Open), - }))?; - } - - // Store config - self.send_at(&EdmAtCmdWrapper(ExecWifiStationAction { - config_id, - action: WifiStationAction::Store, - }))?; - - Ok(()) - } - - /// Activate a given network config - /// Only one config can be active at any time. - /// - /// The driver has two modes of active for ID's. Active in driver and active on module. - /// These are differentiated by the driver mode being called activated and modules - /// mode called active. The driver activates a config, and then the driver reacts - /// asyncronous to the request and sets a config as active. - /// Driver activation is seen as `wificonnection.active()`. - /// Module is seen in `wificonnection.state`, where `inactive` is inactive and all others - /// are activated. - /// - /// The activation flow is as follows: - /// - /// driver.activate() driver.deactivate() - /// ┼─────────────────────────┼ - /// ┼─────────────────────────┼ - /// module is active module inactive - pub fn activate(&mut self, config_id: u8) -> Result<(), WifiConnectionError> { - debug!("[SUP] Activate connection: {:?}", config_id); - if let Some(w) = self.wifi_connection { - if w.activated { - return Err(WifiConnectionError::Illegal); - } - } - - let con = self.get_connection(config_id)?.unwrap_or_default(); - - self.send_at(&EdmAtCmdWrapper(ExecWifiStationAction { - config_id, - action: WifiStationAction::Activate, - }))?; - - self.wifi_connection.replace( - WifiConnection::new( - WifiNetwork { - bssid: atat::heapless_bytes::Bytes::new(), - op_mode: crate::command::wifi::types::OperationMode::Infrastructure, - ssid: con.ssid, - channel: 0, - rssi: 1, - authentication_suites: 0, - unicast_ciphers: 0, - group_ciphers: 0, - mode: super::network::WifiMode::Station, - }, - WiFiState::Inactive, - config_id, - ) - .activate(), - ); - debug!("[SUP] Activated: {:?}", config_id); - - Ok(()) - } - - /// Deactivates a given network config - /// - /// Operation not done until network conneciton is lost - pub fn deactivate(&mut self, config_id: u8) -> Result<(), WifiConnectionError> { - debug!("[SUP] Deactivate connection: {:?}", config_id); - let mut active = false; - - if let Some(con) = self.wifi_connection { - if con.activated && con.config_id == config_id { - active = true; - } - } - - if active { - self.send_at(&EdmAtCmdWrapper(ExecWifiStationAction { - config_id, - action: WifiStationAction::Deactivate, - }))?; - - if let Some(ref mut con) = self.wifi_connection { - con.deactivate(); - } - debug!("[SUP] Deactivated: {:?}", config_id); - } - Ok(()) - } - - pub fn scan(&mut self) -> Result, WifiError> { - match self.send_at(&EdmAtCmdWrapper(WifiScan { ssid: None })) { - Ok(resp) => resp - .network_list - .into_iter() - .map(WifiNetwork::try_from) - .collect(), - Err(_) => Err(WifiError::UnexpectedResponse), - } - } - - pub fn is_connected(&self) -> bool { - self.wifi_connection - .as_ref() - .map(WifiConnection::is_connected) - .unwrap_or_default() - } - - pub fn flush(&mut self) -> Result<(), WifiConnectionError> { - todo!() - } - - /// Returns Active on startup config ID if any - pub fn get_active_on_startup(&self) -> Option { - debug!( - "[SUP] Get active on startup: {:?}", - self.active_on_startup.clone() - ); - return self.active_on_startup.clone(); - } - - /// Returns Active on startup config ID if any - pub fn has_active_on_startup(&self) -> bool { - return self.active_on_startup.is_some(); - } - - /// Sets a config as active on startup, replacing the current. - /// - /// This is not possible if any of the two are currently active. - pub fn set_active_on_startup(&mut self, config_id: u8) -> Result<(), WifiConnectionError> { - debug!("[SUP] Set active on startup connection: {:?}", config_id); - // check end condition true - if let Some(active_on_startup) = *self.active_on_startup { - if active_on_startup == config_id { - return Ok(()); - } - // check for active connection - if self.is_config_in_use(active_on_startup) { - error!("Active on startup is active!"); - return Err(WifiConnectionError::Illegal); - } - } - if self.is_config_in_use(config_id) { - error!("Config id is active!"); - return Err(WifiConnectionError::Illegal); - } - - // disable current active on startup - if let Some(active_on_startup) = *self.active_on_startup { - // if any active on startup remove this parameter. - self.send_at(&EdmAtCmdWrapper(SetWifiStationConfig { - config_id: active_on_startup, - config_param: WifiStationConfig::ActiveOnStartup(false.into()), - }))?; - - self.send_at(&EdmAtCmdWrapper(ExecWifiStationAction { - config_id: active_on_startup, - action: WifiStationAction::Store, - }))?; - } - - // Insert the new one as active on startup. - self.send_at(&EdmAtCmdWrapper(SetWifiStationConfig { - config_id, - config_param: WifiStationConfig::ActiveOnStartup(true.into()), - }))?; - - self.send_at(&EdmAtCmdWrapper(ExecWifiStationAction { - config_id, - action: WifiStationAction::Store, - }))?; - - *self.active_on_startup = Some(config_id); - - Ok(()) - } - - /// Unsets a config as active on startup, replacing the current. - /// - /// This is not possible if any of the two are currently active. - pub fn unset_active_on_startup(&mut self) -> Result<(), WifiConnectionError> { - debug!("[SUP] Unset active on startup connection"); - // check for any of them as active - if let Some(active_on_startup) = self.active_on_startup.clone() { - // check for active connection - if self.is_config_in_use(active_on_startup) { - error!("Active on startup is active!"); - return Err(WifiConnectionError::Illegal); - } - // if any active remove this asset. - self.send_at(&EdmAtCmdWrapper(SetWifiStationConfig { - config_id: active_on_startup, - config_param: WifiStationConfig::ActiveOnStartup(false.into()), - }))?; - - self.send_at(&EdmAtCmdWrapper(ExecWifiStationAction { - config_id: active_on_startup, - action: WifiStationAction::Store, - }))?; - *self.active_on_startup = None; - } - Ok(()) - } - - /// Checks for active and activated. - /// See self.activate for explanation. - fn is_config_in_use(&self, config_id: u8) -> bool { - if let Some(active_id) = self.get_active_config_id() { - if active_id == config_id { - return true; - } - } else if let Some(ref con) = self.wifi_connection { - if con.activated && con.config_id == config_id { - error!("One of the IDs being changed is activated!"); - return true; - } - } - false - } -} From 20f243f4a01d55adb4f6453c9db9023ade674485 Mon Sep 17 00:00:00 2001 From: Mathias Date: Mon, 1 Jul 2024 20:11:29 +0200 Subject: [PATCH 5/7] Rework runner, add Proxy client and add working Control handle --- examples/rpi-pico/src/bin/embassy-perf.rs | 2 +- .../rpi-pico/src/bin/embassy-smoltcp-ppp.rs | 2 +- src/asynch/at_udp_socket.rs | 2 - src/asynch/control.rs | 409 +++++++++++----- src/asynch/network.rs | 120 +++-- src/asynch/resources.rs | 24 +- src/asynch/runner.rs | 462 ++++++++++-------- src/asynch/state.rs | 82 +++- src/command/custom_digest.rs | 8 +- src/command/data_mode/mod.rs | 10 +- src/command/data_mode/responses.rs | 4 +- src/command/data_mode/urc.rs | 4 +- src/command/edm/mod.rs | 2 +- src/command/edm/urc.rs | 2 +- src/command/general/mod.rs | 12 +- src/command/general/responses.rs | 6 +- src/command/gpio/responses.rs | 4 +- src/command/mod.rs | 4 +- src/command/ping/types.rs | 4 +- src/command/system/types.rs | 2 +- src/command/wifi/types.rs | 6 +- 21 files changed, 748 insertions(+), 423 deletions(-) diff --git a/examples/rpi-pico/src/bin/embassy-perf.rs b/examples/rpi-pico/src/bin/embassy-perf.rs index 0376907..630415a 100644 --- a/examples/rpi-pico/src/bin/embassy-perf.rs +++ b/examples/rpi-pico/src/bin/embassy-perf.rs @@ -90,7 +90,7 @@ async fn main(spawner: Spawner) { let net_device = runner.ppp_stack(PPP_STATE.init(embassy_net_ppp::State::new())); // Generate random seed - let seed = 0x0123_4567_89ab_cdef; // chosen by fair dice roll. guarenteed to be random. + let seed = 0x0123_4567_89ab_cdef; // chosen by fair dice roll. guaranteed to be random. // Init network stack static STACK: StaticCell>> = StaticCell::new(); diff --git a/examples/rpi-pico/src/bin/embassy-smoltcp-ppp.rs b/examples/rpi-pico/src/bin/embassy-smoltcp-ppp.rs index 21c9416..e25d51e 100644 --- a/examples/rpi-pico/src/bin/embassy-smoltcp-ppp.rs +++ b/examples/rpi-pico/src/bin/embassy-smoltcp-ppp.rs @@ -109,7 +109,7 @@ async fn main(spawner: Spawner) { let net_device = runner.ppp_stack(PPP_STATE.init(embassy_net_ppp::State::new())); // Generate random seed - let seed = 0x0123_4567_89ab_cdef; // chosen by fair dice roll. guarenteed to be random. + let seed = 0x0123_4567_89ab_cdef; // chosen by fair dice roll. guaranteed to be random. // Init network stack static STACK: StaticCell>> = StaticCell::new(); diff --git a/src/asynch/at_udp_socket.rs b/src/asynch/at_udp_socket.rs index 6dd777d..5b3f87f 100644 --- a/src/asynch/at_udp_socket.rs +++ b/src/asynch/at_udp_socket.rs @@ -14,7 +14,6 @@ impl<'a> embedded_io_async::ErrorType for &AtUdpSocket<'a> { impl<'a> Read for &AtUdpSocket<'a> { async fn read(&mut self, buf: &mut [u8]) -> Result { let (len, _) = self.0.recv_from(buf).await.unwrap(); - debug!("READ {} bytes: {=[u8]:a}", len, &buf[..len]); Ok(len) } } @@ -40,7 +39,6 @@ impl<'a> embedded_io_async::ErrorType for AtUdpSocket<'a> { impl<'a> Read for AtUdpSocket<'a> { async fn read(&mut self, buf: &mut [u8]) -> Result { let (len, _) = self.0.recv_from(buf).await.unwrap(); - debug!("READ {} bytes: {=[u8]:a}", len, &buf[..len]); Ok(len) } } diff --git a/src/asynch/control.rs b/src/asynch/control.rs index 5bc4ed0..031d59d 100644 --- a/src/asynch/control.rs +++ b/src/asynch/control.rs @@ -1,99 +1,135 @@ -use core::future::poll_fn; -use core::task::Poll; - -use atat::{ - asynch::{AtatClient, SimpleClient}, - UrcChannel, UrcSubscription, -}; -use embassy_net::{ - udp::{PacketMetadata, UdpSocket}, - Ipv4Address, +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 crate::command::gpio::responses::ReadGPIOResponse; +use crate::command::gpio::types::GPIOMode; +use crate::command::gpio::ConfigureGPIO; +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 embassy_time::{with_timeout, Duration}; - -use crate::command::gpio::{ - types::{GPIOId, GPIOValue}, - WriteGPIO, +use crate::command::{ + gpio::{ + types::{GPIOId, GPIOValue}, + WriteGPIO, + }, + wifi::SetWifiAPConfig, }; -use crate::command::network::SetNetworkHostName; -use crate::command::system::{RebootDCE, ResetToFactoryDefaults}; -use crate::command::wifi::types::{ - Authentication, StatusId, WifiStationAction, WifiStationConfig, WifiStatus, WifiStatusVal, +use crate::command::{network::SetNetworkHostName, wifi::types::AccessPointConfig}; +use crate::command::{ + system::{RebootDCE, ResetToFactoryDefaults}, + wifi::types::AccessPointId, }; -use crate::command::wifi::{ExecWifiStationAction, GetWifiStatus, SetWifiStationConfig}; -use crate::command::OnOff; use crate::error::Error; +use super::runner::{MAX_CMD_LEN, URC_SUBSCRIBERS}; use super::state::LinkState; -use super::{at_udp_socket::AtUdpSocket, runner::URC_SUBSCRIBERS}; use super::{state, UbloxUrc}; const CONFIG_ID: u8 = 0; -const MAX_COMMAND_LEN: usize = 128; - -// TODO: Can this be made in a more intuitive way? -pub struct ControlResources { - rx_meta: [PacketMetadata; 1], - tx_meta: [PacketMetadata; 1], - socket_rx_buf: [u8; 32], - socket_tx_buf: [u8; 32], - at_buf: [u8; MAX_COMMAND_LEN], +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: Option, } -impl ControlResources { - pub const fn new() -> Self { +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 { - rx_meta: [PacketMetadata::EMPTY; 1], - tx_meta: [PacketMetadata::EMPTY; 1], - socket_rx_buf: [0u8; 32], - socket_tx_buf: [0u8; 32], - at_buf: [0u8; MAX_COMMAND_LEN], + req_sender, + res_slot, + cooldown_timer: None, + } + } + + async fn wait_response<'guard>( + &'guard mut 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! + self.req_sender + .send(Vec::try_from(&buf[..len]).unwrap()) + .await; + + self.cooldown_timer = Some(Timer::after(Duration::from_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, 'r, const URC_CAPACITY: usize> { +pub struct Control<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> { state_ch: state::Runner<'a>, - at_client: SimpleClient<'r, AtUdpSocket<'r>, atat::DefaultDigester>, - _urc_subscription: UrcSubscription<'a, UbloxUrc, URC_CAPACITY, URC_SUBSCRIBERS>, + at_client: ProxyClient<'a, INGRESS_BUF_SIZE>, + _urc_channel: &'a UrcChannel, } -impl<'a, 'r, const URC_CAPACITY: usize> Control<'a, 'r, URC_CAPACITY> { - pub(crate) fn new( +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, - resources: &'r mut ControlResources, - stack: &'r embassy_net::Stack, + req_sender: Sender<'a, NoopRawMutex, Vec, 1>, + res_slot: &'a atat::ResponseSlot, ) -> Self { - let mut socket = UdpSocket::new( - stack, - &mut resources.rx_meta, - &mut resources.socket_rx_buf, - &mut resources.tx_meta, - &mut resources.socket_tx_buf, - ); - - info!("Socket bound!"); - socket - .bind((Ipv4Address::new(172, 30, 0, 252), AtUdpSocket::PPP_AT_PORT)) - .unwrap(); - - let at_client = SimpleClient::new( - AtUdpSocket(socket), - atat::AtDigester::::new(), - &mut resources.at_buf, - atat::Config::default(), - ); - Self { state_ch, - at_client, - _urc_subscription: urc_channel.subscribe().unwrap(), + at_client: ProxyClient::new(req_sender, res_slot), + _urc_channel: urc_channel, } } pub async fn set_hostname(&mut self, hostname: &str) -> Result<(), Error> { + self.state_ch.wait_for_initialized().await; + self.at_client .send(&SetNetworkHostName { host_name: hostname, @@ -131,13 +167,141 @@ impl<'a, 'r, const URC_CAPACITY: usize> Control<'a, 'r, URC_CAPACITY> { } pub async fn factory_reset(&mut self) -> Result<(), Error> { + self.state_ch.wait_for_initialized().await; + self.at_client.send(&ResetToFactoryDefaults).await?; self.at_client.send(&RebootDCE).await?; Ok(()) } + pub async fn start_ap(&mut self, ssid: &str) -> Result<(), Error> { + self.state_ch.wait_for_initialized().await; + + // Deactivate network id 0 + self.at_client + .send(&WifiAPAction { + ap_config_id: AccessPointId::Id0, + ap_action: AccessPointAction::Deactivate, + }) + .await?; + + self.at_client + .send(&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(&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(&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(&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(&SetWifiAPConfig { + // ap_config_id: AccessPointId::Id0, + // ap_config_param: AccessPointConfig::DefaultGateway(gateway), + // }) + // .await?; + // } + + // self.at_client + // .send(&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(&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(&SetWifiAPConfig { + // ap_config_id: AccessPointId::Id0, + // ap_config_param: AccessPointConfig::SecurityMode( + // SecurityMode::Wpa2AesCcmp, + // SecurityModePSK::PSK, + // ), + // }) + // .await?; + + // // Input passphrase + // self.at_client + // .send(&SetWifiAPConfig { + // ap_config_id: AccessPointId::Id0, + // ap_config_param: AccessPointConfig::PSKPassphrase(PasskeyR::Passphrase(pass)), + // }) + // .await?; + // } else { + self.at_client + .send(&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(&SetWifiAPConfig { + // ap_config_id: AccessPointId::Id0, + // ap_config_param: AccessPointConfig::Channel(channel as u8), + // }) + // .await?; + // } + + self.at_client + .send(&WifiAPAction { + ap_config_id: AccessPointId::Id0, + ap_action: AccessPointAction::Activate, + }) + .await?; + + Ok(()) + } + pub async fn join_open(&mut self, ssid: &str) -> 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?; @@ -193,6 +357,8 @@ impl<'a, 'r, const URC_CAPACITY: usize> Control<'a, 'r, URC_CAPACITY> { } pub async fn join_wpa2(&mut self, ssid: &str, passphrase: &str) -> 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?; @@ -257,6 +423,8 @@ impl<'a, 'r, const URC_CAPACITY: usize> Control<'a, 'r, URC_CAPACITY> { } pub async fn disconnect(&mut self) -> Result<(), Error> { + self.state_ch.wait_for_initialized().await; + match self.get_wifi_status().await? { WifiStatusVal::Disabled => {} WifiStatusVal::Disconnected | WifiStatusVal::Connected => { @@ -269,24 +437,20 @@ impl<'a, 'r, const URC_CAPACITY: usize> Control<'a, 'r, URC_CAPACITY> { } } - let wait_for_disconnect = poll_fn(|cx| match self.state_ch.link_state(cx) { - LinkState::Up => Poll::Pending, - LinkState::Down => Poll::Ready(()), - }); - - with_timeout(Duration::from_secs(10), wait_for_disconnect) - .await - .map_err(|_| Error::Timeout)?; + with_timeout( + Duration::from_secs(10), + self.state_ch.wait_for_link_state(LinkState::Down), + ) + .await + .map_err(|_| Error::Timeout)?; Ok(()) } async fn wait_for_join(&mut self, ssid: &str) -> Result<(), Error> { - poll_fn(|cx| match self.state_ch.link_state(cx) { - LinkState::Down => Poll::Pending, - LinkState::Up => Poll::Ready(()), - }) - .await; + // TODO: Handle returning error in case of security problems + + self.state_ch.wait_for_link_state(LinkState::Up).await; // Check that SSID matches let current_ssid = self.get_connected_ssid().await?; @@ -297,44 +461,65 @@ impl<'a, 'r, const URC_CAPACITY: usize> Control<'a, 'r, URC_CAPACITY> { Ok(()) } - pub async fn gpio_set(&mut self, id: GPIOId, value: GPIOValue) -> Result<(), Error> { - self.at_client.send(&WriteGPIO { id, value }).await?; + pub async fn gpio_configure(&mut self, id: GPIOId, mode: GPIOMode) -> Result<(), Error> { + self.state_ch.wait_for_initialized().await; + self.at_client.send(&ConfigureGPIO { id, mode }).await?; Ok(()) } - // 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); + pub async fn gpio_set(&mut self, id: GPIOId, value: bool) -> Result<(), Error> { + self.state_ch.wait_for_initialized().await; - self.at_client - .send(&PrepareSecurityDataImport { - data_type, - data_size: data.len(), - internal_name: name, - password: None, - }) - .await?; + let value = if value { + GPIOValue::High + } else { + GPIOValue::Low + }; - let import_data = self - .at_client - .send(&SendSecurityDataImport { - data: atat::serde_bytes::Bytes::new(data), - }) - .await?; + self.at_client.send(&WriteGPIO { id, value }).await?; + Ok(()) + } - if let Some(hash) = md5_sum { - assert_eq!(import_data.md5_string.as_str(), hash); - } + pub async fn gpio_get(&mut self, id: GPIOId) -> Result { + self.state_ch.wait_for_initialized().await; - Ok(()) + let ReadGPIOResponse { value, .. } = self.at_client.send(&ReadGPIO { id }).await?; + Ok(value as u8 != 0) } + + // 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(&PrepareSecurityDataImport { + // data_type, + // data_size: data.len(), + // internal_name: name, + // password: None, + // }) + // .await?; + + // let import_data = self + // .at_client + // .send(&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/network.rs b/src/asynch/network.rs index 5b7bcae..5d73e1e 100644 --- a/src/asynch/network.rs +++ b/src/asynch/network.rs @@ -7,7 +7,6 @@ use no_std_net::{Ipv4Addr, Ipv6Addr}; use crate::{ command::{ - data_mode::{types::PeerConfigParameter, SetPeerConfiguration}, general::SoftwareVersion, network::{ responses::NetworkStatusResponse, @@ -15,12 +14,13 @@ use crate::{ urc::{NetworkDown, NetworkUp}, GetNetworkStatus, }, - system::{RebootDCE, StoreCurrentConfig}, + system::{types::EchoOn, RebootDCE, SetEcho, StoreCurrentConfig}, wifi::{ - types::DisconnectReason, + types::{DisconnectReason, PowerSaveMode, WifiConfig as WifiConfigParam}, urc::{WifiLinkConnected, WifiLinkDisconnected}, + SetWifiConfig, }, - Urc, + OnOff, Urc, }, connection::WiFiState, error::Error, @@ -28,9 +28,9 @@ use crate::{ WifiConfig, }; -use super::{runner::URC_SUBSCRIBERS, state, UbloxUrc}; +use super::{runner::URC_SUBSCRIBERS, state, LinkState, UbloxUrc}; -pub struct NetDevice<'a, 'b, C, A, const URC_CAPACITY: usize> { +pub(crate) struct NetDevice<'a, 'b, C, A, const URC_CAPACITY: usize> { ch: &'b state::Runner<'a>, config: &'b mut C, at_client: A, @@ -57,33 +57,58 @@ where } pub(crate) async fn init(&mut self) -> Result<(), Error> { - // Initilize a new ublox device to a known state (set RS232 settings) + // Initialize a new ublox device to a known state (set RS232 settings) debug!("Initializing module"); // Hard reset module self.reset().await?; self.at_client.send(&SoftwareVersion).await?; + self.at_client.send(&SetEcho { on: EchoOn::Off }).await?; + self.at_client + .send(&SetWifiConfig { + config_param: WifiConfigParam::DropNetworkOnLinkLoss(OnOff::On), + }) + .await?; + + // Disable all power savings for now + self.at_client + .send(&SetWifiConfig { + config_param: WifiConfigParam::PowerSaveMode(PowerSaveMode::ActiveMode), + }) + .await?; + #[cfg(feature = "internal-network-stack")] if let Some(size) = C::TLS_IN_BUFFER_SIZE { self.at_client - .send(&SetPeerConfiguration { - parameter: PeerConfigParameter::TlsInBuffer(size), + .send(&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 { self.at_client - .send(&SetPeerConfiguration { - parameter: PeerConfigParameter::TlsOutBuffer(size), + .send(&crate::command::data_mode::SetPeerConfiguration { + parameter: crate::command::data_mode::types::PeerConfigParameter::TlsOutBuffer( + size, + ), }) .await?; } + self.ch.mark_initialized(); + Ok(()) } - pub async fn run(&mut self) -> ! { + pub async fn run(&mut self) -> Result<(), Error> { + if self.ch.link_state(None) == LinkState::Uninitialized { + self.init().await?; + } + loop { let event = self.urc_subscription.next_message_pure().await; @@ -92,11 +117,11 @@ where continue; }; - self.handle_urc(event).await; + self.handle_urc(event).await?; } } - async fn handle_urc(&mut self, event: Urc) { + async fn handle_urc(&mut self, event: Urc) -> Result<(), Error> { debug!("GOT URC event"); match event { Urc::StartUp => { @@ -110,44 +135,56 @@ where con.wifi_state = WiFiState::Connected; con.network .replace(WifiNetwork::new_station(bssid, channel)); - con.activated = true; }), Urc::WifiLinkDisconnected(WifiLinkDisconnected { reason, .. }) => { - self.ch.update_connection_with(|con| match reason { - DisconnectReason::NetworkDisabled => { - con.wifi_state = WiFiState::Inactive; - } - DisconnectReason::SecurityProblems => { - error!("Wifi Security Problems"); - con.wifi_state = WiFiState::NotConnected; - } - _ => { - con.wifi_state = WiFiState::NotConnected; + 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(_) => todo!(), - Urc::WifiAPDown(_) => todo!(), - Urc::WifiAPStationConnected(_) => todo!(), - Urc::WifiAPStationDisconnected(_) => todo!(), - Urc::EthernetLinkUp(_) => todo!(), - Urc::EthernetLinkDown(_) => todo!(), + 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 }) => { drop(event); - self.network_status_callback(interface_id).await.ok(); + self.network_status_callback(interface_id).await?; } Urc::NetworkDown(NetworkDown { interface_id }) => { drop(event); - self.network_status_callback(interface_id).await.ok(); + self.network_status_callback(interface_id).await?; } - Urc::NetworkError(_) => todo!(), + 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), + status: + NetworkStatus::InterfaceType(InterfaceType::WifiStation | InterfaceType::Unknown), .. } = self .at_client @@ -241,6 +278,7 @@ where Ok(()) } + #[allow(dead_code)] pub async fn restart(&mut self, store: bool) -> Result<(), Error> { warn!("Soft resetting Ublox Short Range"); if store { @@ -266,7 +304,11 @@ where let fut = async { loop { // Ignore AT results until we are successful in EDM mode - if let Ok(_) = self.at_client.send(SwitchToEdmCommand).await { + if let Ok(_) = self + .at_client + .send(&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. @@ -281,12 +323,6 @@ where .await .map_err(|_| Error::Timeout)?; - self.at_client - .send(crate::command::system::SetEcho { - on: crate::command::system::types::EchoOn::Off, - }) - .await?; - Ok(()) } } diff --git a/src/asynch/resources.rs b/src/asynch/resources.rs index 90cbe50..f115962 100644 --- a/src/asynch/resources.rs +++ b/src/asynch/resources.rs @@ -1,38 +1,38 @@ use atat::{ResponseSlot, UrcChannel}; +use embassy_sync::{blocking_mutex::raw::NoopRawMutex, channel::Channel}; -use super::{runner::URC_SUBSCRIBERS, state, UbloxUrc}; +use super::{ + runner::{MAX_CMD_LEN, URC_SUBSCRIBERS}, + state, UbloxUrc, +}; -pub struct Resources< - const CMD_BUF_SIZE: usize, - const INGRESS_BUF_SIZE: usize, - const URC_CAPACITY: usize, -> { +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) cmd_buf: [u8; CMD_BUF_SIZE], pub(crate) ingress_buf: [u8; INGRESS_BUF_SIZE], } -impl Default - for Resources +impl Default + for Resources { fn default() -> Self { Self::new() } } -impl - Resources +impl + Resources { pub fn new() -> Self { Self { ch: state::State::new(), res_slot: ResponseSlot::new(), + req_slot: Channel::new(), urc_channel: UrcChannel::new(), - cmd_buf: [0; CMD_BUF_SIZE], ingress_buf: [0; INGRESS_BUF_SIZE], } } diff --git a/src/asynch/runner.rs b/src/asynch/runner.rs index 335d809..e255e14 100644 --- a/src/asynch/runner.rs +++ b/src/asynch/runner.rs @@ -1,31 +1,60 @@ use super::{ - control::{Control, ControlResources}, + control::Control, network::NetDevice, - state, Resources, UbloxUrc, + state::{self, LinkState}, + Resources, UbloxUrc, }; -#[cfg(feature = "edm")] -use crate::command::edm::SwitchToEdmCommand; use crate::{ - asynch::at_udp_socket::AtUdpSocket, - command::{ - data_mode::{self, ChangeMode}, - Urc, - }, + asynch::control::ProxyClient, + command::data_mode::{self, ChangeMode}, WifiConfig, }; use atat::{ asynch::{AtatClient, SimpleClient}, AtatIngress as _, UrcChannel, }; -use embassy_futures::select::Either; -use embassy_net::{ - udp::{PacketMetadata, UdpSocket}, - Ipv4Address, -}; +use embassy_sync::{blocking_mutex::raw::NoopRawMutex, channel::Channel}; use embassy_time::{Duration, Instant, Timer}; use embedded_io_async::{BufRead, Read, 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>( + mut sink: impl Write, + source: impl Read, + req_slot: &Channel, 1>, + ingress: &mut atat::Ingress< + 'a, + Digester, + UbloxUrc, + INGRESS_BUF_SIZE, + URC_CAPACITY, + URC_SUBSCRIBERS, + >, +) -> ! { + ingress.clear(); + + let tx_fut = async { + loop { + let msg = req_slot.receive().await; + let _ = sink.write_all(&msg).await; + } + }; + + embassy_futures::join::join(tx_fut, ingress.read_from(source)).await; + + unreachable!() +} /// Background runner for the Ublox Module. /// @@ -36,18 +65,12 @@ pub struct Runner<'a, R, W, C, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY ch: state::Runner<'a>, config: C, - pub urc_channel: &'a UrcChannel, + pub urc_channel: &'a UrcChannel, - pub ingress: atat::Ingress< - 'a, - atat::AtDigester, - Urc, - INGRESS_BUF_SIZE, - URC_CAPACITY, - URC_SUBSCRIBERS, - >, - pub cmd_buf: &'a mut [u8], + 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>, @@ -60,15 +83,15 @@ where W: Write, C: WifiConfig<'a> + 'a, { - pub fn new( + pub fn new( iface: (R, W), - resources: &'a mut Resources, + resources: &'a mut Resources, config: C, ) -> Self { let ch_runner = state::Runner::new(&mut resources.ch); let ingress = atat::Ingress::new( - atat::AtDigester::new(), + Digester::new(), &mut resources.ingress_buf, &resources.res_slot, &resources.urc_channel, @@ -82,20 +105,21 @@ where urc_channel: &resources.urc_channel, ingress, - cmd_buf: &mut resources.cmd_buf, res_slot: &resources.res_slot, + req_slot: &resources.req_slot, #[cfg(feature = "ppp")] ppp_runner: None, } } - pub fn control<'r, D: embassy_net::driver::Driver>( - &self, - resources: &'r mut ControlResources, - stack: &'r embassy_net::Stack, - ) -> Control<'a, 'r, URC_CAPACITY> { - Control::new(self.ch.clone(), &self.urc_channel, resources, stack) + pub fn control(&self) -> Control<'a, INGRESS_BUF_SIZE, URC_CAPACITY> { + Control::new( + self.ch.clone(), + &self.urc_channel, + self.req_slot.sender(), + &self.res_slot, + ) } #[cfg(feature = "ppp")] @@ -109,191 +133,239 @@ where } #[cfg(feature = "internal-network-stack")] - pub fn internal_stack(&mut self) -> state::Device { - state::Device { - shared: &self.ch.shared, - urc_subscription: self.urc_channel.subscribe().unwrap(), + 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, } } - pub async fn run(mut self, stack: &embassy_net::Stack) -> ! { - #[cfg(feature = "ppp")] - let mut ppp_runner = self.ppp_runner.take().unwrap(); - - let at_config = atat::Config::default(); - loop { - // Run the cellular device from full power down to the - // `DataEstablished` state, handling power on, module configuration, - // network registration & operator selection and PDP context - // activation along the way. - // - // This is all done directly on the serial line, before setting up - // virtual channels through multiplexing. - { - let at_client = atat::asynch::Client::new( - &mut self.iface.1, - self.res_slot, - self.cmd_buf, - at_config, + #[cfg(feature = "internal-network-stack")] + pub async fn run(&mut self) -> ! { + let device_fut = async { + loop { + let mut device = NetDevice::new( + &self.ch, + &mut self.config, + ProxyClient::new(self.req_slot.sender(), &self.res_slot), + self.urc_channel, ); - let mut wifi_device = - NetDevice::new(&self.ch, &mut self.config, at_client, self.urc_channel); - // Clean up and start from completely powered off state. Ignore URCs in the process. - self.ingress.clear(); + if let Err(e) = device.init().await { + error!("WiFi init failed {:?}", e); + continue; + }; - match embassy_futures::select::select( - self.ingress.read_from(&mut self.iface.0), - wifi_device.init(), - ) - .await - { - Either::First(_) => { - // This has return type never (`-> !`) - unreachable!() - } - Either::Second(Err(_)) => { - // Reboot the wifi module and try again! - continue; - } - Either::Second(Ok(_)) => { - // All good! We are now ready to start communication services! - } - } + let _ = device.run().await; } + }; + + embassy_futures::join::join( + device_fut, + at_bridge( + &mut self.iface.1, + &mut self.iface.0, + &self.req_slot, + &mut self.ingress, + ), + ) + .await; + + unreachable!() + } - #[cfg(feature = "ppp")] - let ppp_fut = async { + #[cfg(feature = "ppp")] + pub async fn run( + &mut self, + stack: &embassy_net::Stack, + ) -> ! { + let at_config = atat::Config::default(); + + let network_fut = async { + loop { + // Allow control to send/receive AT commands directly on the + // UART, until we are ready to establish connection using PPP + + // Send "+++" to escape data mode, and enter command mode + // warn!("Escaping to command mode!"); + // Timer::after_secs(5).await; + // self.iface.1.write_all(b"+++").await.ok(); + // Timer::after_secs(1).await; let mut iface = super::ReadWriteAdapter(&mut self.iface.0, &mut self.iface.1); + iface.write_all(b"+++").await.ok(); + let mut buf = [0u8; 8]; - let mut fails = 0; - let mut last_start = None; - - loop { - if let Some(last_start) = last_start { - Timer::at(last_start + Duration::from_secs(10)).await; - // Do not attempt to start too fast. - - // If was up stably for at least 1 min, reset fail counter. - if Instant::now() > last_start + Duration::from_secs(60) { - fails = 0; - } else { - fails += 1; - if fails == 10 { - warn!("modem: PPP failed too much, rebooting modem."); - break; - } - } + let _ = embassy_time::with_timeout(Duration::from_millis(500), async { + loop { + iface.read(&mut buf).await.ok(); } - last_start = Some(Instant::now()); - - { - let mut buf = [0u8; 64]; - - let mut at_client = SimpleClient::new( - &mut iface, - atat::AtDigester::::new(), - &mut buf, - at_config, - ); - - // Send AT command `ATO3` to enter PPP mode - let res = at_client - .send(&ChangeMode { - mode: data_mode::types::Mode::PPPMode, + }) + .await; + + let _ = embassy_futures::select::select( + at_bridge( + &mut self.iface.1, + &mut self.iface.0, + &self.req_slot, + &mut self.ingress, + ), + self.ch.wait_for_link_state(LinkState::Up), + ) + .await; + + #[cfg(feature = "ppp")] + let ppp_fut = async { + let mut iface = super::ReadWriteAdapter(&mut self.iface.0, &mut self.iface.1); + + loop { + self.ch.wait_for_link_state(LinkState::Up).await; + + { + let mut buf = [0u8; 8]; + let mut at_client = SimpleClient::new( + &mut iface, + atat::AtDigester::::new(), + &mut buf, + at_config, + ); + + // Send AT command `ATO3` to enter PPP mode + let res = at_client + .send(&ChangeMode { + mode: data_mode::types::Mode::PPPMode, + }) + .await; + + if let Err(e) = res { + warn!("ppp dial failed {:?}", e); + continue; + } + + // Drain the UART + let _ = embassy_time::with_timeout(Duration::from_millis(500), async { + loop { + iface.read(&mut buf).await.ok(); + } }) .await; - - if let Err(e) = res { - warn!("ppp dial failed {:?}", e); - continue; } - drop(at_client); - - // Drain the UART - let _ = embassy_time::with_timeout(Duration::from_secs(2), async { - loop { - iface.read(&mut buf).await.ok(); - } - }) - .await; + info!("RUNNING PPP"); + let res = self + .ppp_runner + .as_mut() + .unwrap() + .run(&mut iface, C::PPP_CONFIG, |ipv4| { + let Some(addr) = ipv4.address else { + warn!("PPP did not provide an IP address."); + return; + }; + let mut dns_servers = heapless::Vec::new(); + for s in ipv4.dns_servers.iter().flatten() { + let _ = dns_servers + .push(embassy_net::Ipv4Address::from_bytes(&s.0)); + } + let config = + embassy_net::ConfigV4::Static(embassy_net::StaticConfigV4 { + address: embassy_net::Ipv4Cidr::new( + embassy_net::Ipv4Address::from_bytes(&addr.0), + 0, + ), + gateway: None, + dns_servers, + }); + + stack.set_config_v4(config); + }) + .await; - Timer::after(Duration::from_millis(100)).await; + info!("ppp failed: {:?}", res); } - - info!("RUNNING PPP"); - let res = ppp_runner - .run(&mut iface, C::PPP_CONFIG, |ipv4| { - let Some(addr) = ipv4.address else { - warn!("PPP did not provide an IP address."); - return; - }; - let mut dns_servers = heapless::Vec::new(); - for s in ipv4.dns_servers.iter().flatten() { - let _ = - dns_servers.push(embassy_net::Ipv4Address::from_bytes(&s.0)); + }; + + let 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 at_socket = AtUdpSocket(socket); + + at_bridge(&at_socket, &at_socket, &self.req_slot, &mut self.ingress).await; + }; + + let break_fut = async { + let mut fails = 0; + let mut last_start = None; + + self.ch.wait_for_initialized().await; + + loop { + if let Some(last_start) = last_start { + info!("LINK DOWN! Attempt: {}", fails); + Timer::at(last_start + Duration::from_secs(10)).await; + // Do not attempt to start too fast. + + // If was up, and stable for at least 1 min, reset fail counter. + if Instant::now() > last_start + Duration::from_secs(60) { + fails = 0; + } else { + fails += 1; + if fails == 2 { + warn!("modem: Link down too much, rebooting modem."); + break; + } } - 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: {:?}", res); - } - }; - - let network_fut = async { - stack.wait_config_up().await; - - 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, - ); + } + last_start = Some(Instant::now()); - let endpoint = stack.config_v4().unwrap(); + self.ch.wait_for_link_state(LinkState::Down).await; + } + }; - info!("Socket bound!"); - socket - .bind((Ipv4Address::new(172, 30, 0, 252), AtUdpSocket::PPP_AT_PORT)) - .unwrap(); + embassy_futures::select::select3(ppp_fut, at_fut, break_fut).await; - let at_socket = AtUdpSocket(socket); + warn!("Breaking WiFi network loop"); + } + }; + + let device_fut = async { + loop { + let mut device = NetDevice::new( + &self.ch, + &mut self.config, + ProxyClient::new(self.req_slot.sender(), &self.res_slot), + self.urc_channel, + ); - let at_client = - atat::asynch::Client::new(&at_socket, self.res_slot, self.cmd_buf, at_config); + if let Err(e) = device.init().await { + error!("WiFi init failed {:?}", e); + continue; + }; - let mut wifi_device = - NetDevice::new(&self.ch, &mut self.config, at_client, self.urc_channel); + let _ = device.run().await; + } + }; - embassy_futures::join::join(self.ingress.read_from(&at_socket), wifi_device.run()) - .await; - }; + embassy_futures::join::join(device_fut, network_fut).await; - match embassy_futures::select::select(ppp_fut, network_fut).await { - Either::First(_) => { - warn!("Breaking to reboot module from PPP"); - } - Either::Second(_) => { - warn!("Breaking to reboot module from network runner"); - } - } - } + unreachable!() } } diff --git a/src/asynch/state.rs b/src/asynch/state.rs index 8ec431e..1f9e376 100644 --- a/src/asynch/state.rs +++ b/src/asynch/state.rs @@ -8,27 +8,29 @@ use embassy_sync::blocking_mutex::raw::NoopRawMutex; use embassy_sync::blocking_mutex::Mutex; use embassy_sync::waitqueue::WakerRegistration; -use crate::connection::{WiFiState, WifiConnection}; +use crate::connection::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 struct State { +pub(crate) struct State { shared: Mutex>, } impl State { - pub const fn new() -> Self { + pub(crate) const fn new() -> Self { Self { shared: Mutex::new(RefCell::new(Shared { - link_state: LinkState::Down, + link_state: LinkState::Uninitialized, wifi_connection: WifiConnection::new(), state_waker: WakerRegistration::new(), connection_waker: WakerRegistration::new(), @@ -38,7 +40,7 @@ impl State { } /// State of the LinkState -pub struct Shared { +pub(crate) struct Shared { link_state: LinkState, wifi_connection: WifiConnection, state_waker: WakerRegistration, @@ -46,34 +48,64 @@ pub struct Shared { } #[derive(Clone)] -pub struct Runner<'d> { +pub(crate) struct Runner<'d> { shared: &'d Mutex>, } impl<'d> Runner<'d> { - pub fn new(state: &'d mut State) -> Self { + pub(crate) fn new(state: &'d mut State) -> Self { Self { shared: &state.shared, } } - pub fn set_link_state(&mut self, state: LinkState) { + pub(crate) fn mark_initialized(&self) { self.shared.lock(|s| { let s = &mut *s.borrow_mut(); - s.link_state = state; + s.link_state = LinkState::Down; s.state_waker.wake(); - }); + }) } - pub fn link_state(&mut self, cx: &mut Context) -> LinkState { + 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(); - s.state_waker.register(cx.waker()); + if let Some(cx) = cx { + s.state_waker.register(cx.waker()); + } s.link_state }) } - pub fn update_connection_with(&self, f: impl FnOnce(&mut WifiConnection)) { + 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); @@ -82,20 +114,18 @@ impl<'d> Runner<'d> { s.wifi_connection.is_connected() ); - if s.wifi_connection.network_up - && matches!(s.wifi_connection.wifi_state, WiFiState::Connected) - { - s.link_state = LinkState::Up; + s.link_state = if s.wifi_connection.is_connected() { + LinkState::Up } else { - s.link_state = LinkState::Down; - } + LinkState::Down + }; s.state_waker.wake(); s.connection_waker.wake(); }) } - pub fn is_connected(&self, cx: Option<&mut Context>) -> bool { + 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 { @@ -105,15 +135,13 @@ impl<'d> Runner<'d> { }) } - pub async fn wait_connection_change(&mut self) -> bool { - let old_state = self - .shared - .lock(|s| s.borrow().wifi_connection.is_connected()); + pub(crate) async fn wait_connection_change(&self) -> bool { + let old_state = self.is_connected(None); poll_fn(|cx| { - let current_state = self.is_connected(Some(cx)); - if current_state != old_state { - return Poll::Ready(current_state); + let new_state = self.is_connected(Some(cx)); + if new_state != old_state { + return Poll::Ready(new_state); } Poll::Pending }) diff --git a/src/command/custom_digest.rs b/src/command/custom_digest.rs index 1db8831..2b4b537 100644 --- a/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! @@ -215,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/src/command/data_mode/mod.rs b/src/command/data_mode/mod.rs index 616b03d..ca9cdfb 100644 --- a/src/command/data_mode/mod.rs +++ b/src/command/data_mode/mod.rs @@ -27,7 +27,7 @@ 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 = "ublox-sockets")] +#[cfg(feature = "internal-network-stack")] #[derive(Clone, AtatCmd)] #[at_cmd("+UDCP", ConnectPeerResponse, timeout_ms = 5000)] pub struct ConnectPeer<'a> { @@ -38,12 +38,12 @@ pub struct ConnectPeer<'a> { /// 5.3 Close peer connection +UDCPC /// /// Closes an existing peer connection. -#[cfg(feature = "ublox-sockets")] +#[cfg(feature = "internal-network-stack")] #[derive(Clone, AtatCmd)] #[at_cmd("+UDCPC", NoResponse, timeout_ms = 1000)] pub struct ClosePeerConnection { #[at_arg(position = 0, len = 1)] - pub peer_handle: PeerHandle, + pub peer_handle: ublox_sockets::PeerHandle, } /// 5.4 Default remote peer +UDDRP @@ -66,7 +66,7 @@ pub struct SetDefaultRemotePeer<'a> { /// 5.5 Peer list +UDLP /// /// This command reads the connected peers (peer handle). -#[cfg(feature = "ublox-sockets")] +#[cfg(feature = "internal-network-stack")] #[derive(Clone, AtatCmd)] #[at_cmd("+UDLP?", PeerListResponse, timeout_ms = 1000)] pub struct PeerList; @@ -129,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/src/command/data_mode/responses.rs b/src/command/data_mode/responses.rs index 03354b0..fddac87 100644 --- a/src/command/data_mode/responses.rs +++ b/src/command/data_mode/responses.rs @@ -2,7 +2,7 @@ use atat::atat_derive::AtatResp; /// 5.2 Connect peer +UDCP -#[cfg(feature = "ublox-sockets")] +#[cfg(feature = "internal-network-stack")] #[derive(Clone, AtatResp)] pub struct ConnectPeerResponse { #[at_arg(position = 0)] @@ -10,7 +10,7 @@ pub struct ConnectPeerResponse { } /// 5.5 Peer list +UDLP -#[cfg(feature = "ublox-sockets")] +#[cfg(feature = "internal-network-stack")] #[derive(Clone, AtatResp)] pub struct PeerListResponse { #[at_arg(position = 0)] diff --git a/src/command/data_mode/urc.rs b/src/command/data_mode/urc.rs index 4ec7f75..b768654 100644 --- a/src/command/data_mode/urc.rs +++ b/src/command/data_mode/urc.rs @@ -3,7 +3,7 @@ use super::types::*; /// 5.10 Peer connected +UUDPC -#[cfg(feature = "ublox-sockets")] +#[cfg(feature = "internal-network-stack")] #[derive(Debug, PartialEq, Clone, atat::atat_derive::AtatResp)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct PeerConnected { @@ -30,7 +30,7 @@ pub struct PeerConnected { } /// 5.11 Peer disconnected +UUDPD -#[cfg(feature = "ublox-sockets")] +#[cfg(feature = "internal-network-stack")] #[derive(Debug, PartialEq, Clone, atat::atat_derive::AtatResp)] pub struct PeerDisconnected { #[at_arg(position = 0)] diff --git a/src/command/edm/mod.rs b/src/command/edm/mod.rs index 691c47b..61694fb 100644 --- a/src/command/edm/mod.rs +++ b/src/command/edm/mod.rs @@ -69,7 +69,7 @@ impl atat::AtatCmd for EdmAtCmdWrapper { 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") diff --git a/src/command/edm/urc.rs b/src/command/edm/urc.rs index 4b78503..ef629e6 100644 --- a/src/command/edm/urc.rs +++ b/src/command/edm/urc.rs @@ -73,7 +73,7 @@ impl AtatUrc for EdmEvent { }; let payload_len = calc_payload_len(resp); if resp.len() != payload_len + EDM_OVERHEAD { - error!("[Parse URC lenght Error] {:?}", LossyStr(resp)); + error!("[Parse URC length Error] {:?}", LossyStr(resp)); return None; } diff --git a/src/command/general/mod.rs b/src/command/general/mod.rs index 66e1b65..08ce8d8 100644 --- a/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/src/command/general/responses.rs b/src/command/general/responses.rs index aea6181..155c8d3 100644 --- a/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/src/command/gpio/responses.rs b/src/command/gpio/responses.rs index e51e308..9a4bbac 100644 --- a/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/src/command/mod.rs b/src/command/mod.rs index 6b2b680..e237b39 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -30,11 +30,11 @@ pub enum Urc { #[at_urc("+STARTUP")] StartUp, /// 5.10 Peer connected +UUDPC - #[cfg(feature = "ublox-sockets")] + #[cfg(feature = "internal-network-stack")] #[at_urc("+UUDPC")] PeerConnected(data_mode::urc::PeerConnected), /// 5.11 Peer disconnected +UUDPD - #[cfg(feature = "ublox-sockets")] + #[cfg(feature = "internal-network-stack")] #[at_urc("+UUDPD")] PeerDisconnected(data_mode::urc::PeerDisconnected), /// 7.15 Wi-Fi Link connected +UUWLE diff --git a/src/command/ping/types.rs b/src/command/ping/types.rs index 16fdaa8..3999593 100644 --- a/src/command/ping/types.rs +++ b/src/command/ping/types.rs @@ -18,12 +18,12 @@ 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)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] diff --git a/src/command/system/types.rs b/src/command/system/types.rs index dbec961..b00e393 100644 --- a/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 diff --git a/src/command/wifi/types.rs b/src/command/wifi/types.rs index 5772825..ccad35d 100644 --- a/src/command/wifi/types.rs +++ b/src/command/wifi/types.rs @@ -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 { /// 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 From 1e6cfbccc81e4c261538923b4921f459dd6e7457 Mon Sep 17 00:00:00 2001 From: Mathias Date: Tue, 9 Jul 2024 10:13:44 +0200 Subject: [PATCH 6/7] Working control handle for connect and disconnect, with ppp udp bridge --- src/asynch/control.rs | 213 +++++++++++++++++----------------- src/asynch/network.rs | 95 +++++++++------ src/asynch/runner.rs | 199 ++++++++++++------------------- src/asynch/state.rs | 44 ++++++- src/asynch/ublox_stack/mod.rs | 11 +- src/connection.rs | 6 + src/error.rs | 1 + 7 files changed, 296 insertions(+), 273 deletions(-) diff --git a/src/asynch/control.rs b/src/asynch/control.rs index 031d59d..ed4206d 100644 --- a/src/asynch/control.rs +++ b/src/asynch/control.rs @@ -1,3 +1,5 @@ +use core::cell::Cell; + use atat::{asynch::AtatClient, response_slot::ResponseSlotGuard, UrcChannel}; use embassy_sync::{blocking_mutex::raw::NoopRawMutex, channel::Sender}; use embassy_time::{with_timeout, Duration, Timer}; @@ -30,6 +32,7 @@ use crate::command::{ system::{RebootDCE, ResetToFactoryDefaults}, wifi::types::AccessPointId, }; +use crate::connection::WiFiState; use crate::error::Error; use super::runner::{MAX_CMD_LEN, URC_SUBSCRIBERS}; @@ -41,7 +44,7 @@ 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: Option, + cooldown_timer: Cell>, } impl<'a, const INGRESS_BUF_SIZE: usize> ProxyClient<'a, INGRESS_BUF_SIZE> { @@ -52,14 +55,14 @@ impl<'a, const INGRESS_BUF_SIZE: usize> ProxyClient<'a, INGRESS_BUF_SIZE> { Self { req_sender, res_slot, - cooldown_timer: None, + cooldown_timer: Cell::new(None), } } - async fn wait_response<'guard>( - &'guard mut self, + async fn wait_response( + &self, timeout: Duration, - ) -> Result, atat::Error> { + ) -> Result, atat::Error> { with_timeout(timeout, self.res_slot.get()) .await .map_err(|_| atat::Error::Timeout) @@ -67,7 +70,7 @@ impl<'a, const INGRESS_BUF_SIZE: usize> ProxyClient<'a, INGRESS_BUF_SIZE> { } impl<'a, const INGRESS_BUF_SIZE: usize> atat::asynch::AtatClient - for ProxyClient<'a, INGRESS_BUF_SIZE> + for &ProxyClient<'a, INGRESS_BUF_SIZE> { async fn send(&mut self, cmd: &Cmd) -> Result { let mut buf = [0u8; MAX_CMD_LEN]; @@ -91,7 +94,7 @@ impl<'a, const INGRESS_BUF_SIZE: usize> atat::asynch::AtatClient .send(Vec::try_from(&buf[..len]).unwrap()) .await; - self.cooldown_timer = Some(Timer::after(Duration::from_millis(20))); + self.cooldown_timer.set(Some(Timer::after_millis(20))); if !Cmd::EXPECTS_RESPONSE_CODE { cmd.parse(Ok(&[])) @@ -127,21 +130,20 @@ impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> } } - pub async fn set_hostname(&mut self, hostname: &str) -> Result<(), Error> { + pub async fn set_hostname(&self, hostname: &str) -> Result<(), Error> { self.state_ch.wait_for_initialized().await; - self.at_client - .send(&SetNetworkHostName { + (&(&self).at_client) + .send_retry(&SetNetworkHostName { host_name: hostname, }) .await?; Ok(()) } - async fn get_wifi_status(&mut self) -> Result { - match self - .at_client - .send(&GetWifiStatus { + async fn get_wifi_status(&self) -> Result { + match (&self.at_client) + .send_retry(&GetWifiStatus { status_id: StatusId::Status, }) .await? @@ -152,10 +154,9 @@ impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> } } - async fn get_connected_ssid(&mut self) -> Result, Error> { - match self - .at_client - .send(&GetWifiStatus { + async fn get_connected_ssid(&self) -> Result, Error> { + match (&self.at_client) + .send_retry(&GetWifiStatus { status_id: StatusId::SSID, }) .await? @@ -166,28 +167,30 @@ impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> } } - pub async fn factory_reset(&mut self) -> Result<(), Error> { + pub async fn factory_reset(&self) -> Result<(), Error> { self.state_ch.wait_for_initialized().await; - self.at_client.send(&ResetToFactoryDefaults).await?; - self.at_client.send(&RebootDCE).await?; + (&self.at_client) + .send_retry(&ResetToFactoryDefaults) + .await?; + (&self.at_client).send_retry(&RebootDCE).await?; Ok(()) } - pub async fn start_ap(&mut self, ssid: &str) -> Result<(), Error> { + pub async fn start_ap(&self, ssid: &str) -> Result<(), Error> { self.state_ch.wait_for_initialized().await; // Deactivate network id 0 - self.at_client - .send(&WifiAPAction { + (&self.at_client) + .send_retry(&WifiAPAction { ap_config_id: AccessPointId::Id0, ap_action: AccessPointAction::Deactivate, }) .await?; - self.at_client - .send(&WifiAPAction { + (&self.at_client) + .send_retry(&WifiAPAction { ap_config_id: AccessPointId::Id0, ap_action: AccessPointAction::Reset, }) @@ -195,8 +198,8 @@ impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> // // 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(&SetWifiAPConfig { + // (&self.at_client) + // .send_retry(&SetWifiAPConfig { // ap_config_id: AccessPointId::Id0, // ap_config_param: AccessPointConfig::IPv4Mode(IPv4Mode::Static), // }) @@ -205,8 +208,8 @@ impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> // // Network IP address // if let Some(ip) = options.ip { - // self.at_client - // .send(&SetWifiAPConfig { + // (&self.at_client) + // .send_retry(&SetWifiAPConfig { // ap_config_id: AccessPointId::Id0, // ap_config_param: AccessPointConfig::IPv4Address(ip), // }) @@ -214,8 +217,8 @@ impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> // } // // Network Subnet mask // if let Some(subnet) = options.subnet { - // self.at_client - // .send(&SetWifiAPConfig { + // (&self.at_client) + // .send_retry(&SetWifiAPConfig { // ap_config_id: AccessPointId::Id0, // ap_config_param: AccessPointConfig::SubnetMask(subnet), // }) @@ -223,16 +226,16 @@ impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> // } // // Network Default gateway // if let Some(gateway) = options.gateway { - // self.at_client - // .send(&SetWifiAPConfig { + // (&self.at_client) + // .send_retry(&SetWifiAPConfig { // ap_config_id: AccessPointId::Id0, // ap_config_param: AccessPointConfig::DefaultGateway(gateway), // }) // .await?; // } - // self.at_client - // .send(&SetWifiAPConfig { + // (&self.at_client) + // .send_retry(&SetWifiAPConfig { // ap_config_id: AccessPointId::Id0, // ap_config_param: AccessPointConfig::DHCPServer(true.into()), // }) @@ -240,8 +243,8 @@ impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> // Wifi part // Set the Network SSID to connect to - self.at_client - .send(&SetWifiAPConfig { + (&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)?, @@ -251,8 +254,8 @@ impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> // if let Some(pass) = options.password.clone() { // // Use WPA2 as authentication type - // self.at_client - // .send(&SetWifiAPConfig { + // (&self.at_client) + // .send_retry(&SetWifiAPConfig { // ap_config_id: AccessPointId::Id0, // ap_config_param: AccessPointConfig::SecurityMode( // SecurityMode::Wpa2AesCcmp, @@ -262,15 +265,15 @@ impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> // .await?; // // Input passphrase - // self.at_client - // .send(&SetWifiAPConfig { + // (&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(&SetWifiAPConfig { + (&self.at_client) + .send_retry(&SetWifiAPConfig { ap_config_id: AccessPointId::Id0, ap_config_param: AccessPointConfig::SecurityMode( SecurityMode::Open, @@ -281,16 +284,16 @@ impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> // } // if let Some(channel) = configuration.channel { - // self.at_client - // .send(&SetWifiAPConfig { + // (&self.at_client) + // .send_retry(&SetWifiAPConfig { // ap_config_id: AccessPointId::Id0, // ap_config_param: AccessPointConfig::Channel(channel as u8), // }) // .await?; // } - self.at_client - .send(&WifiAPAction { + (&self.at_client) + .send_retry(&WifiAPAction { ap_config_id: AccessPointId::Id0, ap_action: AccessPointAction::Activate, }) @@ -299,7 +302,7 @@ impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> Ok(()) } - pub async fn join_open(&mut self, ssid: &str) -> Result<(), Error> { + pub async fn join_open(&self, ssid: &str) -> Result<(), Error> { self.state_ch.wait_for_initialized().await; if matches!(self.get_wifi_status().await?, WifiStatusVal::Connected) { @@ -312,22 +315,22 @@ impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> }; } - self.at_client - .send(&ExecWifiStationAction { + (&self.at_client) + .send_retry(&ExecWifiStationAction { config_id: CONFIG_ID, action: WifiStationAction::Reset, }) .await?; - self.at_client - .send(&SetWifiStationConfig { + (&self.at_client) + .send_retry(&SetWifiStationConfig { config_id: CONFIG_ID, config_param: WifiStationConfig::ActiveOnStartup(OnOff::Off), }) .await?; - self.at_client - .send(&SetWifiStationConfig { + (&self.at_client) + .send_retry(&SetWifiStationConfig { config_id: CONFIG_ID, config_param: WifiStationConfig::SSID( heapless::String::try_from(ssid).map_err(|_| Error::Overflow)?, @@ -335,28 +338,24 @@ impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> }) .await?; - self.at_client - .send(&SetWifiStationConfig { + (&self.at_client) + .send_retry(&SetWifiStationConfig { config_id: CONFIG_ID, config_param: WifiStationConfig::Authentication(Authentication::Open), }) .await?; - self.at_client - .send(&ExecWifiStationAction { + (&self.at_client) + .send_retry(&ExecWifiStationAction { config_id: CONFIG_ID, action: WifiStationAction::Activate, }) .await?; - with_timeout(Duration::from_secs(25), self.wait_for_join(ssid)) - .await - .map_err(|_| Error::Timeout)??; - - Ok(()) + self.wait_for_join(ssid, Duration::from_secs(20)).await } - pub async fn join_wpa2(&mut self, ssid: &str, passphrase: &str) -> Result<(), Error> { + pub async fn join_wpa2(&self, ssid: &str, passphrase: &str) -> Result<(), Error> { self.state_ch.wait_for_initialized().await; if matches!(self.get_wifi_status().await?, WifiStatusVal::Connected) { @@ -369,22 +368,22 @@ impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> }; } - self.at_client - .send(&ExecWifiStationAction { + (&self.at_client) + .send_retry(&ExecWifiStationAction { config_id: CONFIG_ID, action: WifiStationAction::Reset, }) .await?; - self.at_client - .send(&SetWifiStationConfig { + (&self.at_client) + .send_retry(&SetWifiStationConfig { config_id: CONFIG_ID, config_param: WifiStationConfig::ActiveOnStartup(OnOff::Off), }) .await?; - self.at_client - .send(&SetWifiStationConfig { + (&self.at_client) + .send_retry(&SetWifiStationConfig { config_id: CONFIG_ID, config_param: WifiStationConfig::SSID( heapless::String::try_from(ssid).map_err(|_| Error::Overflow)?, @@ -392,15 +391,15 @@ impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> }) .await?; - self.at_client - .send(&SetWifiStationConfig { + (&self.at_client) + .send_retry(&SetWifiStationConfig { config_id: CONFIG_ID, config_param: WifiStationConfig::Authentication(Authentication::WpaWpa2Psk), }) .await?; - self.at_client - .send(&SetWifiStationConfig { + (&self.at_client) + .send_retry(&SetWifiStationConfig { config_id: CONFIG_ID, config_param: WifiStationConfig::WpaPskOrPassphrase( heapless::String::try_from(passphrase).map_err(|_| Error::Overflow)?, @@ -408,28 +407,24 @@ impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> }) .await?; - self.at_client - .send(&ExecWifiStationAction { + (&self.at_client) + .send_retry(&ExecWifiStationAction { config_id: CONFIG_ID, action: WifiStationAction::Activate, }) .await?; - with_timeout(Duration::from_secs(20), self.wait_for_join(ssid)) - .await - .map_err(|_| Error::Timeout)??; - - Ok(()) + self.wait_for_join(ssid, Duration::from_secs(20)).await } - pub async fn disconnect(&mut self) -> Result<(), Error> { + pub async fn disconnect(&self) -> Result<(), Error> { self.state_ch.wait_for_initialized().await; match self.get_wifi_status().await? { WifiStatusVal::Disabled => {} WifiStatusVal::Disconnected | WifiStatusVal::Connected => { - self.at_client - .send(&ExecWifiStationAction { + (&self.at_client) + .send_retry(&ExecWifiStationAction { config_id: CONFIG_ID, action: WifiStationAction::Deactivate, }) @@ -439,7 +434,7 @@ impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> with_timeout( Duration::from_secs(10), - self.state_ch.wait_for_link_state(LinkState::Down), + self.state_ch.wait_connection_down(), ) .await .map_err(|_| Error::Timeout)?; @@ -447,27 +442,33 @@ impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> Ok(()) } - async fn wait_for_join(&mut self, ssid: &str) -> Result<(), Error> { - // TODO: Handle returning error in case of security problems + 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); + } - self.state_ch.wait_for_link_state(LinkState::Up).await; - - // 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 => { + Err(Error::SecurityProblems) + } + Err(_) => Err(Error::Timeout), } - - Ok(()) } - pub async fn gpio_configure(&mut self, id: GPIOId, mode: GPIOMode) -> Result<(), Error> { + pub async fn gpio_configure(&self, id: GPIOId, mode: GPIOMode) -> Result<(), Error> { self.state_ch.wait_for_initialized().await; - self.at_client.send(&ConfigureGPIO { id, mode }).await?; + (&self.at_client) + .send_retry(&ConfigureGPIO { id, mode }) + .await?; Ok(()) } - pub async fn gpio_set(&mut self, id: GPIOId, value: bool) -> Result<(), Error> { + pub async fn gpio_set(&self, id: GPIOId, value: bool) -> Result<(), Error> { self.state_ch.wait_for_initialized().await; let value = if value { @@ -476,14 +477,16 @@ impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> GPIOValue::Low }; - self.at_client.send(&WriteGPIO { id, value }).await?; + (&self.at_client) + .send_retry(&WriteGPIO { id, value }) + .await?; Ok(()) } - pub async fn gpio_get(&mut self, id: GPIOId) -> Result { + pub async fn gpio_get(&self, id: GPIOId) -> Result { self.state_ch.wait_for_initialized().await; - let ReadGPIOResponse { value, .. } = self.at_client.send(&ReadGPIO { id }).await?; + let ReadGPIOResponse { value, .. } = (&self.at_client).send_retry(&ReadGPIO { id }).await?; Ok(value as u8 != 0) } @@ -500,8 +503,8 @@ impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> // info!("Importing {:?} bytes as {:?}", data.len(), name); - // self.at_client - // .send(&PrepareSecurityDataImport { + // (&self.at_client) + // .send_retry(&PrepareSecurityDataImport { // data_type, // data_size: data.len(), // internal_name: name, @@ -511,7 +514,7 @@ impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> // let import_data = self // .at_client - // .send(&SendSecurityDataImport { + // .send_retry(&SendSecurityDataImport { // data: atat::serde_bytes::Bytes::new(data), // }) // .await?; diff --git a/src/asynch/network.rs b/src/asynch/network.rs index 5d73e1e..4e398fa 100644 --- a/src/asynch/network.rs +++ b/src/asynch/network.rs @@ -62,17 +62,19 @@ where // Hard reset module self.reset().await?; - self.at_client.send(&SoftwareVersion).await?; - self.at_client.send(&SetEcho { on: EchoOn::Off }).await?; + self.at_client.send_retry(&SoftwareVersion).await?; self.at_client - .send(&SetWifiConfig { + .send_retry(&SetEcho { on: EchoOn::Off }) + .await?; + self.at_client + .send_retry(&SetWifiConfig { config_param: WifiConfigParam::DropNetworkOnLinkLoss(OnOff::On), }) .await?; // Disable all power savings for now self.at_client - .send(&SetWifiConfig { + .send_retry(&SetWifiConfig { config_param: WifiConfigParam::PowerSaveMode(PowerSaveMode::ActiveMode), }) .await?; @@ -80,7 +82,7 @@ where #[cfg(feature = "internal-network-stack")] if let Some(size) = C::TLS_IN_BUFFER_SIZE { self.at_client - .send(&crate::command::data_mode::SetPeerConfiguration { + .send_retry(&crate::command::data_mode::SetPeerConfiguration { parameter: crate::command::data_mode::types::PeerConfigParameter::TlsInBuffer( size, ), @@ -91,7 +93,7 @@ where #[cfg(feature = "internal-network-stack")] if let Some(size) = C::TLS_OUT_BUFFER_SIZE { self.at_client - .send(&crate::command::data_mode::SetPeerConfiguration { + .send_retry(&crate::command::data_mode::SetPeerConfiguration { parameter: crate::command::data_mode::types::PeerConfigParameter::TlsOutBuffer( size, ), @@ -109,20 +111,37 @@ where self.init().await?; } - loop { - let event = self.urc_subscription.next_message_pure().await; + let mut link_was_up = false; - #[cfg(feature = "edm")] - let Some(event) = event.extract_urc() else { - continue; - }; + 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?; + } + embassy_futures::select::Either::Second(_) => {} + } - self.handle_urc(event).await?; + match self.ch.wifi_state(None) { + WiFiState::Inactive if self.ch.connection_down(None) && link_was_up => { + return Ok(()) + } + WiFiState::Connected if self.ch.is_connected(None) => link_was_up = true, + _ => {} + } } } async fn handle_urc(&mut self, event: Urc) -> Result<(), Error> { - debug!("GOT URC event"); match event { Urc::StartUp => { error!("AT startup event?! Device restarted unintentionally!"); @@ -161,11 +180,9 @@ where Urc::EthernetLinkUp(_) => warn!("Not yet implemented [EthernetLinkUp]"), Urc::EthernetLinkDown(_) => warn!("Not yet implemented [EthernetLinkDown]"), Urc::NetworkUp(NetworkUp { interface_id }) => { - drop(event); self.network_status_callback(interface_id).await?; } Urc::NetworkDown(NetworkDown { interface_id }) => { - drop(event); self.network_status_callback(interface_id).await?; } Urc::NetworkError(_) => warn!("Not yet implemented [NetworkError]"), @@ -188,7 +205,7 @@ where .. } = self .at_client - .send(&GetNetworkStatus { + .send_retry(&GetNetworkStatus { interface_id, status: NetworkStatusParameter::InterfaceType, }) @@ -202,7 +219,7 @@ where .. } = self .at_client - .send(&GetNetworkStatus { + .send_retry(&GetNetworkStatus { interface_id, status: NetworkStatusParameter::IPv4Address, }) @@ -211,18 +228,17 @@ where return Err(Error::Network); }; - let ipv4_up = core::str::from_utf8(ipv4.as_slice()) + let ipv4_addr = core::str::from_utf8(ipv4.as_slice()) .ok() .and_then(|s| Ipv4Addr::from_str(s).ok()) - .map(|ip| !ip.is_unspecified()) - .unwrap_or_default(); + .and_then(|ip| (!ip.is_unspecified()).then_some(ip)); let NetworkStatusResponse { status: NetworkStatus::IPv6LinkLocalAddress(ipv6), .. } = self .at_client - .send(&GetNetworkStatus { + .send_retry(&GetNetworkStatus { interface_id, status: NetworkStatusParameter::IPv6LinkLocalAddress, }) @@ -231,15 +247,17 @@ where return Err(Error::Network); }; - let ipv6_up = core::str::from_utf8(ipv6.as_slice()) + let ipv6_addr = core::str::from_utf8(ipv6.as_slice()) .ok() .and_then(|s| Ipv6Addr::from_str(s).ok()) - .map(|ip| !ip.is_unspecified()) - .unwrap_or_default(); + .and_then(|ip| (!ip.is_unspecified()).then_some(ip)); - // Use `ipv4_up` & `ipv6_up` to determine link state - self.ch - .update_connection_with(|con| con.network_up = ipv4_up && ipv6_up); + // Use `ipv4_addr` & `ipv6_addr` to determine link state + self.ch.update_connection_with(|con| { + con.ipv4 = ipv4_addr; + con.ipv6 = ipv6_addr; + con.network_up = ipv4_addr.is_some() && ipv6_addr.is_some() + }); Ok(()) } @@ -265,12 +283,17 @@ where } pub async fn reset(&mut self) -> Result<(), Error> { - warn!("Hard resetting Ublox Short Range"); - self.config.reset_pin().unwrap().set_low().ok(); - Timer::after(Duration::from_millis(100)).await; - self.config.reset_pin().unwrap().set_high().ok(); + 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.wait_startup(Duration::from_secs(4)).await?; + self.wait_startup(Duration::from_secs(10)).await?; #[cfg(feature = "edm")] self.enter_edm(Duration::from_secs(4)).await?; @@ -282,10 +305,10 @@ where pub async fn restart(&mut self, store: bool) -> Result<(), Error> { warn!("Soft resetting Ublox Short Range"); if store { - self.at_client.send(&StoreCurrentConfig).await?; + self.at_client.send_retry(&StoreCurrentConfig).await?; } - self.at_client.send(&RebootDCE).await?; + self.at_client.send_retry(&RebootDCE).await?; self.wait_startup(Duration::from_secs(10)).await?; @@ -306,7 +329,7 @@ where // Ignore AT results until we are successful in EDM mode if let Ok(_) = self .at_client - .send(&crate::command::edm::SwitchToEdmCommand) + .send_retry(&crate::command::edm::SwitchToEdmCommand) .await { // After executing the data mode command or the extended data diff --git a/src/asynch/runner.rs b/src/asynch/runner.rs index e255e14..8836b8a 100644 --- a/src/asynch/runner.rs +++ b/src/asynch/runner.rs @@ -14,7 +14,7 @@ use atat::{ AtatIngress as _, UrcChannel, }; use embassy_sync::{blocking_mutex::raw::NoopRawMutex, channel::Channel}; -use embassy_time::{Duration, Instant, Timer}; +use embassy_time::{Duration, Timer}; use embedded_io_async::{BufRead, Read, Write}; #[cfg(feature = "ppp")] @@ -187,26 +187,16 @@ where ) -> ! { let at_config = atat::Config::default(); - let network_fut = async { - loop { + loop { + let network_fut = async { // Allow control to send/receive AT commands directly on the // UART, until we are ready to establish connection using PPP // Send "+++" to escape data mode, and enter command mode - // warn!("Escaping to command mode!"); - // Timer::after_secs(5).await; - // self.iface.1.write_all(b"+++").await.ok(); - // Timer::after_secs(1).await; - let mut iface = super::ReadWriteAdapter(&mut self.iface.0, &mut self.iface.1); - iface.write_all(b"+++").await.ok(); - let mut buf = [0u8; 8]; - - let _ = embassy_time::with_timeout(Duration::from_millis(500), async { - loop { - iface.read(&mut buf).await.ok(); - } - }) - .await; + warn!("Escaping to command mode!"); + Timer::after_secs(1).await; + self.iface.1.write_all(b"+++").await.ok(); + Timer::after_secs(1).await; let _ = embassy_futures::select::select( at_bridge( @@ -223,70 +213,69 @@ where let ppp_fut = async { let mut iface = super::ReadWriteAdapter(&mut self.iface.0, &mut self.iface.1); - loop { - self.ch.wait_for_link_state(LinkState::Up).await; - - { - let mut buf = [0u8; 8]; - let mut at_client = SimpleClient::new( - &mut iface, - atat::AtDigester::::new(), - &mut buf, - at_config, - ); - - // Send AT command `ATO3` to enter PPP mode - let res = at_client - .send(&ChangeMode { - mode: data_mode::types::Mode::PPPMode, - }) - .await; - - if let Err(e) = res { - warn!("ppp dial failed {:?}", e); - continue; - } - - // Drain the UART - let _ = embassy_time::with_timeout(Duration::from_millis(500), async { - loop { - iface.read(&mut buf).await.ok(); - } + self.ch.wait_for_link_state(LinkState::Up).await; + + { + let mut buf = [0u8; 8]; + let mut at_client = SimpleClient::new( + &mut iface, + atat::AtDigester::::new(), + &mut buf, + at_config, + ); + + // Send AT command `ATO3` to enter PPP mode + let res = at_client + .send_retry(&ChangeMode { + mode: data_mode::types::Mode::PPPMode, }) .await; - } - info!("RUNNING PPP"); - let res = self - .ppp_runner - .as_mut() - .unwrap() - .run(&mut iface, C::PPP_CONFIG, |ipv4| { - let Some(addr) = ipv4.address else { - warn!("PPP did not provide an IP address."); - return; - }; - let mut dns_servers = heapless::Vec::new(); - for s in ipv4.dns_servers.iter().flatten() { - let _ = dns_servers - .push(embassy_net::Ipv4Address::from_bytes(&s.0)); - } - let config = - embassy_net::ConfigV4::Static(embassy_net::StaticConfigV4 { - address: embassy_net::Ipv4Cidr::new( - embassy_net::Ipv4Address::from_bytes(&addr.0), - 0, - ), - gateway: None, - dns_servers, - }); - - stack.set_config_v4(config); - }) - .await; + if let Err(e) = res { + warn!("ppp dial failed {:?}", e); + return; + } - info!("ppp failed: {:?}", res); + // Drain the UART + let _ = embassy_time::with_timeout(Duration::from_millis(500), async { + loop { + iface.read(&mut buf).await.ok(); + } + }) + .await; } + + info!("RUNNING PPP"); + let res = self + .ppp_runner + .as_mut() + .unwrap() + .run(&mut iface, 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: {:?}", res); }; let at_fut = async { @@ -311,61 +300,25 @@ where at_bridge(&at_socket, &at_socket, &self.req_slot, &mut self.ingress).await; }; - let break_fut = async { - let mut fails = 0; - let mut last_start = None; - - self.ch.wait_for_initialized().await; - - loop { - if let Some(last_start) = last_start { - info!("LINK DOWN! Attempt: {}", fails); - Timer::at(last_start + Duration::from_secs(10)).await; - // Do not attempt to start too fast. - - // If was up, and stable for at least 1 min, reset fail counter. - if Instant::now() > last_start + Duration::from_secs(60) { - fails = 0; - } else { - fails += 1; - if fails == 2 { - warn!("modem: Link down too much, rebooting modem."); - break; - } - } - } - last_start = Some(Instant::now()); - - self.ch.wait_for_link_state(LinkState::Down).await; - } - }; + embassy_futures::select::select(ppp_fut, at_fut).await; + }; - embassy_futures::select::select3(ppp_fut, at_fut, break_fut).await; - - warn!("Breaking WiFi network loop"); - } - }; - - let device_fut = async { - loop { - let mut device = NetDevice::new( - &self.ch, - &mut self.config, - ProxyClient::new(self.req_slot.sender(), &self.res_slot), - self.urc_channel, - ); + let device_fut = async { + let at_client = ProxyClient::new(self.req_slot.sender(), &self.res_slot); + let mut device = + NetDevice::new(&self.ch, &mut self.config, &at_client, self.urc_channel); if let Err(e) = device.init().await { error!("WiFi init failed {:?}", e); - continue; + return; }; let _ = device.run().await; - } - }; - embassy_futures::join::join(device_fut, network_fut).await; + warn!("Breaking to reboot device"); + }; - unreachable!() + embassy_futures::select::select(device_fut, network_fut).await; + } } } diff --git a/src/asynch/state.rs b/src/asynch/state.rs index 1f9e376..c01d3b6 100644 --- a/src/asynch/state.rs +++ b/src/asynch/state.rs @@ -8,7 +8,7 @@ use embassy_sync::blocking_mutex::raw::NoopRawMutex; use embassy_sync::blocking_mutex::Mutex; use embassy_sync::waitqueue::WakerRegistration; -use crate::connection::WifiConnection; +use crate::connection::{WiFiState, WifiConnection}; /// The link state of a network device. #[derive(PartialEq, Eq, Clone, Copy)] @@ -125,6 +125,30 @@ impl<'d> Runner<'d> { }) } + 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.is_none() && s.wifi_connection.ipv6.is_none() + }) + } + + 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(); @@ -135,12 +159,22 @@ impl<'d> Runner<'d> { }) } - pub(crate) async fn wait_connection_change(&self) -> bool { - let old_state = self.is_connected(None); + 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.is_connected(Some(cx)); - if new_state != old_state { + let new_state = self.wifi_state(Some(cx)); + if old_state != new_state { return Poll::Ready(new_state); } Poll::Pending diff --git a/src/asynch/ublox_stack/mod.rs b/src/asynch/ublox_stack/mod.rs index 06d9533..07247b6 100644 --- a/src/asynch/ublox_stack/mod.rs +++ b/src/asynch/ublox_stack/mod.rs @@ -407,7 +407,10 @@ impl let mut at = at_client.borrow_mut(); match ev { TxEvent::Connect { socket_handle, url } => { - match at.send(&EdmAtCmdWrapper(ConnectPeer { url: &url })).await { + match at + .send_retry(&EdmAtCmdWrapper(ConnectPeer { url: &url })) + .await + { Ok(ConnectPeerResponse { peer_handle }) => { let mut s = socket.borrow_mut(); let tcp = s @@ -423,7 +426,7 @@ impl } TxEvent::Send { edm_channel, data } => { warn!("Sending {} bytes on {}", data.len(), edm_channel); - at.send(&EdmDataCommand { + at.send_retry(&EdmDataCommand { channel: edm_channel, data, }) @@ -431,13 +434,13 @@ impl .ok(); } TxEvent::Close { peer_handle } => { - at.send(&EdmAtCmdWrapper(ClosePeerConnection { peer_handle })) + at.send_retry(&EdmAtCmdWrapper(ClosePeerConnection { peer_handle })) .await .ok(); } TxEvent::Dns { hostname } => { match at - .send(&EdmAtCmdWrapper(Ping { + .send_retry(&EdmAtCmdWrapper(Ping { hostname: &hostname, retry_num: 1, })) diff --git a/src/connection.rs b/src/connection.rs index a309f11..eab96f4 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -1,3 +1,5 @@ +use no_std_net::{Ipv4Addr, Ipv6Addr}; + use crate::network::{WifiMode, WifiNetwork}; #[derive(Debug, Clone, Copy, PartialEq)] @@ -13,6 +15,8 @@ pub enum WiFiState { pub struct WifiConnection { pub wifi_state: WiFiState, pub network_up: bool, + pub ipv4: Option, + pub ipv6: Option, pub network: Option, } @@ -22,6 +26,8 @@ impl WifiConnection { wifi_state: WiFiState::Inactive, network_up: false, network: None, + ipv4: None, + ipv6: None, } } diff --git a/src/error.rs b/src/error.rs index ea7bf9d..d655f44 100644 --- a/src/error.rs +++ b/src/error.rs @@ -7,6 +7,7 @@ pub enum Error { Overflow, SetState, BadLength, + SecurityProblems, Network, Pin, BaudDetection, From f9caa1a269eb8423236b6e825edd2e751efabcc8 Mon Sep 17 00:00:00 2001 From: Mathias Date: Tue, 16 Jul 2024 14:03:14 +0200 Subject: [PATCH 7/7] Add a large number of convenience functions to Control and cleanup runner patterns --- .github/workflows/audit.yml | 14 - .github/workflows/{lint.yml => ci.yml} | 61 ++-- .github/workflows/docs.yml | 47 ---- .github/workflows/grcov.yml | 78 ------ .github/workflows/test.yml | 35 --- .vscode/settings.json | 2 +- Cargo.toml | 20 +- README.md | 24 +- examples/rpi-pico/Cargo.toml | 2 +- src/asynch/at_udp_socket.rs | 12 + src/asynch/control.rs | 357 ++++++++++++++++++------ src/asynch/mod.rs | 34 --- src/asynch/network.rs | 138 ++++----- src/asynch/resources.rs | 2 +- src/asynch/runner.rs | 372 ++++++++++++++++++------- src/asynch/state.rs | 36 ++- src/asynch/ublox_stack/dns.rs | 4 +- src/command/mod.rs | 2 +- src/command/system/mod.rs | 14 +- src/command/system/responses.rs | 4 +- src/command/system/types.rs | 9 +- src/config.rs | 12 + src/connection.rs | 61 +++- src/error.rs | 6 + src/lib.rs | 19 +- src/network.rs | 2 + 26 files changed, 784 insertions(+), 583 deletions(-) delete mode 100644 .github/workflows/audit.yml rename .github/workflows/{lint.yml => ci.yml} (56%) delete mode 100644 .github/workflows/docs.yml delete mode 100644 .github/workflows/grcov.yml delete mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml deleted file mode 100644 index 4974b63..0000000 --- a/.github/workflows/audit.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Security audit -on: - push: - paths: - - '**/Cargo.toml' - - '**/Cargo.lock' -jobs: - security_audit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - uses: actions-rs/audit-check@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/lint.yml b/.github/workflows/ci.yml similarity index 56% rename from .github/workflows/lint.yml rename to .github/workflows/ci.yml index 338dbda..4ea3ed4 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Lint +name: CI on: push: @@ -10,9 +10,6 @@ defaults: run: shell: bash -env: - CLIPPY_PARAMS: -W clippy::all -W clippy::pedantic -W clippy::nursery -W clippy::cargo - jobs: rustfmt: name: rustfmt @@ -35,33 +32,6 @@ jobs: command: fmt args: --all -- --check --verbose - # tomlfmt: - # name: tomlfmt - # 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: nightly - # override: true - - # - name: Install tomlfmt - # uses: actions-rs/install@v0.1 - # with: - # crate: cargo-tomlfmt - # version: latest - # use-tool-cache: true - - # - name: Run Tomlfmt - # uses: actions-rs/cargo@v1 - # with: - # command: tomlfmt - # args: --dryrun - clippy: name: clippy runs-on: ubuntu-latest @@ -81,4 +51,31 @@ jobs: 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/.github/workflows/docs.yml b/.github/workflows/docs.yml deleted file mode 100644 index c9b0dba..0000000 --- a/.github/workflows/docs.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Documentation - -on: - push: - branches: - - master - -jobs: - docs: - name: Documentation - runs-on: ubuntu-latest - steps: - - name: Checkout source code - uses: actions/checkout@v2 - with: - persist-credentials: false - - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: nightly - override: true - - - name: Build documentation - uses: actions-rs/cargo@v1 - with: - command: doc - args: --verbose --no-deps - - # - name: Finalize documentation - # run: | - # CRATE_NAME=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]' | cut -f2 -d"/") - # echo "" > target/doc/index.html - # touch target/doc/.nojekyll - # - name: Upload as artifact - # uses: actions/upload-artifact@v2 - # with: - # name: Documentation - # path: target/doc - - # - name: Deploy - # uses: JamesIves/github-pages-deploy-action@releases/v3 - # with: - # ACCESS_TOKEN: ${{ secrets.GH_PAT }} - # BRANCH: gh-pages - # FOLDER: target/doc diff --git a/.github/workflows/grcov.yml b/.github/workflows/grcov.yml deleted file mode 100644 index af13453..0000000 --- a/.github/workflows/grcov.yml +++ /dev/null @@ -1,78 +0,0 @@ -# name: Coverage - -# on: -# push: -# branches: -# - master -# pull_request: - -# jobs: -# grcov: -# name: Coverage -# 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: nightly -# target: thumbv7m-none-eabi -# override: true - -# - name: Install grcov -# uses: actions-rs/cargo@v1 -# # uses: actions-rs/install@v0.1 -# with: -# # crate: grcov -# # version: latest -# # use-tool-cache: true -# command: install -# args: grcov --git https://github.com/mozilla/grcov - -# - name: Test -# uses: actions-rs/cargo@v1 -# with: -# command: test -# args: --lib --no-fail-fast -# env: -# CARGO_INCREMENTAL: "0" -# RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=unwind -Zpanic_abort_tests" -# RUSTDOCFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=unwind -Zpanic_abort_tests" - -# - name: Generate coverage data -# id: grcov -# # uses: actions-rs/grcov@v0.1 -# run: | -# grcov target/debug/ \ -# --branch \ -# --llvm \ -# --source-dir . \ -# --output-file lcov.info \ -# --ignore='/**' \ -# --ignore='C:/**' \ -# --ignore='../**' \ -# --ignore-not-existing \ -# --excl-line "#\\[derive\\(" \ -# --excl-br-line "(#\\[derive\\()|(debug_assert)" \ -# --excl-start "#\\[cfg\\(test\\)\\]" \ -# --excl-br-start "#\\[cfg\\(test\\)\\]" \ -# --commit-sha ${{ github.sha }} \ -# --service-job-id ${{ github.job }} \ -# --service-name "GitHub Actions" \ -# --service-number ${{ github.run_id }} -# - name: Upload coverage as artifact -# uses: actions/upload-artifact@v2 -# with: -# name: lcov.info -# # path: ${{ steps.grcov.outputs.report }} -# path: lcov.info - -# - name: Upload coverage to codecov.io -# uses: codecov/codecov-action@v1 -# with: -# # file: ${{ steps.grcov.outputs.report }} -# file: lcov.info -# fail_ci_if_error: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 9884357..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Test - -on: - push: - branches: - - master - pull_request: - -jobs: - 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 - - - name: Test - uses: actions-rs/cargo@v1 - with: - command: test - args: --lib diff --git a/.vscode/settings.json b/.vscode/settings.json index 1b2bf72..8fc7548 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,7 +7,7 @@ "rust-analyzer.check.allTargets": false, "rust-analyzer.linkedProjects": [], "rust-analyzer.cargo.features": [ - "odin_w2xx", + "odin-w2xx", // "internal-network-stack" "ppp" ], diff --git a/Cargo.toml b/Cargo.toml index 5d31af1..650b27a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ 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", features = ["edm"], optional = true } +# ublox-sockets = { version = "0.5", optional = true } ublox-sockets = { git = "https://github.com/BlackbirdHQ/ublox-sockets", rev = "9f7fe54", optional = true } portable-atomic = "1.6" @@ -50,6 +50,8 @@ 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"] @@ -66,12 +68,13 @@ defmt = [ ] log = ["dep:log", "ublox-sockets?/log", "atat/log"] -odin_w2xx = [] -nina_w1xx = [] -nina_b1xx = [] -anna_b1xx = [] -nina_b2xx = [] -nina_b3xx = [] +# Supported Ublox modules +odin-w2xx = [] +nina-w1xx = [] +nina-b1xx = [] +anna-b1xx = [] +nina-b2xx = [] +nina-b3xx = [] [workspace] members = [] @@ -81,4 +84,5 @@ exclude = ["examples"] [patch.crates-io] no-std-net = { git = "https://github.com/rushmorem/no-std-net", branch = "issue-15" } -atat = { path = "../atat/atat" } \ No newline at end of file +atat = { git = "https://github.com/BlackbirdHQ/atat", rev = "a466836" } +# atat = { path = "../atat/atat" } \ No newline at end of file diff --git a/README.md b/README.md index 4010a77..402312c 100644 --- a/README.md +++ b/README.md @@ -13,12 +13,12 @@ A driver crate for AT-command based serial ublox short range modules, built on top of [atat]. The driver aims to be compatible with the ublox short range modules: -- odin_w2xx -- nina_w1xx -- nina_b1xx -- anna_b1xx -- nina_b2xx -- nina_b3xx +- odin-w2xx +- nina-w1xx +- nina-b1xx +- anna-b1xx +- nina-b2xx +- nina-b3xx [atat]: https://crates.io/crates/atat @@ -48,12 +48,12 @@ The samples can be built using `cargo build -p linux_example --target x86_64-unk ## Features - device selection (must select one, and only one!): - - `odin_w2xx` - - `nina_w1xx` - - `nina_b1xx` - - `anna_b1xx` - - `nina_b2xx` - - `nina_b3xx` + - `odin-w2xx` + - `nina-w1xx` + - `nina-b1xx` + - `anna-b1xx` + - `nina-b2xx` + - `nina-b3xx` - `socket-tcp`: Enabled by default. Adds TCP socket capabilities, and implements [`TcpStack`] trait. - `socket-udp`: Enabled by default. Adds UDP socket capabilities, and implements [`UdpStack`] trait. - `defmt-default`: Disabled by default. Add log statements on trace (dev) or info (release) log levels to aid debugging. diff --git a/examples/rpi-pico/Cargo.toml b/examples/rpi-pico/Cargo.toml index 4a9d2a1..3a26943 100644 --- a/examples/rpi-pico/Cargo.toml +++ b/examples/rpi-pico/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] -ublox-short-range-rs = { path = "../../", features = ["odin_w2xx", "defmt"] } +ublox-short-range-rs = { path = "../../", features = ["odin-w2xx", "defmt"] } embassy-executor = { version = "0.5", features = [ "defmt", "integrated-timers", diff --git a/src/asynch/at_udp_socket.rs b/src/asynch/at_udp_socket.rs index 5b3f87f..4428181 100644 --- a/src/asynch/at_udp_socket.rs +++ b/src/asynch/at_udp_socket.rs @@ -1,6 +1,8 @@ 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> { @@ -32,6 +34,16 @@ impl<'a> Write for &AtUdpSocket<'a> { } } +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; } diff --git a/src/asynch/control.rs b/src/asynch/control.rs index ed4206d..8f03d79 100644 --- a/src/asynch/control.rs +++ b/src/asynch/control.rs @@ -1,13 +1,26 @@ 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::{ @@ -32,13 +45,19 @@ use crate::command::{ system::{RebootDCE, ResetToFactoryDefaults}, wifi::types::AccessPointId, }; -use crate::connection::WiFiState; +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> { @@ -90,9 +109,12 @@ impl<'a, const INGRESS_BUF_SIZE: usize> atat::asynch::AtatClient } // TODO: Guard against race condition! - self.req_sender - .send(Vec::try_from(&buf[..len]).unwrap()) - .await; + 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))); @@ -111,7 +133,7 @@ impl<'a, const INGRESS_BUF_SIZE: usize> atat::asynch::AtatClient 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, + urc_channel: &'a UrcChannel, } impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> @@ -119,21 +141,22 @@ impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> { pub(crate) fn new( state_ch: state::Runner<'a>, - urc_channel: &'a UrcChannel, + 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, + 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) + (&self.at_client) .send_retry(&SetNetworkHostName { host_name: hostname, }) @@ -141,6 +164,28 @@ impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> 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 { @@ -154,7 +199,91 @@ impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> } } - async fn get_connected_ssid(&self) -> Result, Error> { + 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, @@ -178,7 +307,7 @@ impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> Ok(()) } - pub async fn start_ap(&self, ssid: &str) -> Result<(), Error> { + async fn start_ap(&self, ssid: &str) -> Result<(), Error> { self.state_ch.wait_for_initialized().await; // Deactivate network id 0 @@ -302,16 +431,32 @@ impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> Ok(()) } - pub async fn join_open(&self, ssid: &str) -> Result<(), Error> { + /// Start open access point. + pub async fn start_ap_open(&mut self, ssid: &str, channel: u8) { + todo!() + } + + /// Start WPA2 protected access point. + pub async fn start_ap_wpa2(&mut self, ssid: &str, passphrase: &str, channel: u8) { + todo!() + } + + /// 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.disconnect().await?; + self.leave().await?; }; } @@ -338,87 +483,82 @@ impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> }) .await?; - (&self.at_client) - .send_retry(&SetWifiStationConfig { - config_id: CONFIG_ID, - config_param: WifiStationConfig::Authentication(Authentication::Open), - }) - .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 - } + 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?; - pub async fn join_wpa2(&self, ssid: &str, passphrase: &str) -> Result<(), Error> { - self.state_ch.wait_for_initialized().await; + (&self.at_client) + .send_retry(&SetWifiStationConfig { + config_id: CONFIG_ID, + config_param: WifiStationConfig::WpaPskOrPassphrase( + heapless::String::try_from(passphrase).map_err(|_| Error::Overflow)?, + ), + }) + .await?; + } + WifiAuthentication::Wpa2Psk(psk) => { + (&self.at_client) + .send_retry(&SetWifiStationConfig { + config_id: CONFIG_ID, + config_param: WifiStationConfig::Authentication(Authentication::WpaWpa2Psk), + }) + .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 { - return Ok(()); - } else { - self.disconnect().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::Reset, - }) - .await?; - - (&self.at_client) - .send_retry(&SetWifiStationConfig { - config_id: CONFIG_ID, - config_param: WifiStationConfig::ActiveOnStartup(OnOff::Off), + action: WifiStationAction::Activate, }) .await?; - (&self.at_client) - .send_retry(&SetWifiStationConfig { - config_id: CONFIG_ID, - config_param: WifiStationConfig::SSID( - heapless::String::try_from(ssid).map_err(|_| Error::Overflow)?, - ), - }) - .await?; + self.wait_for_join(ssid, Duration::from_secs(20)).await?; + self.state_ch.set_should_connect(true); - (&self.at_client) - .send_retry(&SetWifiStationConfig { - config_id: CONFIG_ID, - config_param: WifiStationConfig::Authentication(Authentication::WpaWpa2Psk), - }) - .await?; + Ok(()) + } - (&self.at_client) - .send_retry(&SetWifiStationConfig { - config_id: CONFIG_ID, - config_param: WifiStationConfig::WpaPskOrPassphrase( - heapless::String::try_from(passphrase).map_err(|_| Error::Overflow)?, - ), - }) - .await?; + /// 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 + } - (&self.at_client) - .send_retry(&ExecWifiStationAction { - config_id: CONFIG_ID, - action: WifiStationAction::Activate, - }) - .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 + } - self.wait_for_join(ssid, Duration::from_secs(20)).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 } - pub async fn disconnect(&self) -> Result<(), Error> { + /// 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 => {} @@ -442,7 +582,7 @@ impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> Ok(()) } - async fn wait_for_join(&self, ssid: &str, timeout: Duration) -> Result<(), Error> { + 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 @@ -454,42 +594,81 @@ impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> 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), } } - pub async fn gpio_configure(&self, id: GPIOId, mode: GPIOMode) -> Result<(), Error> { + // /// 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; - (&self.at_client) - .send_retry(&ConfigureGPIO { id, mode }) - .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> { - self.state_ch.wait_for_initialized().await; - let value = if value { GPIOValue::High } else { GPIOValue::Low }; - (&self.at_client) - .send_retry(&WriteGPIO { id, value }) - .await?; + self.send_at(&WriteGPIO { id, value }).await?; Ok(()) } pub async fn gpio_get(&self, id: GPIOId) -> Result { - self.state_ch.wait_for_initialized().await; - - let ReadGPIOResponse { value, .. } = (&self.at_client).send_retry(&ReadGPIO { id }).await?; + 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( diff --git a/src/asynch/mod.rs b/src/asynch/mod.rs index 49ace50..afb3f0f 100644 --- a/src/asynch/mod.rs +++ b/src/asynch/mod.rs @@ -13,42 +13,8 @@ pub use resources::Resources; pub use runner::Runner; pub use state::LinkState; -use embedded_io_async::{BufRead, Error as _, ErrorKind, Read, Write}; - #[cfg(feature = "edm")] pub type UbloxUrc = crate::command::edm::urc::EdmEvent; #[cfg(not(feature = "edm"))] pub type UbloxUrc = crate::command::Urc; - -pub struct ReadWriteAdapter(pub R, pub W); - -impl embedded_io_async::ErrorType for ReadWriteAdapter { - type Error = ErrorKind; -} - -impl Read for ReadWriteAdapter { - async fn read(&mut self, buf: &mut [u8]) -> Result { - self.0.read(buf).await.map_err(|e| e.kind()) - } -} - -impl BufRead for ReadWriteAdapter { - async fn fill_buf(&mut self) -> Result<&[u8], Self::Error> { - self.0.fill_buf().await.map_err(|e| e.kind()) - } - - fn consume(&mut self, amt: usize) { - self.0.consume(amt) - } -} - -impl Write for ReadWriteAdapter { - async fn write(&mut self, buf: &[u8]) -> Result { - self.1.write(buf).await.map_err(|e| e.kind()) - } - - async fn flush(&mut self) -> Result<(), Self::Error> { - self.1.flush().await.map_err(|e| e.kind()) - } -} diff --git a/src/asynch/network.rs b/src/asynch/network.rs index 4e398fa..79532b0 100644 --- a/src/asynch/network.rs +++ b/src/asynch/network.rs @@ -7,20 +7,18 @@ use no_std_net::{Ipv4Addr, Ipv6Addr}; use crate::{ command::{ - general::SoftwareVersion, network::{ responses::NetworkStatusResponse, types::{InterfaceType, NetworkStatus, NetworkStatusParameter}, urc::{NetworkDown, NetworkUp}, GetNetworkStatus, }, - system::{types::EchoOn, RebootDCE, SetEcho, StoreCurrentConfig}, + system::{RebootDCE, StoreCurrentConfig}, wifi::{ - types::{DisconnectReason, PowerSaveMode, WifiConfig as WifiConfigParam}, + types::DisconnectReason, urc::{WifiLinkConnected, WifiLinkDisconnected}, - SetWifiConfig, }, - OnOff, Urc, + Urc, }, connection::WiFiState, error::Error, @@ -28,13 +26,13 @@ use crate::{ WifiConfig, }; -use super::{runner::URC_SUBSCRIBERS, state, LinkState, UbloxUrc}; +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>, + 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> @@ -46,7 +44,7 @@ where ch: &'b state::Runner<'a>, config: &'b mut C, at_client: A, - urc_channel: &'a UrcChannel, + urc_channel: &'a UrcChannel, ) -> Self { Self { ch, @@ -56,63 +54,7 @@ where } } - pub(crate) async fn init(&mut self) -> Result<(), Error> { - // Initialize a new ublox device to a known state (set RS232 settings) - debug!("Initializing module"); - // Hard reset module - self.reset().await?; - - self.at_client.send_retry(&SoftwareVersion).await?; - self.at_client - .send_retry(&SetEcho { on: EchoOn::Off }) - .await?; - self.at_client - .send_retry(&SetWifiConfig { - config_param: WifiConfigParam::DropNetworkOnLinkLoss(OnOff::On), - }) - .await?; - - // Disable all power savings for now - self.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 { - self.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 { - self.at_client - .send_retry(&crate::command::data_mode::SetPeerConfiguration { - parameter: crate::command::data_mode::types::PeerConfigParameter::TlsOutBuffer( - size, - ), - }) - .await?; - } - - self.ch.mark_initialized(); - - Ok(()) - } - pub async fn run(&mut self) -> Result<(), Error> { - if self.ch.link_state(None) == LinkState::Uninitialized { - self.init().await?; - } - - let mut link_was_up = false; - loop { match embassy_futures::select::select( self.urc_subscription.next_message_pure(), @@ -128,15 +70,11 @@ where self.handle_urc(event).await?; } - embassy_futures::select::Either::Second(_) => {} + _ => {} } - match self.ch.wifi_state(None) { - WiFiState::Inactive if self.ch.connection_down(None) && link_was_up => { - return Ok(()) - } - WiFiState::Connected if self.ch.is_connected(None) => link_was_up = true, - _ => {} + if self.ch.wifi_state(None) == WiFiState::Inactive && self.ch.connection_down(None) { + return Ok(()); } } } @@ -228,13 +166,37 @@ where return Err(Error::Network); }; - let ipv4_addr = core::str::from_utf8(ipv4.as_slice()) + let ipv4_up = 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)); + .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), + status: NetworkStatus::IPv6LinkLocalAddress(ipv6_link_local), .. } = self .at_client @@ -247,16 +209,21 @@ where return Err(Error::Network); }; - let ipv6_addr = core::str::from_utf8(ipv6.as_slice()) + let ipv6_link_local_up = core::str::from_utf8(ipv6_link_local.as_slice()) .ok() .and_then(|s| Ipv6Addr::from_str(s).ok()) - .and_then(|ip| (!ip.is_unspecified()).then_some(ip)); + .map(|ip| !ip.is_unspecified()) + .unwrap_or_default(); // Use `ipv4_addr` & `ipv6_addr` to determine link state self.ch.update_connection_with(|con| { - con.ipv4 = ipv4_addr; - con.ipv6 = ipv6_addr; - con.network_up = ipv4_addr.is_some() && ipv6_addr.is_some() + con.ipv6_link_local_up = ipv6_link_local_up; + con.ipv4_up = ipv4_up; + + #[cfg(feature = "ipv6")] + { + con.ipv6_up = ipv6_up + } }); Ok(()) @@ -272,9 +239,8 @@ where continue; }; - match event { - Urc::StartUp => return, - _ => {} + if let Urc::StartUp = event { + return; } } }; @@ -293,7 +259,9 @@ where self.at_client.send_retry(&RebootDCE).await?; } - self.wait_startup(Duration::from_secs(10)).await?; + self.ch.mark_uninitialized(); + + self.wait_startup(Duration::from_secs(5)).await?; #[cfg(feature = "edm")] self.enter_edm(Duration::from_secs(4)).await?; @@ -310,7 +278,9 @@ where self.at_client.send_retry(&RebootDCE).await?; - self.wait_startup(Duration::from_secs(10)).await?; + self.ch.mark_uninitialized(); + + self.wait_startup(Duration::from_secs(5)).await?; info!("Module started again"); #[cfg(feature = "edm")] diff --git a/src/asynch/resources.rs b/src/asynch/resources.rs index f115962..20db742 100644 --- a/src/asynch/resources.rs +++ b/src/asynch/resources.rs @@ -11,7 +11,7 @@ pub struct Resources { pub(crate) res_slot: ResponseSlot, pub(crate) req_slot: Channel, 1>, - pub(crate) urc_channel: UrcChannel, + pub(crate) urc_channel: UrcChannel, pub(crate) ingress_buf: [u8; INGRESS_BUF_SIZE], } diff --git a/src/asynch/runner.rs b/src/asynch/runner.rs index 8836b8a..5153b77 100644 --- a/src/asynch/runner.rs +++ b/src/asynch/runner.rs @@ -1,21 +1,31 @@ -use super::{ - control::Control, - network::NetDevice, - state::{self, LinkState}, - Resources, UbloxUrc, -}; +use super::{control::Control, network::NetDevice, state, Resources, UbloxUrc}; use crate::{ asynch::control::ProxyClient, - command::data_mode::{self, ChangeMode}, - WifiConfig, + 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, SimpleClient}, + 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, Read, Write}; +use embedded_io_async::{BufRead, Write}; #[cfg(feature = "ppp")] pub(crate) const URC_SUBSCRIBERS: usize = 2; @@ -30,8 +40,7 @@ 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>( - mut sink: impl Write, - source: impl Read, + transport: &mut impl Transport, req_slot: &Channel, 1>, ingress: &mut atat::Ingress< 'a, @@ -39,19 +48,21 @@ async fn at_bridge<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> UbloxUrc, INGRESS_BUF_SIZE, URC_CAPACITY, - URC_SUBSCRIBERS, + { URC_SUBSCRIBERS }, >, ) -> ! { ingress.clear(); + let (mut tx, rx) = transport.split_ref(); + let tx_fut = async { loop { let msg = req_slot.receive().await; - let _ = sink.write_all(&msg).await; + let _ = tx.write_all(&msg).await; } }; - embassy_futures::join::join(tx_fut, ingress.read_from(source)).await; + embassy_futures::join::join(tx_fut, ingress.read_from(rx)).await; unreachable!() } @@ -59,16 +70,16 @@ async fn at_bridge<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> /// Background runner for the Ublox Module. /// /// You must call `.run()` in a background task for the Ublox Module to operate. -pub struct Runner<'a, R, W, C, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> { - iface: (R, W), +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 urc_channel: &'a UrcChannel, pub ingress: - atat::Ingress<'a, Digester, UbloxUrc, INGRESS_BUF_SIZE, URC_CAPACITY, URC_SUBSCRIBERS>, + atat::Ingress<'a, Digester, UbloxUrc, INGRESS_BUF_SIZE, URC_CAPACITY, { URC_SUBSCRIBERS }>, pub res_slot: &'a atat::ResponseSlot, pub req_slot: &'a Channel, 1>, @@ -76,18 +87,17 @@ pub struct Runner<'a, R, W, C, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY ppp_runner: Option>, } -impl<'a, R, W, C, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> - Runner<'a, R, W, C, INGRESS_BUF_SIZE, URC_CAPACITY> +impl<'a, T, C, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> + Runner<'a, T, C, INGRESS_BUF_SIZE, URC_CAPACITY> where - R: BufRead + Read, - W: Write, + T: Transport + BufRead, C: WifiConfig<'a> + 'a, { pub fn new( - iface: (R, W), + transport: T, resources: &'a mut Resources, config: C, - ) -> Self { + ) -> (Self, Control<'a, INGRESS_BUF_SIZE, URC_CAPACITY>) { let ch_runner = state::Runner::new(&mut resources.ch); let ingress = atat::Ingress::new( @@ -97,28 +107,29 @@ where &resources.urc_channel, ); - Self { - iface, + let control = Control::new( + ch_runner.clone(), + &resources.urc_channel, + resources.req_slot.sender(), + &resources.res_slot, + ); - ch: ch_runner, - config, - urc_channel: &resources.urc_channel, + ( + Self { + transport, - ingress, - res_slot: &resources.res_slot, - req_slot: &resources.req_slot, + ch: ch_runner, + config, + urc_channel: &resources.urc_channel, - #[cfg(feature = "ppp")] - ppp_runner: None, - } - } + ingress, + res_slot: &resources.res_slot, + req_slot: &resources.req_slot, - pub fn control(&self) -> Control<'a, INGRESS_BUF_SIZE, URC_CAPACITY> { - Control::new( - self.ch.clone(), - &self.urc_channel, - self.req_slot.sender(), - &self.res_slot, + #[cfg(feature = "ppp")] + ppp_runner: None, + }, + control, ) } @@ -146,38 +157,201 @@ where } } - #[cfg(feature = "internal-network-stack")] - pub async fn run(&mut self) -> ! { - let device_fut = async { - loop { - let mut device = NetDevice::new( - &self.ch, - &mut self.config, - ProxyClient::new(self.req_slot.sender(), &self.res_slot), - 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); - if let Err(e) = device.init().await { - error!("WiFi init failed {:?}", e); - continue; - }; + 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?; + } - let _ = device.run().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>(()) }; - embassy_futures::join::join( - device_fut, - at_bridge( - &mut self.iface.1, - &mut self.iface.0, - &self.req_slot, - &mut self.ingress, - ), + match embassy_futures::select::select( + setup_fut, + at_bridge(&mut self.transport, self.req_slot, &mut self.ingress), ) - .await; + .await + { + Either::First(r) => r?, + Either::Second(_) => unreachable!(), + } + + self.ch.mark_initialized(); - unreachable!() + 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")] @@ -185,43 +359,33 @@ where &mut self, stack: &embassy_net::Stack, ) -> ! { - let at_config = atat::Config::default(); - 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 - - // Send "+++" to escape data mode, and enter command mode - warn!("Escaping to command mode!"); - Timer::after_secs(1).await; - self.iface.1.write_all(b"+++").await.ok(); - Timer::after_secs(1).await; - let _ = embassy_futures::select::select( - at_bridge( - &mut self.iface.1, - &mut self.iface.0, - &self.req_slot, - &mut self.ingress, - ), - self.ch.wait_for_link_state(LinkState::Up), + at_bridge(&mut self.transport, self.req_slot, &mut self.ingress), + self.ch.wait_connected(), ) .await; #[cfg(feature = "ppp")] let ppp_fut = async { - let mut iface = super::ReadWriteAdapter(&mut self.iface.0, &mut self.iface.1); - - self.ch.wait_for_link_state(LinkState::Up).await; + self.ch.wait_for_link_state(state::LinkState::Up).await; { let mut buf = [0u8; 8]; let mut at_client = SimpleClient::new( - &mut iface, + &mut self.transport, atat::AtDigester::::new(), &mut buf, - at_config, + C::AT_CONFIG, ); // Send AT command `ATO3` to enter PPP mode @@ -239,18 +403,18 @@ where // Drain the UART let _ = embassy_time::with_timeout(Duration::from_millis(500), async { loop { - iface.read(&mut buf).await.ok(); + self.transport.read(&mut buf).await.ok(); } }) .await; } info!("RUNNING PPP"); - let res = self + let _ = self .ppp_runner .as_mut() .unwrap() - .run(&mut iface, C::PPP_CONFIG, |ipv4| { + .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."); @@ -275,7 +439,7 @@ where }) .await; - info!("ppp failed: {:?}", res); + info!("ppp failed"); }; let at_fut = async { @@ -295,25 +459,23 @@ where ); socket.bind(AtUdpSocket::PPP_AT_PORT).unwrap(); - let at_socket = AtUdpSocket(socket); + let mut at_socket = AtUdpSocket(socket); - at_bridge(&at_socket, &at_socket, &self.req_slot, &mut self.ingress).await; + 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 at_client = ProxyClient::new(self.req_slot.sender(), &self.res_slot); - let mut device = - NetDevice::new(&self.ch, &mut self.config, &at_client, self.urc_channel); - - if let Err(e) = device.init().await { - error!("WiFi init failed {:?}", e); - return; - }; - - let _ = device.run().await; + 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"); }; diff --git a/src/asynch/state.rs b/src/asynch/state.rs index c01d3b6..4dc3784 100644 --- a/src/asynch/state.rs +++ b/src/asynch/state.rs @@ -30,6 +30,7 @@ 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(), @@ -42,6 +43,7 @@ impl State { /// State of the LinkState pub(crate) struct Shared { link_state: LinkState, + should_connect: bool, wifi_connection: WifiConnection, state_waker: WakerRegistration, connection_waker: WakerRegistration, @@ -67,6 +69,22 @@ impl<'d> Runner<'d> { }) } + 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; @@ -131,7 +149,7 @@ impl<'d> Runner<'d> { if let Some(cx) = cx { s.connection_waker.register(cx.waker()); } - s.wifi_connection.ipv4.is_none() && s.wifi_connection.ipv6.is_none() + !s.wifi_connection.ipv4_up && !s.wifi_connection.ipv6_link_local_up }) } @@ -155,10 +173,24 @@ impl<'d> Runner<'d> { if let Some(cx) = cx { s.connection_waker.register(cx.waker()); } - s.wifi_connection.is_connected() + 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(); diff --git a/src/asynch/ublox_stack/dns.rs b/src/asynch/ublox_stack/dns.rs index b3aed1e..00550a2 100644 --- a/src/asynch/ublox_stack/dns.rs +++ b/src/asynch/ublox_stack/dns.rs @@ -25,10 +25,10 @@ pub enum Error { /// 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"))] +#[cfg(not(feature = "nina-w1xx"))] pub const MAX_DOMAIN_NAME_LENGTH: usize = 64; -#[cfg(feature = "nina_w1xx")] +#[cfg(feature = "nina-w1xx")] pub const MAX_DOMAIN_NAME_LENGTH: usize = 128; pub struct DnsTableEntry { diff --git a/src/command/mod.rs b/src/command/mod.rs index e237b39..864dec2 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -21,7 +21,7 @@ use atat::atat_derive::{AtatCmd, AtatEnum, AtatResp, AtatUrc}; 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, PartialEq, Clone, AtatUrc)] diff --git a/src/command/system/mod.rs b/src/command/system/mod.rs index d3403a0..9ff5be2 100644 --- a/src/command/system/mod.rs +++ b/src/command/system/mod.rs @@ -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/src/command/system/responses.rs b/src/command/system/responses.rs index da05fd9..fcd5de8 100644 --- a/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/src/command/system/types.rs b/src/command/system/types.rs index b00e393..b00b2a7 100644 --- a/src/command/system/types.rs +++ b/src/command/system/types.rs @@ -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)] diff --git a/src/config.rs b/src/config.rs index 44fce23..5380f32 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,9 +1,16 @@ 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; @@ -17,3 +24,8 @@ pub trait WifiConfig<'a> { 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 index eab96f4..b5f7292 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -1,4 +1,4 @@ -use no_std_net::{Ipv4Addr, Ipv6Addr}; +use no_std_net::Ipv4Addr; use crate::network::{WifiMode, WifiNetwork}; @@ -12,11 +12,29 @@ pub enum WiFiState { 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 network_up: bool, - pub ipv4: Option, - pub ipv6: Option, + pub ipv6_link_local_up: bool, + pub ipv4_up: bool, + #[cfg(feature = "ipv6")] + pub ipv6_up: bool, pub network: Option, } @@ -24,19 +42,20 @@ impl WifiConnection { pub(crate) const fn new() -> Self { WifiConnection { wifi_state: WiFiState::Inactive, - network_up: false, + ipv6_link_local_up: false, network: None, - ipv4: None, - ipv6: None, + ipv4_up: false, + #[cfg(feature = "ipv6")] + ipv6_up: false, } } #[allow(dead_code)] pub fn is_station(&self) -> bool { - match self.network { - Some(ref n) => n.mode == WifiMode::Station, - _ => false, - } + self.network + .as_ref() + .map(|n| n.mode == WifiMode::Station) + .unwrap_or_default() } #[allow(dead_code)] @@ -44,7 +63,25 @@ impl WifiConnection { !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.network_up && self.wifi_state == WiFiState::Connected + self.is_config_up() && self.wifi_state == WiFiState::Connected } } diff --git a/src/error.rs b/src/error.rs index d655f44..fd38a1b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -43,6 +43,12 @@ 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 { diff --git a/src/lib.rs b/src/lib.rs index b8e84fa..283875a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,14 +5,14 @@ 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" + feature = "odin-w2xx", + feature = "nina-w1xx", + feature = "nina-b1xx", + feature = "anna-b1xx", + feature = "nina-b2xx", + feature = "nina-b3xx" )))] -compile_error!("No chip feature activated. You must activate exactly one of the following features: odin_w2xx, nina_w1xx, nina_b1xx, anna_b1xx, nina_b2xx, 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; @@ -28,4 +28,7 @@ pub use atat; pub mod command; pub mod error; -pub use config::WifiConfig; +pub use config::{Transport, WifiConfig}; + +use command::system::types::BaudRate; +pub const DEFAULT_BAUD_RATE: BaudRate = BaudRate::B115200; diff --git a/src/network.rs b/src/network.rs index eafa25d..fc0a593 100644 --- a/src/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;

U@VEykGGVzg#r6gqmv61oYK1>8LiioV$@JXev5EDfyQt8B38Jv*qh4f6jLy zWIDvSY3!@0 zSp#x&`mRgZxZQ_>cwS!jz;jK`ZvUFUV|D0{PJT|At&SwwnVJS6r_&BV6tHfT! z*PjMI5VDa;cwReQT771?cyl!ghva(1mB>;y0KDTOk;+Zhys8@zlFa2;n~?`;45bx` zg|2g~zU4{N$uG0&a_+z?u)$a&HOaWnPyv0}Gs_RI_HF}V-gwW(wX<6wTUhgcnVP@) z__L#eT`RV1BF?s7MjUAF?`+ z*~!VRneBpH6;95fTtsH!GstizCnt^>GlnV?ggponTkw+TfMgFhw`5|1FkMvD45RIQ zs8Z#$Uw+B!(Yc%8m5Ie2dV77BWISTT&cd&F?v1~9$Z=9+;``@mYwA0EMer5wXC54M z_P{CXNb&O>aEuc@pFu^tU=;x%V~|gEW8*l_$==2mJ1ng%yY*bJQuS}3tsJ}R?1Z^+ zmkc;9C!)irJ_3V!fRjglH3I* zm^F>AD4qEQs!@`+=+vOWGx4hqoM~aGm<&IgUyQP6lz7{R?uypqXjF8Qj^^CNp5ON;Rba z87l3qbKkXtx=N<^J$U@M*I02NZ2Z3X{wLW9H}&$J6)#_2;&nSAd8S86w|NCoff9a4(muKog6?G8 zK@fAr8@#iQNqpSLKUmen{rtKwm{VUnJrOlbL&_Y!7jco4{Q{oAyP_*QN@`y1^ra3? zlwk9;u9+nJ;9QmXqOTq_>f=|}R(e6X!;T=D18@S`-!Uj5+FHJ!V)~NX{H>(o?17YU z*N{%de)VI#4`klCi_f|FXR;@>o3Nph$$r8RJ>Atl;H_?n6wYuaBlR*^Wq9e(B2EyX)vSg*-;zo>#?$GTS3GO`Sq? z?4zDNyv~>h1S<>z--qpM-ghfe;d*>u3q+WsuW#3+d{rE&8Yv>q%9YVaF5~@(YdV!8 z4Z19*=acJIufAi}megl0@>Izl?1kVN?0M)ZwwLRN9d49j8LFe>e$z}(Z^Pw>-(r>O zT@6|sQ;D7%{mC#tKS!QxM&$}4wpCkf7M#3ZHlRR3I#Afa8n&E1t(#^Kcl)8$It^>KV& zII)k1+pm+dPW9Ix3JzTp@SE{Eo_~HLZpyEZ^(vxOcRrH*{eb*l+t-IN;n25I8?CnK3(tbIo3Ei2WhwSxkjy9Z=hhF8j+WmXG_g1 z_9F)m#&ArV3_QkL?t6Pzztar0qP4;puo96{hl=KnRhoDlvjKCZ+@s|Z3B-d3n}F}U z;-^sCHEyv#Iywliy)FB%&q&^ReKabBiiFlYN=Sr()9A}kkcf;e;a1k`4N*}&K$Dm{ z$OXPPqg??!%%w@1IrCV|rZ0CPW$S%Ex>#4aGtlc18D#0QWon@h=zrWvru`2NTa;a7 zatE1OpcR7YRDzjt^E2VBj~!{Y+| z(-@?^)Ls(GQc}7qP@A#s{ygZVpaSfIR8-J4MkZiL;(nf&$BxqOw64e;MdvNSfzi@9jpJ!F(*=vZXGqL&ealH=OTRa6@~WH-!$6)YP^^2bl~7 zP*M;}@~ENv_!Om7YHqoj0b}h-mDtAgk%DX*qLc0u3+TQ-P~UHe>cSK^8+O&E?>mR~ zk2ESC{71)b3I0r^Q#((vYpy zVJsho2B1ICdI=|3AgV=+JoCSwYsr8&KHMfHNgx*oP?|WO zId0SBud90HiKFJ1w+}8IzS=qQ3-arIsaLPAX0B%%gMWffF@_S9tFa4cob`jr47YZL z(h6%aHTG)seY8Qy)7a0`*wT_!=F4eu-TU{y%nH#(kl`ey_EjS(ZXKsjpMLUfZ4|X^ zyednaH`2eND}5T7W(rDA*3?DeU5kT};#0k)GE@U+EIFUX4;>HLF#nN{h;&I6?IQ{X z2ErO}u(-aEY&DtuRaDdg&e<{rVoCI5^Mc9a#*j zO@RQrw_qv2nTf^syKAbOkGcdLxSUSM4Pz#cAyJZTFU`phU0044-7z)=nS4d zwWpUAPfLYt%S0eTZKr2!e3=sR9{e9M}usmN^BKa`w|jMke3SfYfl_N1n`pk_mI4oAR}X>4(wKBJ4h_Yq>ALbLv#*$Sw=+ z=*jA+lcU@YA3B88&Ya66xCUsH;{fImZ1YS#iF5+W4tOEcS3$2x1W3cf-@fY`ShGoFa;{6p}uXZfbWveaDSqkEiJW1B7?zOAffOa%J*hvbh zq!$d}_FVy}1)6o8G3 zaJJdsLeFx{OWybFSFsW4^IojaClcy*vB~uG(+K9+-#{ivqafCxLR>(Cq|X}hO64nt z^K<5ix+hQHaPThIs1&gMv@2H_ZE4EjruzMcV(K5~wciBBLiFb2|Fx3rMh5Lu@r%*+jbPlLX? zPl~P6ZfdueYB%Am(M4ZxZyd_7KGzqJ)AG*D?D)Rj@A-=uld#E)7np-SEbl0#3x82* z*;!Alk;46;GJmicir(F-i>V7klYo_(Q;$6lyb@{a(5|7!M!Y&1WWIj76zUP?!>Y(d zJ(%pz9n~}l>;YnLRk7o#`&A7qDL?HUg{XGwh2LYc;Ksa>W=k2i`XCVc3KTm4CFM^H~rY$l9Uw zvIwO}S#cm(4EoQ1g=(9Ci%rjyqY8xZhbE@axv_Cbds)%^3^k?dqh*9l_@`U%SKJR4 z#8n0(!@dyh7T6c~E-j22Xc}FRJd$HVlUs`yN!Sjr7J916cmYC5`SdG*%VSsSiP!^a z#CFacxL2I3)}XCO;QSUQ<8yf)Vr?}rA+Y&Vw#KbhB`=H|YkC06m%>d&JI2u#01dER z%;+h^a6@>`=c5g+M(5M!$6$;=#jB1)57sI|2Qd>hFdERTWRu%=8cigq2kvLP8#iBI zWT^a#HJ<1JsFEy5dQ=@Gq+N9s;_=FKt1O;i1X;`g1%>#Kud!=g!CseoHZ)e(FjbW` zQ$WN_cZrb`$r3XLClycybQkF77CJdur$ipW;uHcU+p z1CXWw1~T=W_`cuN8q+`+Wicu{AU-mT2@Q}6`P0qay&9)e2CO+I0>M%9nUrXTZQZ(x z1WS4SoFOYbw<6G|THk3yavvPb3fXG#72>FYKRJhf3H0*g&t=6q-MvhsCw1!CNl)=A z=l1n;*44H02bSR}j3_}Y__&?tA3YkZt(<g@}*wJjRe($aAG}Spg@L!>*EU5HNc|RatmKGg8)x9f2=QVHl zYp-VvfP$oXH+4V1Z0yf|*JG!fO0JZj&*B2$Lz!JB(&!OsWagaA#8||wQ@T@FvYzzw z^MmwfPz%xjnwx}<; z?D6Br*lq^#^A93^h<#?nYr{>+%JXgxnL75`qO%&iI0w}riDGr;rp>cFdvOn?A@M{Y zIqD1 zZ0QhzxOziL5-(wWc(^0SRxDY#;~?u}G}XCVrlzK2A)ycOb;m_EM+RyT6v_kQtCnmuLFJCBC0 z+i34LE(pZe27|?`*RJTzF}6d>dgEMAB5bSO+k;gQP}G7Kv$A>>G}DOHP_sN6KAL(c zyqav*g#6fA_Tyz2N^nMNdlQ830z5@sMe<#bITgg17%wda;UMh21oJ1#seNE<^y9;6 zSJ!Ua)|+t_hA3oK*s_Web#-L}qgKU5MUvxK2O&~_jO2b=O>FA^D6_HNwHdxG=&kGm zI0$nn&0cpp(73rCWcw4Oz}a*56Gpw->S1G@9(P$Et=Auy2DM$dcOP&aBo&7_D1Sn% zNu*BhOO0RGMz)6*ZoKjR3K(tr!wL8X^eND{8+umP$3eB!!X6}J+0G%^n<-r3Sz`GB za@b*>_MX-VGtxpsT+cgLJObp|zcYC9_U#o&tbXra@;+CQ84-s&FfJI@!AKU0GL=hu zf%C)Y)_^JjA;f@l^E=V4NxTE9g4XED(A3n{34eiZ-9hpUk8g;Z_`*Odzx@He&roc& z(ti-*S-jxl?!NZ1#-_q@_-3KD`NlSrd^j+SKG7{&AFZrtKl6%|{ z_nGs`Jn!?N)z!7YGGtpTt7D?<{B#&Sa(n$OpZ1KltO0x|y-yKmRy?w_m99(UE2#Ir zL0SaMRrtb+vN!Tka#Q`@q3#BT2HST5n>AVcCvatVT=k>}j>5rHJ3OSgl+Mw=_Nbwq z&Gcp;frrhjaiigHE3R3kG|nV#8q_F1Y0|j-0==RO=l?AB;vFub>f*-97zpw~KzNUL zEr(y!U2x3Hqq(-yO6mpgGQ$n_W3{w?;eI!ok6Sf<;83T(p`8Eu(*8mK{)-s?1*243HY32*AieO0{LiVs@l^HTjSasi{S*HCe}9$-)hjDUtV>(dx2N5wQ)y=F z_oZz%zUfq9+Jj>$M?7N7G6 zgRFXg_W#E-8tD0b8;{BCpCIiz-nL!Zh|BTq`B<4f{`eFB_&9%FfDH+sy40-CtLMSx z_sdhM`2KzWg~9&yMEm{mSp?4A&IcLZ{#t9pf5V&!b+kGztE#`9E*7x*Uk`Vhwh|DX zJg8vCRYKy}alfx`4PA@qF0ms~WF&*83c_k6g^EL~E4faU{{Uj0zWH5QJHcAFhajz& z>S)k{m|=r8Xu^cqH=v}^#Ce&3fgPshPzaDpY2X*PYk;7+e<8L11_^i7(ULKJdaGQB z{pr(>=xdS7EE_5jkOgBEff3&@SpJUlzUl=*?=Tw=?_z;r$n)nT6x61IV=%ZbH3$(w zfjgXlJd91#RO^Fs0cNQ6rzXY^IU2-}e=AmuhyfMMSPw;G4HSse##NkJ_Z|s^sKuT8 z>GS6z=|YV#6wz(V@pttwBNLlv8b|gzY*mT6FENW!T44z z$m#0RC}Rs1fywFj&cBZ1ISWIrI&CM*`a5y(pk**i)l_CAsgxR=s8qV8joS9UjJ z`IVcSi>zQIZ7CjG^@7B-1Ro2aSy2Rulof`wUn1g)2CZAyfM$R|)yB#-R3n&}EIK?)nQ;Jin=YsuyYb~-wad-VO& ztKY$(%D-yDp+G0OEMrfV^(d=x@sInm$ZySi*WZDc`Zu7;lrddUoA67>>zY;9B8vMgEnUGeCZuRXg zKAn5-#NQr^t|YeA`{m8gCgZhJ_fq z46;oo`}zEyCb*Ixe&|wx_nA-0kK$%Hry3#hVVomLBe8bxqp1Fn8dABXCxKmx*cy$@HdHV)}VxYH>4$eCRY5JfS z#}ndedOqs1b+##8OrmGEXZtl9f2!GXw4SI56tgk5IEiZV=#d`AU0k%3u$wGCj>|4& z_%@QhN{s9Fb{WLG6syJbXa8>sB7pL)v0l;hw5pSb?lD_vC%4|*?zYMoC6;$i1ftR= zBuX}xv}*_rVE%M6`AV0@kHo1g4?*gb8#H2H}6AmX=RIqMd!=k zvy37c)Ju}RevM7JQB`FOgTVxT1jB}_v5B|rkUXSPpM2}lQ)r98U1K}S9(r*4yTkV$ zvFQXD@n6ga{1E&_q(8V@V(hQ+$!G#D_+4lfWaCJi$Fmn;`(PccLJbQjoBizv+&HwX zgRgINADc$4S{V{#5WZesTCE{}eFtK5?&R1rGZ{EY?<`~p>W!|n5yZbyK*?fA!s?+7 zjOBem;IEPD=nJhN%3q{gjL4entx>n_z4hczMZ^N9`U$=MIFJlqUqZkytvLop)E?5# ze8^Ef0a*T>{{Yyy1@+eB+0QDYAG-pC435b>?0i$eE4^0vy#e>UQ8!+#+ZR$p7MX$( z^eK4=1BN^e)GpvnxY4fP-GT*j5S<@eg)mpD%b){5V>iwl3|*b})0YA(U~v#XuSbE^ z{i(1Y{B5B79W>RH&umeHC}t*)!B}?`-x1BlScC(>$M6G%OFwj@p^{fy&V9`K$*)>W z$3pTDI2h+78{cFFf4bz&;j1zPp`g**4dNk=^hAcIuU{u9)lD^rZEZR2yIM1<5>E@E zU3b;eQhmHuvhK7lK$|+_m#XjUf&)v7wn3o_mFRrVa5C*XI@d|nKNH7 z;a1*?6l*I8zDN@SSL|(Ul>xak>+Sv6I<6oXcEAFF=rSP7!cqlrozPf$txl<{4c2U` zI5q42zK5-MEM)i=C`t+0)=1yK?yhx6rA~u zwcETBSNcPgaj+vF!4b!83807j5%Lk26L}UrE{U>ZMyc z2b>BXIHH}dkB5!5btO&+$V)<<5#okM#DfJ=GpMLexLRDektQjCZz`6C=gNLg+I!W6 zVJ9qi5*~V2T0bIAh0s^e&=8kl3kKP72wjNj>Wh7!nswrRz*pm==)A97`=y#ZNOkg35XHN_0(W! zbf;3?D%`opHZ_v$ekjA>NDCM4`<~~Q_CGBzMV0ConmIcHYuiK*8$!6`aY^b?T{Zx0Ye5=uFVi}%S7_85{rP2f zJ^j_L^j_?)ROdcjLZC{bR7)LI^p=aCnW;3Xs}*Lj^P1;1^(RN~`1IYky76@uIP}=o zWMPbYuw*uP8X3zMbQ8x5&Ww4xb~Pzov08Bk0wJZMV^vmP6I^-I>IKo@$cWQYiYrn zh`Sj!LAZ(OzzKN=2N^8&Ka|OU&WaH>^$I7AX-m)HB7M@Snn`h{$rNBzd3_ew61zQa zLgS2LDuVJPC57+am~?SYBdi+^bQxL*Z!88bRJx4K3R{3}37D-S_4ulfpFe}ftb$aD zEi16TU&#KZ>uBbIk|S~K7&>&Qv#Fkm$-Y7A&)&VuVx-t@S10bdfUh+BNl8h@EWdJ_ zH|BRZ^H3&nL<0(83pL~vKiBQL%9P@gs*=LGI;&r<@|KUPOI0u;>OocAc1sPmzviRltNm;aoprXS2Y$DLx2(OSgdj92WKQK|LN z(kAu6|1(B%t;gn%409nM`16E^n+Ie~rfA##n*VvRGvVjeodSXUQ?B!vRElW(c0%DK zSNWZzn*8ju^xV3$2LF)Zq}&ZNF*n_zq5Jpm<%j-)7Wkm)P8Du?%?S%yQN>1= z=T70naaJ{o(zQdX>oqQLgGv=yy|I;LlMq^A{iuQG_T=Z(9{P|h1v%4Y#PpzGV=FzD z7!HFIr!ig=f2z!c7AbStU--q&#X&Z|LCr%!*#B11cxinNwfee_{}%S}vyJm!z7&+2 z)ttJI43=|Q%D-vMCLZgrPb-icSQ{z4{ICIj7T*o&nL_7^_L2je`YjbG6nnV@!M15UYFJn4zkhaH0}(nG`zgu`^_%$!7;;K zu~t8O%MYgL7Y{QSmHo%A+BO2C4sZ7P>z(pn(h{Ow78z7x|XK*|+;@k(XZJ(zCuS$@9Q*+X@xnIfcx!=!E%ds=Ax zqc;{aaRev0h~;}=>+~o~5C(~tEfBdFsZiP=3PD{xn}x}2jayjwjpHOM$VlWg&@)iI2ZzW@LzG z2jd)Q76e*@p{XcCEGtYpZR8IE6Uz8AXeJta%)v1QcieIP0R0S@D8k2RS_JWuVFXkB z`GM)wzP{{ImKJTlKt?#28-n0r-@PphyksFRKS-cFfLW%<6mP4kBdCyh3rb4(asebb z5dUf7SLq!s#8|MmxXq8?1gitFV? zXhEw_TmR4>tVVwj=B|l^GW}jakXR8>eAD8?pzS*}5pwd!G!s{54Ryd0uG9j%Y3fElNvkKVg3(f0N~K5c+tu6UK?6AWdrS4H;<;j#)3FTtOgdsglQ5y87 zQatKY@a);|WbRAI*ZQBHxwlZJV5}}JgH-WlqmlP}e-M&W(_d|$b$aQc73FAcF5Wz= z@7Q(1W#dTJ(jN9YMHa!y&*&v?pC0tN?z52-yCkn1W47Srgn>i7&Gc;M!&#)Hrzg1D zpAdSuOIcBuIYTz#Nkg_Dw{tLH!yW&iky+D=&ykB{xs_l3T*sOX^xCp42W`Fp+1LT| zW=_s1*pT(*Ay&WCS?SL}e^ngy?nwg}UV^#^1b1f>Yi*^1 z#C?K=?}ykLJ5E+zjLNFREd0^zQBcvzff~V}N`!^j+n@O?EO<|2g6l}>=l5)6YS)XS zZqYpHLr8W3J+bUhoiVYxduPPoaf$uA!5`RleS&llSq#g&FR&X%3~w!)G*N~wx$y5^ zSFADUJB3NdH&7rYh{ucUou;A^w8-8FyzDRP8ggPsAqbzr%$a zxdcdMP*z!po~_TI!R*DXs16IGTep3fNE#=!pZE2^)Sku$_3lP=T(xqgIhsL+JjAU- znsEv-D!^2Pb#k5duMfC%f{J4@_fDYRkGPaJ;;8vCChc8Bt)33DQG9kLJS}?6o!$>{ zP_(qPG;Q1V?)b#)f}%U6$46eNMJ6WhixmuB^(XthAGK5!jZg*06zr}X@hE{@qn{l7 za&BkfNtUe4|N68d*c_|OW!bC(zp7AJeweDOjEvFf+M?Z<`$pwUHI@XOCz{OS*PciK z>B?Vn9fdOn8(6n-qcd_eP#*#Em{M``@Q{WI&~7Ir2bv?X77(BYH6wG9c=;@o0Qcp6 zZ(fi>mZUMVbj$lCkHQY2P5Ssb!PN{5Z+SJ@Zl-5xWwo7xja(uO8pJf_PzE(>@H<1H zR8cJokHV;m)s{0zN9Psa_M#D^wLXt9PG|ILy)_{Q8_pwh+c(V0+;L#%?{8Y50Fa&* z0gQsS&WPzEFqzA^V$tQ|%H4m`l*n}|UlG8)xmUDy)aQDJTFj|cX9FP)PcVx>0wSto z!w`06Ksf1VhpX>gth#dW$b#BK2gxN9J8d?Mi9(3qqK1S}C5<2;lH0t+Ef78OION+S zkU(>IiPVTBPX=QV^5W)NPXTqtEp^uZ;o#tK@5Gdb00BwgUMCX!#jSJi>mr`wJhGX* z0Ky-u>{T=YAe3mDdbS%r{+;u;oFNN>)}p180n`Myy zw2a;sX)=khxvkH^VVYC|>9V~BusHk@eBmg>bgLgW>nd8?*VZ-M^|fSqCBD%<{#o$& z;VK#M^LB8Mk!Ao!c{50^WV9>YXwtG9bRIho=z?iYFTb1}e}1}|UT-}((BP7ky3D}} zdRDj*k%u&bXHtjqOeGQb?o=H1_{fP1JJf@55ow%3ZIxQ~GpDTaYNmZgF&5Y8`zq}< zs^}N}Z=CPxSWqI|9R&2z8}CJslsCuS-Bw@A_&8S^c|RTk6(sJRckkL&9ez$hG~=Cl zPX+~+jdrDBQXfmpB@;KfxKxJ~35X3M+#;UG7C}K(4V3vif z#}V$7x5-ly1#BjaJC_Bj!-aoSLPg4G+wD`WHlIWzhqpzntDc+PQK)*calonqEq8(- z5`A|;y<4wGKEV*mp6t)d(g&WN*_0wxHO2Be`;_`A z&Kp2$5$BO$x5$##EtS~!3Wl4OQ6}os_ckTc^;wf*M_GK$(>3<_y5JkD(L_ntG>xw9;#fRgt+;*Cj3Y1d7F0y5*rB~YYRYy#?__)##VY#4N_LXLf6 zaxuBEChcCWl@+*Pw93J`Icae7a^NH^!-8^g&ogRN@_c>oyGi~nxRre$??#78r7#sE zOE4X0o&AR6=Sm zK5IOTEbk9)#{wn-2FmcKP}?K|xkSwWTx9r+kg|5&x~^m+?5%wvuT&r7LO83$AwfdJ zzSM3z*%W2GINXaZU*Q1n2U*K)>p4)W$i-YkEBCHkHu%&jk#ea8E(Z2Z2sS* zP0(xTcB4#3M~8pWq`I!h=XJpw$8L*D;v&e*Z{CUC9$Wv4Vf;W&t6615Q^2jcYhd6inTW{p#(!M*D9>s~nyF+G59Jf<8c zW)##Q18_f=;A%01{B*-n6OS5`k|l4ccO=d5TZB2gzGF$2$`@bMj$a;56YpFR6M_a& zm;3#CzYglbuh%1T1IlFlfBj=WLaS%iL>Y?|b9CAjN(kyqk@ATkaK?l&+5s7^i@56(I49nto*;M4>Qef#Djb|t z9bDe@SPgSIlf+1>SvflCKXA8^@pK-RBA=xo)Q(=gTGMtdU_{08>JXZMsrPaiikU?t z5#OYMKO!njy*8FdknpwHWRxF@$&BUvQN4Qx2 z4_!SG8XJPnl-`XXYv2&YFP0h$@K`LS(#Lhdz<^QFaP_H-5d4l+$VtD4;!@svyfMv) z?CF4SoJuzweInWyVA8G5r+fGBTYb-vPyLj&Z$J?97^_;i;-`^I5kcez8wpL5o4NCSzX``%3mXh5Nt@g;+3Ne&OiY%#Y zMIuWvWy@G2vX7{2ZQuKOWahli^SZA4@w@N)kNbK&&hwgcX4Lol`Mj6ocx?w5%#TL} z)oUo#c&twEyq@4r;yf_R{RMC&-)k*z@xJMwz8}NVl`WQ6tn%x&v_Z&vcbP=KtW4Ns zScKy2$Xo9WJvYl`oq3qe8HZ6##{{k6`dW(ZcP z1|WP$08zDtzb;@7Io+aAqWhbgJV{@E%CT1m^k=kQk?@Yu-&0q0_;$87dS~a4 zOZpRU5j*@HZHmnDMG*#9_DWhUB${Mwa2XWw9B9-^Fm=gDR+1cMLbLED>Z~vRU8cx# zw5?me-f_)XEAPm7FsXIf7GJApeE6%$EAcX|UaAe``L{{*-U|vLL8h>5yq~z8kWMzF zga48(h+D__J>?EL=4`?sg3Iu{{15$_gmo$=*3T9An{c1)+)NwX6J4KrvYgUNvdt8g zB{^OGVQ=}%Qc4^KFG%eQB*hG)#{6+0o@^MZkiwjBK)qpvUd+66aN~mdFQ@`OgCWj` ze6-xr_3ilSwu1RCV0kgAApDF@jGblwc$4<+U!JI#lcFPvvU@+pPh*Bu#9FkOxx1eD zRz>$z<0Z8+^uE%>48yle&HqNYIqYAerrXL@hs4OYtwx9RADXcL{72YC{|y)6(;QIN z9kWn?+`9bnkszg*)C|{0qGm_25joBBn)9842h?OVq9?rXnZ|BT(Av&F0XMc=C4lIN z1~q!HIq0HeDA?$Z>oA@JbzfpX=>LP1Ocaf=H{J^o%a{PRKjjs{1b?93m=oNiYak%1 zWcdg#eXnfESClcr^gx|~W}O_0)WI`pN(hj1M;n1{e@f=8+#!W!2skxPHZ^rWq>)1H z%BpJnl3n=v<>1)h#QSTT9}bisS2u8=MNW=36)*}R#K|END( zKSB6#F@$eDM5`p3Ts}{E0;BOn{s_T@*WqJa~v)bj7WmpYu!@Ud)3hEKiA|> zXZ}3~fBFj1;PVuY4IO`{(#O!OA~j&wV@Wh11d78SXRM5hiE`dHYJH6kZ(l`$(7)+9 z4W10}cwmRxyZ`c3N5Ai@n@beHSlhBvoWrj}`f8 z(9)j}D9K0}FQ)!AMvX!73V_pBJc&4dE+RxZ?f)Yw#C>jM%>kkLKq+ca%ICL_P96K> z(IF6d&3JgIeclp)udT1&Nan&w9HNAGw(Z>csQZ9+UAi1|e|l95!`XxL0lz3F`+u&5 zKFxC822D8TCwA$(zR`z-8(yR7>f5~Ygs$^8gsnYOl_5v!)a|V`WN;^ME8^U2D#RMF zZNRXd%?Aa}zzSRk%{jXWE&s$b7lSKDHD2Rf)KV2o`u{591G$=t&ZN6fei&k`#w}Wm z@bECi{c(VHgEXFb-#>ZCO6LTYp)ermQe{GQ%M>2IR(S5ar|A{#Hnd1*hzEU4Js=ds zGlK~%@bK_}J=q&n?Kk@O-YVAoSit7Nv5s&YNci;>7~5WOeI2Bz=BkyLxZjKZ2kR1% z>q02LTapkV%(|4~t}L*jKy;irG-&@Aj-?sn|G0=%ASYL%%jM1Kl%KL3hS$31oX6`> zZg9fMeFILxlK|!Rth;CdPnJ9wzQueJ`lyDFKE|c2%G{|fujH35%l}nqfnRY!S zgFXnw$kH=8EX1?AgA5!TZ>6`_eRKajJOSEo7hCGAa^PpVE-96bTBGTQ&m zt{?_id94!M1ss9xhR7ol7nr(g%!IS$1uvH2Ifetf2))Gs1g~XZAIym0bx1@jdHl1W zYa**obC8Hqmbd6kA$=$;;p+wOy6Rlghb!mz4XRGF3_lyBtoG+eWiR~4y}Z7bcr#rZ zCLu`PP;XBk88a<-RdC_*wvQ;B_v}eDpUd1(-o{JG5MgoR}fgl=f!WI@Q!-ri%@?NxMqd!kM*`{E7xnb`0A{N&3vqyNzYyr@l4(H1`f zvfc1AxusMyT<3l-%Xc5%RULEhOZ7j5ogReUUYv4Fq3}b_84;-DDlw}Hq~)~TT#Ey} zHb#u$oGB7tQ%Pc5QHd^1vKUpJ?E7a?s^~v>icXr(8!;gEnAMjL54O?#4m)EWw~BKG z=EG613y>Ld=RH8}w=wZ0zFxL`c`35JcTWly2aKkf?z7% z)28pLmC9*_5i9168{(y--My(plQG?mM|ID;oH!xd>_ndBDZ7yi-@cDKeIs^)`@P3I zF6TVHdBfaki`M*+P9yGKZZO7ryILK+3s&DPb{O^3Ncw&C{iyiHx!EyPmWL~?nXcG|5tbX0%sY1;?dH1I8rNMTAPwM`QJkh=pFuPGa{~?$T}t%n z`itq7BGivhd&u-H_5o?e8@QSPoG)>9z;sCj5~ZY%?;YY0XkH!p0I~0TH7$6pBobF8 zi-U2~R1Va(ojQ%0(T8mBjHge}J3rw-@k8F}hWpC3jgXs^5*2yh<|dTsXpqqG8GJip z*W?8`8CAzdKKym~a18__?#iR0e5Z-!>AVgp;&6WP{{2WC`N^sp6L}2D*nfyD!hP0m zNMZ2DsCM6&6gjh&PD*6_o+-MH_O+cVX0M)a5o>LIe|4jYrNxaz#`S}4I>HM|kD{}m z!(w&Sq9>c+8WYR3PFrc5Ru+$X`-@D9#lR0%Df2SW*E{^tMn4OPbJ1SqJ3%9NoSov_ zmLt8zjKm{fyte_CyI8CV#&KR!UahYopyZ@g)KVG#0=|BqR z|MP``m#XweGmlc?Kz_+jsr?y>qY|}h&Ud-V2p&Jd?!{pc@uf%02EcL?PR~#v#9pv9ZI^n9-;!u<|A@v)|Qy#QTJWzWa~; z6gBWa{q}6$oB=2>vVrMR<$FL`?~PrU`f>#_`#aw;<)N#)d&?;QlDZC*OauIP!l-$Y zN6BT8ePphq<5t%nXCH0@>R?@2&%b{I@XWccHXRCcA9s}W5;vwKwHX?!kk_49!jq5K z+qlugf*jL0*pUh8%Upi!2Gfj5uH6)Lep%=s7$IKjp8X-a)eLCg4 zd4mH!g*GH)lSOD3%3QWtSf7YjCYHuOSC0AQU(Y!8q3kmH&56aj8%wu8TzkV15GQzL#bQ3)O)H z5#;%UImAs&O$#wo%S_sruV2dvffT8?UrGDuU*KDWB10kf5)lLv5>)326Cx%iO{)tB zaDUlkG~q5^%W8QZIA;BfN8g=uo{Ce13=p*Rpq;vD!I!8};o*AQ3ebRoJC_9|SRZ(V zm?*fl2~<3s{Jx@&4#luJ@Mou!=arfmjN>395UqSK4pIX93BKa`apISfk{GTX^m_{k zT@wreP!4iBgLDBv-!arP0Qab}4ZC!)e_cJvwGa2jORjdw#N)%|0ef}OY;8GhUB2%S z1=U;ZoL>$lWPx(apG!VuGw zx&4L@`MMuKQ4A8V%3UT}1w3H*Z~TUM#4nsKb%=P<)Gfw@i!O=TNMrIxjv56YuMK{~ z=RH_f{g$+R83oKCLJ1~0fm9&hc{UP^AO$5!8*1a$u36(q_X45Fq=^O`0?{4;URDt zjt&kAnN-S8E#zwVs;$Zi_u|A=JweJljqT4FW&4h-tz4O+aO~JIOKpEzcyVH9XV>G) z4E?QjeC1~OGMq95%}{?I&}_YwVw6TheEN7Q-BGl$b+T0??44LMz{XoCK4Uv{xUlH0 zHjX>@;+*^U$IkjQWz1sznK=)uQA7&34f-We!l8TbEY(7`9;VmoEYlrm5Q`e`=;#3-2?7-Rs3^&1Zx{6EhvE~vI@^tt{FlwCp(AoU(kFyf*KCH!|n35XB5OxOT{ zV`guQ(Vh|&lZl;J%m?4fcBEW*Z)!v#OmuW1+AxIqDRV9oeXubqCFD{;#|kezygo8;bKAos+c0-w6JD@x?LhW5QX+|8>6 zp-d%e^MO%>SIF&gyGuZA)q~kNIq&zK>garmk`RGeL|i@5cXQmlNg2K7K+n{iU2UTh z^NgGz(!GZ+7Jc3QNy{66$6{ACK?e?!w$y-BMG2n90M-Z*iMZ0R-^mw8yMg>J zzvYqsc-$b=PU5Uje45C{oBc33lp`Dwo*2BBemwI!yojYf0H`DTJ){D$Gxalw57Ff< zslD%s4S*y1lH>cuAO0NkzULRP3yk%3t)Y0rLkf=Z{t^=tOXf~?b-Qx;fLaLEzGyiT z$>wHI=%PhsYH`573Aepk+8S)$x;5r;lL0gHqTLHtpY2&&m3u1mkJLb=f!(HTEmf6Z z8mP}uo?8h?pvx?q*k*yp;4d*!06!2M( z!YQyZA`;0K$B)mEw4`v&+pZ-A50CIY@A-1qEb9|r8HY@P{4z=K`*Jzt;U9M&VhOQn zN;=QF^;@i?%LAn2c=5dWK0BY&G>hF!GQ$x|CA(ej4DJAE?fs9pH^@4*O3x~Af=_fA zU06}*e9Bf@&=GDIrXA^Q9C-H&rzM$+Gu4XK&k&62*>k zIi%3YHsI66I&5PJ>v#rN4a$zI@*-RIOpG$OJfiZ`g`jlqceMYx@I2vhZ8`Y)2$y+x zel*A7zLL&+rbiDWqdF8?-CK@I2|U_kVDU;q8k^L76?N5nfYJNx1#3hvayJZ6LX4tR ziVTaHj_Xax4r9GT=)fn9lXYnSVsX(_$n~S5`!)Jp?L`Z_bx50 z%SXDx^Zry?DjF|(OOy1IRDQHp{VG13jD07TSX%i{8DGR9#!*R^zn}h+t6nwB{-bH# zP6`msh+)l0LZ8*Y;p3f=WxA?1;UHgkhu-ddc$0ssO_bDDQ~586UmTps-aoX_E^%VZ zf68Tv(TFb zYO_u0BE3)d%#tRU$LF1PgrZlnxfCpYiEIOdoC-gPkGJ1~79DbTcizOII*%kc>YyAd zTD>!No&dVCHu()MeYG}QpkC-sMO;T~FJ>Q>FbRJvaW)87WPpkQoW2zxI`|S=nBxm# zWB@l!_&VFzwvb_T5(=6t{8t|E>q6Q7wJFXNgR65nT||aOeWY9bOxJ)RKPvn&bj)+}HreukHA- zb?Y*2+|UFi0UYGnTm>b;t;$Lj@j%44J20yfT5%uM9@^&wRpqWLl8R}1B~MKT`K31+ zZ0_{e9u7+VGk<0O$FjI@Q4|#tOdvMXNUG3KOCAN*#?bubmE>AW&QVTj?#yJ<(mlvQ4{c01TQ#DQiifS~qiQzi(18!5 zv|>yndTHtQo)?{D&MYHbO=@CuYG1+AeE9@fO3$}H<)k9+Mz`; z&B<<}Apr}Z%EreCCcjDHu8?8sgmq*m3FN|+(=YkR+FE=aKcxL=MX~a>@7R&(Sy6?B zC~K*5#>R@Ur~uPj0cF!#>Tr<;h)ns5o~Ng0M8-vDcIDtK*7T}5+8XQJ<U^ za6=S#EUjdHeov4GTdvQX%GP0fgfI!?zus{9T2J=Wcx;t9pP}&!y1e4|fBN)E?V zlZM=kvWk+(H%cIxL7SdGhhks$GVN>k*cbofHMS1xGusZ&(X!zX0 zYo3wh(O%rdd+4o=qWo2@p-mCQCM22n=@W^3>z~KZ@Ro(=tGEOPw_CJV z*#*s;HZ|_wf@m@sZ14JnzB2aS{*0zI4;}2gc2?r^#Rd41M*3sUyuM zHaT)|e~K-pIZ9r^~BfZjMt8A^R!9dr3Lt&Y*^lo7P~gMYX|)H zrsMMfF=TEumvSZ)IGaMu1Y&xPB)T10b}T?WsmOIn$n{?J0v93+5yjEMnS|_8z9aUm zkNFvt{$Th$@}5x*x=-&fDs3KW#C-@2v5Vd{&JiN!5*h-yhR?_zLEr0Vz<&s3nY0mP zXO*NsrRhf;s=Xjur{q-_vN>hga68hS(-ZtvGdM<)yAQ%gxDQjSAmXP? zpMC|4oh|D?ViC|tDwuiK8Z?{;J`tk`REZ|3NA#0H#5;d~tz^ILl4Ml1XB?pn#Wt~e zxY)!DGg5ew%xLGC&1VXch=94DYs}S%8f|wB3x*2%j5&GxxBc4ptQ#iVtV?i?xEhXc zmc&U%uSbV{C3}I-Qm^USmMrJ&P8tWZ@2ZB>nK(s4e`tQhUC5Dt3t}a<*9a~*8g!Xq zM{_Qt{lGUB^7K(~Lxa75v%+m0oYcj7;QU-|-n&2?0Gx-D4avq?DW}AAtq^Ov@bEqF zKpSk^w${`818yRGaj)5bW^fZW3h$!z9b1g7IR!En55)G35@)!9rtUYFx|pul?EM~^ zLNYe+&RF#7HCFGYde*m*?(Xgbl4p+zcxi1$t#M$l=9<mKn|Ej)CP}fp64Ar`@s{WE8*L_f%eKHztQV0UN~$DH zFUkhdcw{FUc})y1=fjFlQpT4<0$@JxN_w=ls*018K~e!7`k<={xt00o(RjZA)H)rs z{vScn$`%@u#a~zbqrO)kLSEmF_PYQNK}gWZn{)p>zHjTK4ZDo$xEqE=5kdpI0x>xD z!B_A7crLajw?Zi#`R!4tVV|bXU)om^jdCc%#9Y7{UsF?kg zs5KG_%t}@7vL_<+VilZJE_0AMXLPT*la}c_f8yF8w$=C;5DS;QTl%t;By+J9_c`4! zL}5*;?Dto@=Q5x-|Lj$V9Teh84EQQa*}Z$Ww#MV2E|0=ZoRWvG9zA^c6wWh_(=itU z0}?;3r$!KsvgMD?VQcJIs&A&(RKBg9Rt>1ZDO5*Z@1*waLgzk>&Y>s=aKD0@9)T!$ zXo_kGAJZg=$sGsOfjX_x$}ke>;FobZkYQ?seIcw{m3Pyiw$5E04f4FjS&u|h^7%O> zvY+{X+ca;2tAT{H;<*WLyiva+78I((-IDlwAi<{-z+H~ zIVWO{EHAuV7c-8_(UKuwHmfB{b?q=;>g{Pi7nriO^QY?jLZtJbvn6-qKn_N+g#)Y3 zmMj5(Ir~R1s+?`_!ok zMH}83vWy11Z1r6KH@q>0vqh3+Xp7X==eGLm!v{jDR6flN$w2!}OVy6@k=9R0b+Q%e z4mX%$TGIxl|0n3;>9v=l501p_7)43)*<+!9FIdAb(>DM8%bs0qJ}$eSIt}cKpb=Gx@A#b*E=+)Fm&ibsu|E&+ZCxJ3e4^-9ls1#I17W_Xo;NUM7Z|I+l z4ga#&c!9V@E9;-0GiI4?CBG|NI;%Nr$_70eckMrZ*`|68OpQ0)**qf3$xAQJa8*3= z%)%QtUS76;WAF8lAgh?33Gcqjk5snnG+D!NaAG^7!2jwSW6Fo;G`G&%nKJtmQY}RFWJy6_gqEVm#n(V$cK2Ec_D@gN*8Jg`*r3m7EH1#i|jvXz1 zA9lx@o!x3~+8Ey=U5sr%o3}%HIxU?YGz?dHY<;mb>uul9ImGpmzS_fm2X&D`Byy~s zoT??9x0K)m93xo<3HiU?0k7s%sht6tolUng#@DMwJI@B{#9|t5vs+eUl3oVAIcQD` zEgKEbUY=_JJY(Nq?(+XzTX+uB4|_*yKxoDH=4x`%b)WmRH47LyH-5loR6x{I;-Pz$ z8#Q^l8~F$aX)9#wzJ492-i&mgm+)&>5lRCuCiX;GGH{Y#kxtPUaKS&&a%G#xrCQ`$ zPoc24ynbMZ7*7*6CHot$(n=ZarzYGM?Wi4+H(dM%ZQH8;43P2zUL`l>CAmy<@+eyhFWF4LG*k$T6>5v>4#|J|J z_i43k?KM2oSd`^tbZXrj=6;@q*3x5m{}$P;G&EcHc5rJ3;)9w_MJAy>dC~^A?9+g6 z6TQ+onq|HL79%D*xIdc9VL~a?mfRGEPbGF%>eN-28uaeiu`LKi6TT?Bk}RT62!1!< z2VGE0l~o3rg*{^5jq&Jr@oG`~ZvgM2(udHe>XH$N3b4D&pu5SbMKG{7HkpH_H&yoj zM+;Ce-uGYN$GxjJHjmC@r|j%JsQkRiTm!8(Z4%AvH*Dy7wlwa*0gNRW4qWrkEqvXO zo0)9yJzznJf32WV-Wx6<*sv)BScufRgBC-|w?Jka|%)b)hd2zU!Sf zA247qEC6z-fgJ2qzb>jF{IOCR(^-l7IQ*za^~;}n{|h-eq}OfwH4YFBkFuA&C(UtVFMnt1NsSL(ZBoi1DUsQ* zL3|!S0Px}tzx*;jvKMAm@EZ~E|G=^aQKuRW@?mR`m+Cv7sI25v9fH!qZvWfIvOE;c zBV)9RsnqvU*@F8(EQs+ElY-z8RGCnWA{j$Lgrtd-n>e^SN`gV};Zq!3TcCR(f_^)& z>KH~6aHd3DtdikzI3o1jNyteTU;=5CKRgTE4}iud43dnSYNEh6gqpx)lN~IZ24c9| z4}aqyQ3o!Y>6e1n;-ABO)(;yEL+?Ea4b@T4)z#1t)(7g>^xG4CwDy?H0hMMIoKIh% zU$s&-1OVU#K3o2U>?0Lb(p)IS?87T!5|oro;o}eido4vn5>hz7_D6lE=bQ-#DP+91J3XRGtAP&&f7!CV%POsy zq0skh68e4Kuzvki%q}je{XNIxv79&Wc!_fe4Ufzm?tee~_`m(Rn)Cgx2CLTMrL~N< zhUc@Iho|yb!ja$KnfsuG_s7G9Bj7fNyy=k>vqTX1^L8)<-!)uScQYrtOHt@#)GR~_ z@JpQpoyJxRZxg$>w}D(^7*@4nmi=6Ws%Uh3_xaqUeAZ4UU_N)x{iKYb8D{(E)TrY6+^NmQr)8sC3$WF|Xl~72Yjc5TaBy&Kou+KWrkZanhW_Wz(KI#Y zJpaA*RkQ#03;zBGTx>@F_M^IX0zG&JomR_$CKqhu?U|uHMqc5YY8aXh|F<7KWx`$W z#gRF~Hnz;7UsH%h6YtQ`Q12F+|NDnD25yaXGl_6bX#a5u`6R?^#qb9HxZNa|c>$*~ZX&8pB zFbpWznR$J8*GYiw`zEJ!&=Ox5N0B7;cy<22p%%@KrL7=33;#`9NDCySfED$EfoAz= zj;XVlP(#%Z&U865a|O5wjEyk$L^W}ZVhU@{KzDVA>1XJ#11%?wEgYw ze_IXrcf<`S?hh?%Nqo5R!aaO_UQNQ08z4o@kJqp!8jlOjCDnJ|Zl*iY+H3;CbBV~Jq)I}tF5YTx%I|C+1d*K*yN`0??{FQ+bAToZkk8m_s$Nlt|E#zQxc) zuxIMQaeJ4ouQ4Q7z%+0ZXZza|n8ovhmcK~0L-P4;IqnDNq7wVBn_=|*9W#awtxZ;B z{H?R$6Yl*@By?H$gyWcSk3X@i3V84w<=fFD-}+BX7nU46W}UG)@YA}7aS4wm4^b2B zRKm9RItnrBXF6B!FZGOl9w4{^TewPGyw5H;N7lB43Ys|xt_5Q&H%IoS zCB`4i3)lb(x(Q?y$C3gNlnLx89k7gU$u`H1)ST|EsNyqMFb653a27}#jfM?hAv<1!O0dX+mq_!_6oH{&$9 z+jZjPy>;u>wQUqKWJ9xCh%ul%skb~(t{efrfn0K&n1)Lw2iQ8I$xV?7 z;`|0-hs6M^RwZz3Fq$)jAN$zx{va_htsZ*Jk46K7L#)m}-hYz~esl;i{RQG`OZi3qmBf z9x{QP*d+FY_pyTgr_l(2`L*|8-pIZ0CFcJSrB8uH3BVD%F8klCRFr)aBBR&_k}R>k zLF?X|;Z4=DPHB_bqLNiGib*D6Nt3QCg1sVI5gfuH7V!i0X)6BJ>?IH2sc*tDs_4o{ z=w{}MDg14j)x%W^dvmNZedzv+$X_A5EDP>by)Q=bGa#TmkE!gu%dxx&sZwn;_D}g) zprSQcz!YeZ@>zM&Dljx`c0i>=zdxf=62I^NOKN5O50yz<^i(Ol#k2i(YvlIrYhc$= zyYPdBltZVh5bF~qnz{k=%jk+!B`~s-)RkGjO8SS_6o3J%?8%s@sa(#hg-`~Woa#a~ zD{+>z^9hN8n-ajo{`_+sQZ0Kbrw7bwUEnq8e$s!jyI7Iib?mqX)P%)74h5Cy2a>8# z8C#NoD3iQ0$Y@a!;BM)M6IW&bacpHLsyX!Q{fyfs{61=#)_Co1EVzJVS(^CDQRG>O zuN@)q42~E+(FtZwD)2Q%Yt9y3CyQYyTo>s$o!RMzP>0}-w`l%?c^kl^Swx6hQ=u(= zpLi;HnPYg0UBTb?dET$ebSKNnuX6k&=Uyw#xwQHG+(q}4Q6bjXtG+n+YdhUeA3!1; zbQyr2DB~l(h}jT!lby7bVTHaI*i3xfb#yeNvKT`xi+-0NDEr?R?=;&;FErrEGVi0XUfn8YSxt$1 zcd$WM5F%Iu9Cr;kec-!@D6AHCA1E`5=_a5MmW z+=hIl?NsRLYQQSDV$m_tB>Lo2rT>R#P(NX41DT-g!3mD_M-}-Taz8@O#^jG@Is$lh zCGbPGL=A|LFC~C>6(h*v?OhT;e{7uX{2molrKe;&-l}>7R7mj|arS0H%$FuHeJSL- z9%#}X;N)Nq2Sq~3%T6c*a9}w<_?mDDmWt}txJM8ds#J8sekE>+9`XeUa-QU=qH@_l z+2-lxH3XIhPgY60xeQaCUqd8KQlbDz`})wQ?+(D7J#>W#mtmUP_w9Rj!lv2*IGH1^ zo{wCyHsu0u3M7abg-&Nu$MPxpgAoZ)TK7gsbFj{R^lvY8W?ZXDyJhnPJ}W~M>auh> zpgD3LWr(Mmco#eyF?DF2WPF{iE__b9jwUb?ogzuVd|t zW(85eM_60iOSS?a-w$d}K(%LFz(p(*iGbRaoDN2D8>p&Y)lCGH$$Bp1c3;y~?Rbzl z7#bDBur=|Zwu47?y=*NtvsdR{Spm~gQvy*~0H<`Gly)e^ZvU-3Lf$wbBs?BLhGf`- zvjN3@CabMOht+(aLMk@2UAahjH0sqWM5&hSeQ!OVT2>V(YiKcn@8I;&j zmU_)}z^|L3p;2>jK2jX!0H>?SoO4m~DVX~k&A$$19Z3^KUGN)cDJPTdl%9lt-MD#k z)`O%D7plvBMx`%a3A7d})c{9X`O!66HsK@5jU03~Wna&zw7ON@l4S^v4(OHe5_`s$ z+0BoVXp!A>&l@tP11Bqi>X-PIA%#aPYm(81QRIYLQJbG{=ETqezIyp`Fw*DcUq6V*+vkT+DxW_;r)lQYSV{1Kgu_LDzuprW z^b6=Kgc&j}k<%>BAA= zeh0aHAG}*7$y?W+@&92dk>99<6k=duCSl6Wt)25JW$rD@UT0!#FXFZ!-U4H5VS83f zzBZa^zL8pn7@z>Tjvi^%q{$#s*2$kfz;AS4Ic*Lw8b=#3G!k{ez1{woUs?sDUV27+ z{^I9%LAo(~vT4^vMNMvyKK_zZ$YC| zCSyjT!zIl9I^1LyC`Mer)tPs{Mgmoq-WsMq>n0Hy(V|dd&B9Pgy=|7mevyp52WJ$WEabf&;Pr8G+?qgjqn7P0PC{E60CI6-g!8w9d(|=+NeJ zPpfAA^^9Gco%GX@zy;GQe6Q-k!Kx}R8q>#ETPvu*O+hMnP(yrv{A?FR$d!h8BDT7k zeN4kI8Pw>R`Vj1?01G8p{k-$(?dTmS>n>q9^L|;?B1SwnG`x<1vsm>TA(fXFFZyuBu!`%WkWG4ZUC}4M}j_3 zK|YtkUWi-diX`TNbq99z8gM6?qmsj7B_XF|ii@wpo(fX9XjXC1lua@6>e>I>9?bwf zxgswg4J(_uJwhJ=SyyVFCuu=`DVk3S@|W?YmqC3F9H#(_H zOdg9oc55cG){b~Xkua3Fjc8dWfR!C9}3qvf0Dkjf+o zH)b{7@u^y+rQ>ujB>2PtFZBM-#i!<6b6y^omzOte&N*I-7Y{~|X{iZVbP_DgD-ah; z4!7eEdI+Ls>vKSOarDVVDztbfiEES4QQA*H_+D0(pVTw|U zgUt5zy1|5Fa$2Ci@W)^jn!-AxdWb%~A&~A9{9Ps_qqUp-;}O2w0@FH!~99{E~q(AL8_IqaXn_uax>Kcg>%l2PqbRP%0q4*%n-Y2k0A4~OM8LW&>oC- z8xU0O&uBB%+Qfw@dCgENm4(7(iDRbxWRh|?-mSidY z-5`3unb>MOhslB!2ZNw23dsSwyQ>$*a7>Pe<66!dpFDF*{**zK>cF|($fXY$J4$2B zK&gAG-!f1P^Don8hk!mI#_W_b+PC^*yE5uN!$pUo*$38Yux`(uJzKrKrcal=;{NxC zKeDRR%Cc9l`%5BLX(_D&kjcNXAJ*6=ywlHsB$|muN89oBg-u^I2Q^#5@y;>YxHK{- zDpe%y0zmlA!i~uRXukS=zTCuCcb>9z@oKH|*kV-M9_26VY|1mDr^*|3MXRf@lc@RY z5>yf}wkU)Q3F#BDWZqfgvPx@{Bzj!lzgfTEPh1}%w%s++c(Ev0h4+`Z=*@&N@8=rX&Z5AHLG8@ z?yZXbucsxXI}IPc?);LFdPo@fok2D0*1&Fr*Tcyvz{EywmTwm(yxV3zE0@18ox42$ zwXW)wW%XA?MsjXfSzP%#BW!WAgZmR6$w;ekDFSk$S0oKW(ts%7FSj>NevhKg*U{;I zB~J6&`A%imbst5^jLkugk>15GKYpAP`oet_lQALNp5jo9>>0Msm@ZquFr zs=LeIqvpZv)b`X+5Bp-G_TT>iNMDZpf$R_Z*kYidb~fW>nV8J2IDBpXgxWgh9V=4v zj67!tT^TpSqi>ku--M5|$3(>g!@r4|8V`amq|l$$VJH5`TvVfRvRo6a^$o29zxlM6 z;-!h2jdS=v-$wWZ(iLqUi)h0(tb>+u7tc0>qHzvz=d5MCk@qNoXOonV9G=W&KfI_4 z`k0i@*whqG$pKYmPKkaOs=l@o{Sy2?j!pnMm$xNfyeRkBd8h9Bqt-!hb|CF3_Y=EC z8oN@;eX`m-H{Ly&6!!UCmhO9VyS03ZHyu~jdpCwejA9}`2i;#zKRIIL$itt$au9{V z-HuP4?HB&;o~^b=1C2yc7(ZZg2a(xg;1W!rTeo!WD~HY|@gq zw^vhbGu{yoy)g^{RxdXopY!%s?un%{b*8=dKJD|c`T8C%~B{7^~9fC)a8D$X{ zWax6;pLy^S2?25b_2ZNA@4E5#rV$UW5Qq{S8~`yBrcGT29pPGpYPpS1bbzvQA!Sh} z3vp5Yd$@+_P#kpC@I<-{H33yt@L+ce%cG?wEEiT^+leA+SUCTTvf!bxQ^= z`_2L+GWJ_0SkH;$$E9jTRLn<&9O_6ikpz)Y+quk4F9@t?02>NQZHq;MXgQJnC_p{5 zfiU=#WXH#WA;4v<#oJ74b%W1A3DHp|xx=zPhp&iznsYLDKn|crEo&s{2$PH$Wcp(^ zGw*K@b~cc#9D<~JQxbLP)$7!aWj*FT6&Zs}NzX-_Fm-()sU%0RkPXC*F87&clE9e#%>{boyk7IRGNsthoZ`2J9RKN9`4qY4tD_2 zjnSvgP>nerH7_Uz`8j>=oSD^n@kUM9GrERNwRgKOnT~}@156KvOtKp@iCJIIi;8Lq z^Wuk`CESa4KsXlXK1CC|n3;`?FU8a+37<@4H$=qDDg8U*;3mStQNCEy+pvM%PpL?1 zO5aN|0Svb48hrgM2Jw-siL*{JfMmlWA7!mm18wJkFb#@3Jc;8fmUnNPu7I@tjt}Qg zdSA(^IMoWEXa1W!Ljm0QE)hRCL6OsH8=p_~;6?UT_Ww8|tf14`ca=a?rKneDwef)= zmDprXbJhRU%0>L(gYc1LBrB|g=um$Sh?0mW(HH%9zg0-#SC8q_*KipDiZCuWFTur( z8d@fy2Vu2I3FsU%z2=tJ)^OfJ!geVQ_Ze~BZ1 zr6vz%vdI&8EzXHCM;k_fp;|#1O2`3s-K@pOmLtg#)<*^TbUWk&8;1@;?jkAj>B}OZ zlV&wIA5OnOGbI;LP40I$+BKPe##Zb`n~I8}M&qkL+d48Q45?Apf&L?x=MRapZ_)LN zg-ERzPm0&9UtdyvvfuO3%}&`P7IHfLO8~}S5~GbB{EN<`v1;`A6)x$a@SI5VB{7`_ z-llFF$<1J=)Q+Bz)%k5FhZrtoP;N0y@?ev{-U6#-WJPV65O}-Cg#)9S>gbph33#pJ zK9}Q{WkG@^r;jdAf-X5<#d?wB-{x|eLQ zRV+r;8V6UCCYK~!ttde$O7Mq~N0+ft?ad~H6zVmXjgFf(_H1P@thKUQu{PmmOF2j@ zh=5?j>eWNygBnew2#P(@7mtwm!pZ+gxss-kz+H-TKFfZpXfjqJigyDzZIwu*r|CHu zS%kY6vfSV}igZxWGfO|WRY)Kk)mTT&+@(tW-OV?lgm%P+8$_mBfH%AqwDnP>szjgk1x{OwR-%=H6|VFb=HV6Pl_rnubLnD z$BriJcRq9b{r=*ci$CAE(QRSW^T0nA?)}znVJ^G->{$t7wO+nPoDknxfG97#C^GE7 zn1w!{a%hD$rPk`rM;wfeC!bI2%>e+WwHvYa4g}_7&MfefgNkPR@(XRoncW(Cf@TZo4(wD<+*prhaf);M$4yJdOOj z>lsMKyp7-fdG_?_b-fCea-p>DvtV%RAi6R^vlM02JcF}xZ{EzhecMre*CX%;^ejUj ziVc{3gd%WVnvR)k9t4sC7pmFQXu^S_9gGRny5rrBg9i}=Zam7HTg!U4vY4vQc~m=t zspac_1#U^kkaX9q_37j7zc96a1Pxu*pz$Q6Xf>qxy^J#U^3N!kVZWh*46UbQI zS9DmwO9UPGH|tUVcRI%g88m0kQLVnV>-RkwAH8&UH%m)F$QdNiLdVouW^jq_rY0$a zGkL?=P0DW^ZZruqZzl9!`pEz)%#-CK!_sL_8~>u({bX)n#b3SV_oIeCF(L7|flvk< zJsqdF9j>Xr+9t$iUo^Kindc3gG-<-Hw`9HJH*efPId|#to zj1^)uu8rGhoE=YAjp%pGU>@jS+Y43gyEKAttVN22#DG$=c~@yHcXb1&!sce>ozfCD z4Rh4GWK$D~fD6745=w1XD@hpW^p4VNK0^eoOWP%VD)#}hz}$*F1^i4<9}Asgj_qi` zgy2^j@4N`MRe-0xq(oxUy!a*7SHkXpUirpC)I6LPG9Lom$ESbmW@8T)_uHh5J$SH$ z$1d3dj4Kv)KYJ&6H7`p5Hc zz5gs=|INax(|ay1#Hl|rtR9(xjqQ5U2T)c;OhfGRCSJlp^X#OzVx#bHu7ucK>iLec z8gU*h`g16Zi=<7W$H3P?!id&ie8&w2tD@avN@2owoPNqZOO(kuT_jEAVx5oz)ws|& z_?5~WU^wV!2^<0%8b$d?e|nK$^xorGS5UPKUN}ed`ZQLV$DBET@J5I1B?FgpQjzuW z9v&~&f#6;!j!|H5X7H3kgaVXf6mboiVUq#nP3^$t89(ksTh8omfJo*{!Q-+4&-j5= zDn)Nu)M{DVFMC?~f8UIFh=-R=CFY0vV#DRLtTO|cD{#sM!2NA~rP5+0BMWRdJ)D}D z6?zDog<*AV`|k~P8do!8hx;my#VHvKc)Xf_f2ket?1Zxa*IKLu+t?Ts=U9^|!AVai z=yDTP4~jY-ry++ai~%7-OUvFghO{NT?WA*7aOPK?t5&r_h% znJYwsT(?03XlI#COvN4M7=M69T5f+5;K_PAG~rCr&Z>5PXVAmOW`Gbg>iHnhz!9dN zzQ0;_j>~j6nj7p?xjWBiL#B0*L;6?P9x#?e;F$hV^ zml2*U88y-!tFm@va))B3)p1&YT&)GrG$%2-^EMKK#^?&#yEnIE>(e|*$--3=nz$3( zL6;L#l6A@3^~oMH-)l`~>G#QC?I7|qV62QIZ1Kuf+x+#m{_QBg>5==UXsigfD7M5+ zX2l_Vxz)j|RE|$by-;1c%J%Ey+5$U9D zAH9@VGb9PycI+@(F@HdyXAnbNxx-en(^#@havbb)H3SfK3_~Z-C=7nwFql7l2jXqa zTuh;byNK?j1KbiB=~hU`_(al&cyoNeEL7w)2hZ_&T_VTleJ3Z-I| zjnjg7*2-CkYTga47W`_!Mf)zddXxv=tZX%K3VEaM-45-&%v$G4LT_aabG6}a8>_&6 zP}5Qe!L;egSlWdf%gN^?+yQO~6@<*137+03AGS#f7EXYl+W9W(Ashv_xnfSVPC&WI z-UrFe;`I_3`I;MR!J6RTwpgg!ZlTfY3Ma+Q>O9-c-#_j2HJXRZO~(YI!3j0~1MDA- zdtEPo3Jr0>05id(a-(pALw*}S6k!v9PGxcq`_?Bt zWNWo~mD>*8%&jVe_RODh?SAUZEJ=5zptt|M6Il>FEe=PUTMSk%-*uCg;38ryRwJoz zLusqfUk#0SUO-DJSxPKaE<+Ji(3F=T3{~CbW(0~MNid8I^qesxPG^upt5F-aqxfME znQdkU65BLwTx_{w~e2aepcgE zYb~t|R_l;-_$R_3Uk=h~*~i^721 zw=|78P(1_27(i`<`a0W@BR#RhVUs?I!QySM_MWL4D-<(17khuYW957Qs{i%FBW!F| z4YFO_f1ICOy)gICWk1 zjCDOnhSdr9b|L6ojy0-wiMk?StQU8w_}-*kq&-#3H4+E&9#SMWM@EhSoRmN$Xjx>V z0^gxYWcF$-IPBD~*7Oe~#x?`(#j{PuMIA`n|G3Zn&X{Yi z;})b?fdKvBx5mzKJr}3;2t(U_;K9NnBP@waZt9#=lbh3Q>P9BycXeY5!V*&MN7m^td${E6kXo=M@FE) zvYmGLz#>k>_FHWzH2X7rh9i;5mYEcqZB2YNy{-ovwms$>+L;5U+u+?DuB<#?5q18N zk;R+?2GMmGTFTA3e)jlp*Nvj)F_dJ+UWptH^*x}#M=P_*2zQj6I3x=`iN!xo2{pl0 zk~Z&`a=Td-1&3ed;^Md7=g{=?wxIGi+lFJsM&4Gq6!+fFoja!hPdSprbNQpn))}6& zR|L!Ip4s)ke-h}bHWaGuzV;4GYgFM$_G-o5j3Jw3gf~D|>nOS#o~R^d(Naop&i`xn zD79)l75yeEbzXIsyT6@xa#W5-bpsI}o4?m$9;4AH_GR(|wQ*Zz)PVBtB94dc>iw0! z$ucSI%?*WKDY5U?!YwPTAAB~Q_(;3z(!cGU@;OnLPqU;K-(bqi6}*z;QoMC&Qkwpt z)XsYRxHYM|NCPed4%l$xvxx)LP(a+xDVpv$1qff4`H0p#A z=rA_!pvIB!_WHF*TJFH+yo#8N0+U?U9C{_FBgvN+4F2}*t`-*05m&ee$cR_sR8@s| z<_6?;jbkS>4Y_eJZH@)a9-v12nEh-1T=R}8PSap{p~dw<#zi0D?_ZhIQg4rer$%@i zF-iWSb*=rK96SEwsw-jPz{Ol;I1mq7w%>&bnF>Y$N zw_gipT|$FX|JE`fjt6Q(z7RqP;pHIUMvu_$S(zc$7BP?Tz}@*(7?38GUxv85jUq~2~>kuK!((I_$ZM5NaVttjbhw@ zhLy49;t1sDj~F!yc0^=7Rw zs%W)rnaydn;jfeYZke%|mzJJ#>QwVashcYL{rW){goCSp{@x86>Vxu`CT==ej*EL;@L%wi&h78r?kKnpqPifNTvm#AL3u*zGW0-y$s={ z3528R%E-)AWqMK5dbJAtOB*QIax!yu@R?eax-SvxG1YM!A4?~4qRcmywDI|^8#f-v zk#8GhUA8Xila-0a%%wU!lj{HROok7%9>xF=dP+&Rw2jrq{LC++aj7j6G>=Hw(7^GOw(Z%|AVbDoaMKJDD4vbp^&k&jY}x=76e&Y$7}oqFx4gRz z7@$YfCTTGg-iIb%IB#6>Y-OX%i+sOdFjzzN;zb@yTP5Wa1$yX%`d7l5s_)%I&zmsz z*dLg!I+ajOuS26}8ey9=%>F0rB^l_^ZkD7_t#nTWuXpq*e!%JSna+3CmoI>Xja#;? zH*O6 z^lcoasu}1SrRz}KJgQC~Xhi1E;U^r?8^)<4MTq9yAV*NfAMpUQIL^P*4GWsbvMppU zih+u+CS#JFj!x)vzOrN_0Hxb`|2~vP+s{p}44S4H^7`tZQ~-gV|5a}NJZq?P`bI-3 z31&5<`KoRP^lGjOUn{nW^~O?Z&?% z-eZ~s9GSs0g8&fQ7WNe{mOwuU6vF@vRd>}nOXd@I9a7$2tSp^IK39IzG#$5qmQczZ zHsz3PQhd?sMW}kIoh2*ckTIq*vG+_cN+UDw^bimE)@lr z^CI6hqc>BQj7hvvck@45fUPGI$Lz07Xq5X2Q?}l?Y0Yr)$069D9 zJUj3~KL!rRI>!5S3)3Eyc1ri}Le;+DuWh=$x)h2H^75O_e8Zi5!|S@Lxw$8=eB~R; z`^13Bdl*tp_6g7UW)qo-CM5#kjZD|wvTIj1DYA!L6I-&!V!f3)*7a1q)*3z?uX$^F z=;!F->bT;sBf@q!Z>Da0tC{sAE$t_cj>>K4G>2=dm{2w3wtU28Eg=MCZCnJ?Qn~56 z8^wY1(Fu{P{^osVaH~-sMGp^ThcYg{woJ>p`sh)sSVt*)0kCRMVSlb7o`)btP-1W! zQ5^o=%Txb+neatQC<-f{7Dbg->(=IjLn-%!hh?kN64N8Q-dKS)?}V0(WbW1qvCPye z?(`pr>_!~eTbkO`)(O{b;&o=43|4eEA&B2BHZIO5mXPDb@W{yc2;SL&2!Oz3$6tR< zb-xYx!CzqYbEuxH{{4NNA!s0|EZT4M{gk*HVWcOy<|`R7_T5(?MwP z8`{Ib1HMUWh$IxY92iZ3f);n%Plpi_l4nw;pp52xq3o1vm~Ai=;3(^#w()1_D_y#V ziN^4-(4(O;1 zi+4Wcuy44C>ud!exK8zE7SUz$B(un!J7>lMh|h}MI>%!%79mcE-?Hnd~pB-dq?p5w|-+FjXf`!2V< zEZW!DxT*blzgMqc%fwhNrXf#p>7PhV1=NchH*gAtmVH>KXRlvh6#+Pokrw|KXKwi&POT5J9H-p_vD<9XlbIG+9NXD#Z!f4}ed8qVuH&#T?|6`Z!T4ig{kiB& z)=(r>WgzQwQ(R*k`u`f~KB5t3nOnY^>R~ph{M>8lZ%8Jmb-+ z`ht7n-@*630LumBV|5_zXR%_ENGW!cVKuu!YfRBmhd$i0Lk9!?ICs4m+MR3SMryvf zAU%_-J&ufMYO~M{w5>9YQJS<|8Tk3^q32U4bVrFtULM*~<)A99L+BJQE*?o9EaTR( z#yI#=V*rK%V&0-Jl1Sy=48$1>Z~gl9j8eCf2h zfF6i@5}kbzhk z+}t*WdrpAk=)MWDIZjvDpW0BQSVu~hFIxu4vAoOrwQF}la)O&D6;e2j{*rckWmP9F zzEopW)wQRn=Z_pUYE4ls1?ajk>T9ZZD+tz7Gx8iXen3R9*o2-MaA^gfOgIK=W~i3iUp zDUIXniVhWnRZDO?Iys<;{@9Zr`C*TE49tx7ysnM4c0@v(z_cqa^ZHLMi~Bq|q^)2w zrh;9CSF{c{YtAZo z|90lX?jCX4oOaOU!=droqFN>v4*2C4u$2f>bQ2ucqJGzPyMS}W=TYg4{Xs7UbfWKG zYEj-lJ9eX2WtNn?L^T}Zw7l{jRsKvvE*Yb-)L%<-qyXogg*+$q6J9Q%7VVg$*Pc5j^Yd z7k*xiN4LNSq+xLxqkt5{*&F+AG3s1DJV}ldosr^Nw|H?BDJlx4+?FoJ$L-JR;;S|9 z-9sa4Q^HNxIR%;RFP9E+dS_?Su9dVGS<^=)^xvg&X?ZC)ZMWH}On@_k2AY4Zy?2g* zVMeSq!&)uau@{mw5FQGNn=o$`uyp&=HM;cc$Fl0}Ai>)qfT@Xa6cSQw{(B~GXH^wj z(fUaOk~&8`mnq2^yw*pDin>pBBQw!$4&x3qUN9UUmdk(vPCEt(y%BUg|MUVILLH*y zRIEu6w8(BpVlX!RV)(3Vp$!bSiLDY<#>y`k$GE+%F3;hA%;$2;-nUyQ0k)lKdCHrx z30D91Vm~Ysr7gdTxdRsQt3aZl2Q%@P+V48{4VM{y@p{@%xnUwyM)zavS2k^6u!>d^ z9yPKRVUrlt4308%#~||`Vu}n&CrxU_Yz_o7oW$!M8y(#bfc5C^*l(!AfJj#|*}xvh zq=fO1YSef)*{_eaj?q0YL8Yp`77N@K=$@4lNp&kJrq-oS$Mi2SXGuYIIP%-@n28HxT%LS8Hq5-%`z;J!Z2k;bShu$X zE7|S?O~G!`RYQdc*K~Dncgtnb7uaJS{vY_*Kru||2d2Gr(pl9u(+|95Sx`Vf4AKUQ zJ_ya`{`qVY*O)TNP7q?AgQ!8t|1J!cNXFo~czIHwNrIHQQw38s=JQ&Vz8pT|0-GB6TE8iN-! zRmL~8)%oi6f8$nAB@oqb!tg-bW1h`8=7itJg7#%gJZgzwnNSjC2Lf(|QRboq1{SdQ z`=HFUp^clwWL=i0T-5`PF?cCFEl#n#F3BiMJv}3$*Xcn^m#JWOGaYre*R46pr2nb+ z5C%?#SYG*}_~7kYf%I0^uCIQ~+{~icM*r5xU0d@#&P%htCe(%t=q0$T)O3NXO%IYB znFecal9N0>@ zDkuT4Ns3y0(e_BTejmC&)FXK!E{4s=|NQjM?gKPuJoOy8r$WloT9 zAoPC>yvN$MXU8{g-jMzyuARx{p$Yx}^V>J3(2I5Q_Kv@^DJXUbYz&=vTFD>vlUi=|QPZKUx zi}|O*(iX3MTYY8EYZGmcrA8Vy_-}yhsnw=~qM-{>KC$>}|Imp`XrC)|ZTjo6TT@7l zV)4Lkbw1!~RT#)5;0L4*(e*K&Ozph_h?^&S5t<*v{}KvLAx1Jzgf=1e6$Ue5*Hq(4 zF_z|DpLw;2O)P@oDa6Z&MBvW7ZZ{{5E!pB9;Qm$29+3~m7cNsmN0)srMMXVSPhGqW zCzJ)5R_5)0!BYu%-^*KW8er4?%eH$`ZlO-4iHrf<>PtgOwFBMU-uw$U!D6eR#1#&e}?1l$) z4&2zVvXJ;(emODo#J^5D9d~#`1H;yI(?qHYvyF!ed}^xyvZX`a%cxf9qvpV@AcXjT z|6m$l_n0i?g_Nu|MKkDpFB{&%HSkqp)SYQ(seXS#uPyLH+$ihoz(UxzlG(VdKvPY; zZ8mXCCxGK)$aRFG+EY*VbWnYaYqztdg;iw{uqg81=EYSoT%UTqmCY@5y>^=b;r$-@ zl8ze4&BfIxHhsJg196y$paT_*He&gTbP;#fuC6p-M4z={%ReTDbad5ogr8E(N-tgPJ$bo zFoQowNvqDMg%P(LZtSa=31${c*1j$CjGJU<_#of4mQE!CdoRo{ZPcAxBF2QvtPoih z;MSR1hd)ezVE%ait`HFl5QB~8Mgf1*}jz57C32gm3A812lLBJNHYS4$p;RJQkCm5eP zy~GGiKzjCph@k@NBLPJ|EM6v7od_LX>#W;Z_+;FSCrOD*&idD_>PIW@<4-q~&@Q}x zY)=q)tfqX1$?_gcs8=Y{_>GJl+*khq?w-I#tb}#a^HceYr8tvP0E1ezA3{YH=^3$9 zs5JlzvG;z(GXf)nUDPI0Hpnmnk}Gi;#AfC9>)UYX1*-c?nJipriclS?BZxqlDXwH1 z4F#_BYVO3<9EoyBncc?rZF_6)R;({J2fRIPyg0tP+_k(y?iyuHD|2&g5E4;n!Q4;) zFHo;_gWo3QJf8*N@EJjI^r{jZy5^RM)r#=Oq@FW0iig54iAcx7@q z@shgCRwl}wn!QY@^1-`sBU=mS4m{YbO&eW^F0e|6{qze@8_g)>Q18ckTb|0)D|Z_^ zlNmrOd;8H=J##l@%RGTga{}8uuGdaF!|^Y-6R(~#1w!_gm7SeHNAMFWwSuTI^{j#VZ3YGe~?tI79q3Ga3^PwXDp*e?*yDUYit(zyy$2XUe;4a>QtOfea>$AMeOE zM{0c_-O%a_c|w%ecsQUsSVgV`t99b#%MRku@&QyB@~%L)n?D~iI_AL5VRY)Xw%6$? zQLWFV9H>J_+q_`-zb4bh&&$c>~atiXuWP$Fy0oS&``2$lTA3Y~QXz>uGu;<}e7(~PmhwtvL!J?_hy0XDsn zX2@(r!~7UU3c(J#Ssl(m1E_Gno92_e`q^-6_g|3ak9}r~a1O5E0I1)z;p) zdDYLJZMqlcyUq*Y!t~7FhMfWk(8o`oCO|zKd00KvY7udrvkZY}S|Sz^gr+^iQxl@% zz42JWffJ5b-8%2%V}3lr5m6!XODrLr%IG7Sr{{`i54~#W+s>RTnX4bKsME~en=r&% zxPjyn`v6f`J1yd`^(mZ6Lw>E z)DcL-suDtw(qO{|n(f`5Web~8JbO)XeFE2nU?bX0x2pk7u6&%Tl-P_-w|;z; z1c`9Iy_G=l)|69$>%(c3y1;J1oB;^>gRfdUwEoq#kPA93^h zIH_W)54a88OE`+-7~Dw#_$1QF0%aNoxQ?s8vdudaihGR={^JUoAceHHMcI)Pm`!r8 zrq5Js(yZBmqiF}|T?Mnh__YkehR84G45UXE^(Yrot~qqOHGaJiE7uFsed70AH(gb+ z2n-07js;7jg*V6}x=9xnINsqFm*AR=PGy5#_xEW#J8qJ9ywgVBapTG7 z77nNuk~zEGvG*-P#+mX{ExJ#vr557ue!rr%#)d1qawe`0x*XaYzO|Nq0k zQmen+Xx3kJTX`=5jzPB&S`M`e0C^n$>(Kw4C=B9eQ@c%MQovYU)a2Bm=UO37&BOb-8c#?&d+G9J%I%>AGrzVP5N&d-<zR`FE0QDztk!R*AgNq0)K&%nkj@F*igLi$*5>>#s9 zD8b!eiRa9{{fUT9y(S)x<1SR2cK3kp6iu~Y$8)?hylAg(?Yx2hO~I@ohqmuJtP`1Q zQt9OyXofc4%jkOAot%X)wEyM;>~mqx%Q&%1_wI9_W~dP}d1B*fzUc5o4-Q9)|8btA z&1aDC^wXQ^j zDV(b07cMZ`v33I^rzW4m{=P1)fyg1p3XchM01**8-D9{FTJBhV_?d+rsY2Tx8_1MM zq%4=2c+dOuj~X}ZUpd5%Ik@1x7m%mZ8F=8ppQm2_z8O#h-V(BTNL3&Z>QkB>x_0dv zEMY~5TRF|#a=xK_S*D%=#VXE1+o1DM=0%nDQ1Hr?`mF1syGkCG5}Spqb@pH_HD&?h zUPG>$m%c(-^~ftj+xWHL?$Ug+s|#h3Qg@wP6)7H|`S2wpVW+#V~e5+%=$!j(fbvRT%xh zfthahVMF&0)#YY|$9_2|g#!$E@I0Bni`fzvgo@ZYoBP+Q+3ZMHGcJx6z@NsvS2ue7 z(HMX%N=D;|CobZWOaQ+L%+sdV(~mRM1UH&~kHsK1*?^1R2lR?RQ5jZ%u@Yug;hL2* zK5aG8VAZ_1Nh^plD|F!xy}|J5&#t zQTh@xpN7y9rO{Dhtj4gdpr~I}n`E6g3~3C!19Sb9cwooC7so#cNe%y$Z53pEqe}1)l&SxuQdRO+iUVfRYZsK4{A_*=Gqh zkZ`bwc5UP`%-}b4NrH7i?FrS^`5x8bKSx&Rs|jyXO+GMbo!%oK4}cy+^vp-Q$?2R0 zE6Az2#q_mrD}EokmwC$zjHTSH_j)GlGZfbMrxg?9C*q7KIicC8eTPBI+V)j@Is@q3 z!QXt>H9{sLR;z+8<(o(ihbRTtH|grg{Du=OvOKsH2wsPxZ;*bgNtfBmZBJNAO4nN> z5H-dfot6`nPs;ydD*>mQY75Bv>eXtGWCP1r&kfb_0mUiyBft4pd@l`VKU{2_*XIC$ zfIA7wk1K(6BWrL=uupvg-7}iAA@>AsPFEMm_F(FoTO(N2hH^rDVBpr9Iui3W2WF$)c5)l+q=r^ye7-8qq2G5P}8a^9ww zEGs!bl)|VbRnhpLh7Za+f?z`rnRaGLfKUiITsJHU+l|3np3%XGJ$zvRBM0W0!8Uf~UMV^X;)bvx1`G#562D_D78YyKFfA5}(vL21FM712)#U{M_B4 z)!)Z%g26I{YzKh6Qui`UI)QLO+41a}@;L3}@U1CB_r^E`^yJ4%=%*y+s{|z>Gx?r| zs?#HBU{e@5PK<58vj)(qFo0^}|DwDnj3T~n3_`%e-q%19BbY-+vix}Jq>DAx>0X(m z*J7wla=naJ?ue#@MdYAP3&C2~G~Jt2LtQ~#E!HCd1F}8M(sPeOiGBb9!Y;SCo^(q6 zTIL3CyJCV?>C*gwF+iW%_kX%o8KV@EFGX$!$OQ8!o6+GVUbG>bc=tS@wF41Vo3b=% z1dVpntu@;dR6%7vc`hP!I#HU*a+NFwgw-g_?p43 z-$Pwte|NZ{I>MSNj^Man%7*Q#0O#+HTF8TDKh4Oq>tE7@N>R*?Qv=>oEF4{-yQI%j z)BY`gtdUXwg^T&`QbaP#(DAjiFr}cA1op9ouegiQVLjrICVoKB$*|#OY%&q|TLNDcSQd+d}vUG;+grI5lLQ$c7 z+yQHTj<2u&;a=;s{$b|NuBA3cM||NSaWn#8Pi>}yE@ZkGJc#QxxrcQeThHxt-ZVCA zV>)!tp?j^vZ>#Yt~tO9>JMx}+)#!Cb) zsj$3h_V#iyP?&jF4b|L)Ru#mPlD~Zo*x1-lR zJn@RYvR+OL6BFg-24_aEZM7%f$)k}pBA*+|Ra?0x?JU4RrPyWiEBnW$Z>KV4S`R#@ z*z&fk4g28iuI4qa)l>f37XDgVXjb)^QnqK6?RDB*1$CN@;~ImY8Sr4B@*pwN5{E$n z|6+SAk_F>5{me<#;t?HUAmeuACTN*hF*5HH4lT@75vfv$2#%k}=1bH@M~^*SJn}44 zegSkyh<0-doMe;-?j)PTQ6m%_&n4WlQ*I~q2o44-Xk89drYP_#9DvM(nhOTm8t~`I zOI!8Me7d}*Yso}5gWZn#fC8V5wa_9VB(QN|4Q=TsB8|HE1p4|OnsN(J6n!y~-Hm=)>@J|H zd+r|@NyW+_r3=QO&@2@fzjkMK3(=}8I&VeOZu2UAY#ChxF~GP+O-eaX2h)#_%J-&z_c%W9?%lgZ zP;51J>>CXbT^`&j8011Upw`U1^Ulp`Ivc!*cM0og4k0%gD*!|?7_zp>7QCO-Bvm$< zpk66+NurNJ3_~l2!_;QyD2!@nP^bYb{jU(NctRKkmH!Kfi+8orvFgNj-PEgjyV2wk zs24J7*icp_YI$Fh_IJByc@p!Ci zB%QLCv;~n+(o2n+jM`aWA&Z$leyF`ASq1c`8BgMpWP9Y-N`8K|r!2o451;Ac*R8A= z=H*@-rIONX}}oxrhU0HD&Y*XOLZBS5?cXmJMS5J^!Gd8F^<`U<-b8$OwA2< zaWs+!5a_-|+qMUsd`=ZOm%CjOqKxo6%AG4q-{zIa)2(6{{#mA;Vn&G_Y{RdQ@SLK+ zx|!I&;m1ztEqx%Rp3lkYZ}lER9vh-ca2-NcrV$>%S@}3=DIt7hdoyUDIiyUrYaSwh zS028az$hf)@pDDs#V@i*lm5lJ1nk5G%!+16A;pmLn+I54^ z;pJSo{Fu~9OwIguwhB+1;ac`>47vBnBomRoR*_tRDW;pvo%^ZzVMAOyyngRg(b&`S zwraxr>lYG!FH>#U5Lok)T}D>Ioao#@iHRpS$9%nYXx8x= z?QeC^x;AER&I3=^w07V2ci3!lQOjtM`@Km|OUst8`Q($?OLJoM$MO>gPNu!s7Bjc! z^0=7xpSs0;ssO{RCI|g&#TOe=+U{*JosYFOGt;770@4=qEymi<5D+lvT}7t1BWd}R zkppEiuMndK+J}aL#N5F<9JM3_!9evbRund<-lX%aZN6t|7Jp(-Vm0q9l}H;JyKB0DA#I-+LxV_TF^E1Npxw zI3Q-7!(Si45{v}DPuDs}`d?I|)R^)ly$91AV18WepzbV)y{@A_2}}1+f{P$k^71YP z7Kla`tbj1K(=oVa_T=S}k@dkpskJhncIBV8U`)jokj#Opk!P!vbE9R=hkPpYvlCk` z*L$&dS5P1${c}I0Er59k0I1f*w@{mtvv2I#SVZ9UV9oNTaYa;GoYfz<9#?w@m=}pq zL$#vtm@0Xqs%V(C-D@@gQT8c_NV}D;~05 zX+KrHgrB&(u0FI$b#>8H^S4R`KEzI42j}B|kzV$$`|>X%_eara)B5zkzj)jW`Q1P^ zvc#?9@F=E9!8PkgEK7nUJej#twGH=Su3SD>JjiKki#uuhCS0kwoTXyw%uRR--|v zQJA10M0Xz^78bwHtzSQ>m4vSF>dQh&uV8E=cfak_*TZwr>czWMA(O*7xvN)29+xE` zvH=P@1VtM=7A$sZ$ZV4Y)qnr~^Cheb_F_$WP-NtgptB$0M97jXg{;4EPxeGcVnor# zgb}5KTz7+NlovP06d)`T4<^2{iSmTk4k=Ntr=` zUGM0j+h%9$>yX&FEGDJ@^-L(=otS7*{WzrBowtfzkMwZ=!99)2%=>}OuVZw|`JNjd zGZkU%%Tr$rq%+`u! z+M8ir)kO&*G)EEe&dtM)J~G6=3Q#=SrPs<6J6Z_2Fbw+0)C# z6f+WTHzl+k>v$F7!^%*%7jKd*8t~?x3UCBe8VC5c91(Eht=vd)RuN`W(ZUG2-zH6) zqAcrdXc~We8n_UJsp5=RF5^bwu_}NN3b5WX!JC@IYv=dDTNd1%CzjW`FJf&NpvMbT zsZQ*qRu12I`O8_t$reVFQSQ@M=)G`S`a!$DELOV|^NWcXN5Ga;(631`n_cIP=*%e$ zK62CRPc^3YXXe*9Equ|JKQB`8T;>&u)ni!qeE}3gfzQk|Bz2I8sy#Rife)MjQiUze z8+}Wyui|Gf4CLiLG=1ZM*`22yHgC)g?>~N@YX9(5bzUF`w>KRqnB(uPd>2L zi^Rpyj3lq&1%%;E__l2WusuiV+rpdeN73yx*7ad5cJ|y`E~e--=v&9r(eX`0Q^(Bz z{L_V90-1(Jo*1;le2jeFlG&;p9rcy9o4SIsGmRPAXJibENmtm8^|d$H&1M35obKny z65>02WMmr!H_=nQatkzf3Ssp%pi7F9LVjnTZ{eLCO#(_D!3{-?pe8CO(E+jPk0)5) zww9(q4a;MwpmbyLhQa zou0qHmtI(F8@=eBOS%y@is=2ABJ>&&7a8f(DTi721qKokr`q`EAWXgjEbnvDdUMgm z(Ox49{mci*de*h=wma3| zyLU{wRkun-6Sk;Syccxu-#iC{X9-eV&hMhKoDIC z;svNDsRI}6Uu!<<3qTHNHsQn5a})uByt4Ld|Gu+qeg()BTZ`;Nv|=ECJ6Gs2cfY^_ z&nkWuo?QwN>*K*Bs3UNZUJM|}^2@_rU(CM}sCRyGkIJtmo~&~w)zgkhF-VAIv2 zcoc^SEPu^!EdW4Sg%=%p_dJuU$hTimK@q5L;XxyNTNtju3yKG>gy1gzOl(gILc@`g zR#KDUi^4a_DT{hblOUy35q&>S#)> z^5XS~uFXPA{yDI2)5xe0p-RG!gLK|`^$DO*3}baC)irO!Lw0=|G|bCCF=K${`OlOF zVirqPD{J+3ycSw2_^blTLJ#ap#P%2}h1edSexI&WSP`GIb>;-BQ_bqtMGME<2_b`n zv@7Cy3IaQE9Mw<$*1hESXSrKCt^w^3(k8`C2qb+T_)9Xq$OXhN6(W?lb#jo@WJMm= zsg-OQ2>2OJK`1ai5j2zTJ#wUJY5il@M;pEl@IR+S*ZLg~g6tDRF19AxyvTXDVb%FG ze9Pu_88tc*-Y1VAKkhqrmao<8QIE~uU3A~=td*0^3lb;- zXk@A7gIud_(cez^%brRs6>m&CiooZJ_loPQ>$Zrv*PCbv2uo?p z=y(05P4!?eKF_f^i$2fO|2iG?y%Td!IKYM?mf=P)AE+4y0ckjV^tjBGm@(L(1(51)v-Po?`s*L zbCnx;7Pgu322>K_>i~X(;6D}I)=v7zLOzC zV>L3^=+arFBR-wN=tfcjU5fmn6N%jhLv)mN)qVVF8-qslBgxA=LRO#h?K^09uQ_Vi zEKS(r*e^=OQ(UK8cjyp-`0)cL z#%NnzkdbY|W{cDbCNXZA>st`&a8wj(XAN%#mK~+r51#8Mx5b{EU{arSJLHU zD`&L{5|Eu0XGl{=q>k58kenxCNP+ewj*zwd?y&rBbH`sVfA20;>0jT30%qLkkm=gw z0P7Gb`N2(0o%~I?JFY-QK;|9~yxap`?Y%v1`8}<*;Xoi}m3^*MK5{-2(!!qaur(X3 z_17%}99g|ss^G#oWNa_RCpHm0K*8WoUxZW$52! z-iy5lpYIsC{}MKzOlF|jPC=fBR?{ABlI;qgzMN2+q(r}g zJR>wzx=6l|!~D~LbZ!r$picsceb3CbT3>L)Eutq+DAC<>(9WAU@VYC&0mxnucSET? zcfQi?RNx-431zY{_XX<vKSS z%8pCkr3B8kL-)4%e3BGggnRt3ci1DwaCaKnzhKOc2a)CNWz+xIv!mrpGMQKaGGJN0 z1RqbKO;CS`m8x)5NWJF8{o8yi`SIjJFJ)|+QYm0n?6HJu>i&-dR7<=T7Mm%^>0&0x z5CfQCNaHOf>N!YljS&dv98)R2{8qcj9cJ`G(zQS&S#Dyo zq0de!4S&8KtMZ6qmb4my3?`j1oo0>9KIAhiwD}5>R(vb z5k<6KEqH&pTQzUvz%x4)X`2Ez#IekW9wV9_;@-V` z;(rLQnr8F@&5u8LfYbvu2V9H}4-dj=0BT8}2Xl+_%(iXcE}Q*mS7=Yo9j@vuvLWQ4 z<+)KjPyE7hPr+9$Xy{&9fC{2Y|3ixs&%IR0npmI&1OZpbY^+o6lXS~yt)BUtQeK!h zJZviAL!>>l!z|s=ti*ZswIc>VBauE(98~e)tnQop-`o8J z)2U}@_`O4gW>F{`&%sNihNUX_2rg>0F9ib|is|;+W9Z^#odTnxXY^JZ#vHgkt(00L zhYCS=<+sO>Na(Lp`6g5;lMXH>+={T15NDo)*~UE<@9`l9UmsuCMn%Tm1qp76UeeowQ8+9F=3 z!D4YaI*Z8+G!%WxiU@eJcNhzt5gdRX)HRnlT>x{fx_3XFk^UD(t26U8g+12LCwv{C zPm(!tcpUs{5xaE7-Oy(Y7lPPhJ?hw6cvw6`eSKt+1x&hy;f?am@-=NJKDj}?OuXNe z($x^)8~xPf{+n@*s?u&y!2<#_$3g_&6d%%ZS-!?mXvel5>qA0B#4YYqR87(e7uE75 zpwhT&h?h`6;g@kDZ(ypZDLZhaPRc%Y_38lWegRnssGzXK$*e&MO+!M_a-RgG3CzdSiY}lJlQvBAYzL@R98bPDB$OUCo+%P1z%5BikaP ztVbk_z69VbTpF6*VMLJM1|Qqpd2wUk+-a^RgVLiHnSA(Qrs2Q#OAcdmDu!Cp&>yCz zkfZ=EYJBnx--#>LtgE9~DO1bKuJ7e_mnzU^PC0tt@rh2Ob$yKPX<*91we`XNoGw~@ zW)Gqm8E(Uj$rZ|s{7>-DrTF*r^78}F{3ps6fRXK>n9nx41Ew9y4X}DmA99TXE$HDg zcbJFOd{f>|gG@(JO`8xubZ_MH8+1T5J;U(+-{q0$`jaQ1kP(~fBhhX0QTL2KX~(6} z@ZD6*5B;+^{k!iQ_i~#6TZoTsrSKYHKh*@zld*Y6O>*yL+N{XwJC`h3qNAxUbu{x3 z#&Gqr&MZB3A}E8mc9oV1##wc4tlCqygEVJbu0I9Cko!#d}#WRy3^>h?><}PoSX_ z?K~&@#e?K9yC#Xop4|xK%7`6NsF|NW(uCZ-HnEjjS45F<(-e;mAB$ zPm}#eqt%1@h9xX>K7#HAu)8e(@-+%(AXx@kNY=L5H9?nW$p4@Iy+CLAWhxd!e4-ZL z6u6+<_5+{C2Ab;wj7(rlP!S!p}=K-O539mvMX-mApE`a6RzD67O?%i9`bOuKgPHxd*hxgqI z8D4QNTm}y8lX4d;?Dr69DVln;)sr3A$s-Q8qVn-vdSm~X(=xPzxH}L%fs4jw6)@Uv~6H6yfr@)~1@dS;a=x9Uo*9dI-cxq2MHsXLr z!Z-PLNS%B#a9fuk@-O6dM%jAiKhJOw#JicEg_*@h@)pCdh@0D~dM6PF>D{S}S7Giz zVr7&d>n;KvX zk=vr9_bj&P&QOsOlAc%XWbz?!1j0MRpD1FI)dMb)Z;-`r5ah2QzzKTz`z;C%E%hT( z?(9_OA?X;bShHcnWwZxVmz!BlQ*BaaoS=7NTlB;~CuxuSN?