From a6db8678eb5a7c9ebcf6449799b4ff525fe52d5f Mon Sep 17 00:00:00 2001 From: Ulf Lilleengen Date: Wed, 4 Sep 2024 11:04:36 +0200 Subject: [PATCH] Add utility for setting configuration for a context --- embassy-net-nrf91/src/at.rs | 45 ----- embassy-net-nrf91/src/context.rs | 196 +++++++++++++++++++ embassy-net-nrf91/src/lib.rs | 2 + examples/nrf9160/src/bin/modem_tcp_client.rs | 64 +++--- 4 files changed, 220 insertions(+), 87 deletions(-) delete mode 100644 embassy-net-nrf91/src/at.rs create mode 100644 embassy-net-nrf91/src/context.rs diff --git a/embassy-net-nrf91/src/at.rs b/embassy-net-nrf91/src/at.rs deleted file mode 100644 index 04cbf3876..000000000 --- a/embassy-net-nrf91/src/at.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::{Error, Control}; - -// Drives the control loop of the modem based on declarative configuration. -pub struct AtDriver<'a> { - control: Control<'a>, - config: Config, -} - -pub struct Config { - pub network: NetworkConfig, -} - -pub struct NetworkConfig { - pub apn: &'static str, - pub prot: AuthProtection, - pub userid: &'static str, - pub password: &'static str, -} - -#[repr(u8)] -pub enum AuthProtection { - None = 0, - Pap = 1, - Chap = 2, -} - -impl<'a> AtDriver<'a> { - pub async fn new(control: Control<'a>, config: Config) -> Result { - control.wait_init().await; - Ok(Self { - control, - config, - }) - } - - async fn setup(&self) -> Result<(), Error> { - - } - - pub fn run(&self, stack: Stack>) -> ! { - loop { - - } - } -} diff --git a/embassy-net-nrf91/src/context.rs b/embassy-net-nrf91/src/context.rs new file mode 100644 index 000000000..6dda51793 --- /dev/null +++ b/embassy-net-nrf91/src/context.rs @@ -0,0 +1,196 @@ +use core::net::IpAddr; +use heapless::String; +use core::str::FromStr; +use core::fmt::Write; + +/// Provides a higher level API for configuring and reading information for a given +/// context id. +pub struct Control<'a> { + control: crate::Control<'a>, + cid: u8, +} + +pub struct Config<'a> { + pub gateway: &'a str, + pub auth_prot: AuthProt, + pub auth: Option<(&'a str, &'a str)>, +} + +#[repr(u8)] +pub enum AuthProt { + None = 0, + Pap = 1, + Chap = 2, +} + +#[derive(Clone, Copy, PartialEq, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Error { + BufferTooSmall, + AtCommand, + AddrParseError, + Format, +} + +impl From for Error { + fn from(_: core::fmt::Error) -> Self { + Self::Format + } +} + +#[derive(PartialEq, Debug)] +pub struct Status { + pub attached: bool, + pub ip: Option, +} + +#[cfg(feature = "defmt")] +impl defmt::Format for Status { + fn format(&self, f: defmt::Formatter<'_>) { + defmt::write!(f, "attached: {}", self.attached); + if let Some(ip) = &self.ip { + defmt::write!(f, ", ip: {}", defmt::Debug2Format(&ip)); + } + } +} + +impl<'a> Control<'a> { + pub async fn new(control: crate::Control<'a>, cid: u8) -> Self { + control.wait_init().await; + Self { control, cid } + } + + /// Bypass modem configurator + pub async fn at_command(&self, req: &[u8], resp: &mut [u8]) -> usize { + self.control.at_command(req, resp).await + } + + /// Configures the modem with the provided config. + pub async fn configure(&self, config: Config<'_>) -> Result<(), Error> { + let mut cmd: String<128> = String::new(); + let mut buf: [u8; 256] = [0; 256]; + + write!(cmd, "AT+CGDCONT={},\"IP\",\"{}\"", self.cid, config.gateway).map_err(|_| Error::BufferTooSmall)?; + let n = self.control.at_command(cmd.as_bytes(), &mut buf).await; + let mut res = &buf[..n]; + let res = split_field(&mut res); + if res != b"OK" { + return Err(Error::AtCommand) + } + cmd.clear(); + + write!(cmd, "AT+CGAUTH={},{}", self.cid, config.auth_prot as u8)?; + if let Some((username, password)) = config.auth { + write!(cmd, ",\"{}\",\"{}\"", username, password).map_err(|_| Error::BufferTooSmall)?; + } + let n = self.control.at_command(cmd.as_bytes(), &mut buf).await; + let mut res = &buf[..n]; + let res = split_field(&mut res); + if res != b"OK" { + return Err(Error::AtCommand) + } + cmd.clear(); + + let n = self.control.at_command(b"AT+CFUN=1", &mut buf).await; + let mut res = &buf[..n]; + let res = split_field(&mut res); + if res != b"OK" { + return Err(Error::AtCommand); + } + + Ok(()) + } + + pub async fn status(&self) -> Result { + let mut buf: [u8; 256] = [0; 256]; + let n = self.control.at_command(b"AT+CGATT?", &mut buf).await; + let mut res = &buf[..n]; + pop_prefix(&mut res, b"+CGATT: "); + let res = split_field(&mut res); + let attached = res == b"1"; + + if !attached { + return Ok(Status { attached, ip: None }) + } + + let mut s: String<128> = String::new(); + write!(s, "AT+CGPADDR={}", self.cid)?; + let n = self.control.at_command(s.as_bytes(), &mut buf).await; + let mut res = &buf[..n]; + s.clear(); + + write!(s, "+CGPADDR: {},", self.cid)?; + + info!("RES: {:?}", unsafe {core::str::from_utf8_unchecked(res)}); + if s.len() > res.len() { + let res = split_field(&mut res); + if res == b"OK" { + Ok(Status { attached, ip: None }) + } else { + Err(Error::AtCommand) + } + } else { + pop_prefix(&mut res, s.as_bytes()); + + let ip = split_field(&mut res); + if !ip.is_empty() { + let ip = IpAddr::from_str(unsafe { core::str::from_utf8_unchecked(ip) }).map_err(|_| Error::AddrParseError)?; + self.control.open_raw_socket().await; + Ok(Status { attached, ip: Some(ip) }) + } else { + Ok(Status { attached, ip: None }) + } + } + } +} + +pub(crate) fn is_whitespace(char: u8) -> bool { + match char { + b'\r' | b'\n' | b' ' => true, + _ => false, + } +} + +pub(crate) fn is_separator(char: u8) -> bool { + match char { + b',' | b'\r' | b'\n' | b' ' => true, + _ => false, + } +} + +pub(crate) fn split_field<'a>(data: &mut &'a [u8]) -> &'a [u8] { + while !data.is_empty() && is_whitespace(data[0]) { + *data = &data[1..]; + } + + if data.is_empty() { + return &[]; + } + + if data[0] == b'"' { + let data2 = &data[1..]; + let end = data2.iter().position(|&x| x == b'"').unwrap_or(data2.len()); + let field = &data2[..end]; + let mut rest = &data2[data2.len().min(end + 1)..]; + if rest.first() == Some(&b'\"') { + rest = &rest[1..]; + } + while !rest.is_empty() && is_separator(rest[0]) { + rest = &rest[1..]; + } + *data = rest; + field + } else { + let end = data.iter().position(|&x| is_separator(x)).unwrap_or(data.len()); + let field = &data[0..end]; + let rest = &data[data.len().min(end + 1)..]; + *data = rest; + field + } +} + +pub(crate) fn pop_prefix(data: &mut &[u8], prefix: &[u8]) { + assert!(data.len() >= prefix.len()); + assert!(&data[..prefix.len()] == prefix); + *data = &data[prefix.len()..]; +} diff --git a/embassy-net-nrf91/src/lib.rs b/embassy-net-nrf91/src/lib.rs index 70ad176da..beed7c65a 100644 --- a/embassy-net-nrf91/src/lib.rs +++ b/embassy-net-nrf91/src/lib.rs @@ -6,6 +6,8 @@ // must be first mod fmt; +pub mod context; + use core::cell::RefCell; use core::future::poll_fn; use core::marker::PhantomData; diff --git a/examples/nrf9160/src/bin/modem_tcp_client.rs b/examples/nrf9160/src/bin/modem_tcp_client.rs index b1dac18a1..817ad17c7 100644 --- a/examples/nrf9160/src/bin/modem_tcp_client.rs +++ b/examples/nrf9160/src/bin/modem_tcp_client.rs @@ -2,14 +2,15 @@ #![no_main] use core::mem::MaybeUninit; +use core::net::IpAddr; use core::ptr::addr_of_mut; use core::str::FromStr; use core::{slice, str}; -use defmt::{assert, *}; +use defmt::{assert, info, warn, unwrap}; use embassy_executor::Spawner; use embassy_net::{Ipv4Address, Ipv4Cidr, Stack, StackResources}; -use embassy_net_nrf91::{Runner, State}; +use embassy_net_nrf91::{Runner, State, context}; use embassy_nrf::buffered_uarte::{self, BufferedUarteTx}; use embassy_nrf::gpio::{AnyPin, Level, Output, OutputDrive, Pin}; use embassy_nrf::uarte::Baudrate; @@ -63,9 +64,9 @@ async fn blink_task(pin: AnyPin) { let mut led = Output::new(pin, Level::Low, OutputDrive::Standard); loop { led.set_high(); - Timer::after_millis(100).await; + Timer::after_millis(1000).await; led.set_low(); - Timer::after_millis(100).await; + Timer::after_millis(1000).await; } } @@ -123,51 +124,30 @@ async fn main(spawner: Spawner) { unwrap!(spawner.spawn(net_task(stack))); - control.wait_init().await; - info!("INIT OK"); + let control = context::Control::new(control, 0).await; - let mut buf = [0u8; 256]; - - let n = control.at_command(b"AT+CFUN?", &mut buf).await; - info!("AT resp: '{}'", unsafe { str::from_utf8_unchecked(&buf[..n]) }); - - let n = control - .at_command(b"AT+CGDCONT=0,\"IP\",\"iot.nat.es\"", &mut buf) - .await; - info!("AT resp: '{}'", unsafe { str::from_utf8_unchecked(&buf[..n]) }); - let n = control - .at_command(b"AT+CGAUTH=0,1,\"orange\",\"orange\"", &mut buf) - .await; - info!("AT resp: '{}'", unsafe { str::from_utf8_unchecked(&buf[..n]) }); - - let n = control.at_command(b"AT+CFUN=1", &mut buf).await; - info!("AT resp: '{}'", unsafe { str::from_utf8_unchecked(&buf[..n]) }); + unwrap!(control.configure(context::Config { + gateway: "iot.nat.es", + auth_prot: context::AuthProt::Pap, + auth: Some(("orange", "orange")), + }).await); info!("waiting for attach..."); - loop { - Timer::after_millis(500).await; - let n = control.at_command(b"AT+CGATT?", &mut buf).await; - let mut res = &buf[..n]; - pop_prefix(&mut res, b"+CGATT: "); - let res = split_field(&mut res); - info!("AT resp field: '{}'", unsafe { str::from_utf8_unchecked(res) }); - if res == b"1" { - break; - } + + let mut status = unwrap!(control.status().await); + while !status.attached && status.ip.is_none() { + Timer::after_millis(1000).await; + status = unwrap!(control.status().await); + info!("STATUS: {:?}", status); } - let n = control.at_command(b"AT+CGPADDR=0", &mut buf).await; - let mut res = &buf[..n]; - pop_prefix(&mut res, b"+CGPADDR: 0,"); - let ip = split_field(&mut res); - let ip = Ipv4Address::from_str(unsafe { str::from_utf8_unchecked(ip) }).unwrap(); - info!("IP: '{}'", ip); - - info!("============== OPENING SOCKET"); - control.open_raw_socket().await; + let Some(IpAddr::V4(addr)) = status.ip else { + panic!("Unexpected IP address"); + }; + let addr = Ipv4Address(addr.octets()); stack.set_config_v4(embassy_net::ConfigV4::Static(embassy_net::StaticConfigV4 { - address: Ipv4Cidr::new(ip, 32), + address: Ipv4Cidr::new(addr, 32), gateway: None, dns_servers: Default::default(), }));