diff --git a/cross/Cargo.toml b/cross/Cargo.toml index 8d25fba..6d78c56 100644 --- a/cross/Cargo.toml +++ b/cross/Cargo.toml @@ -2,6 +2,7 @@ members = [ "get_fw_version", "join", + "dns" ] [profile.dev] diff --git a/cross/dns/Cargo.toml b/cross/dns/Cargo.toml new file mode 100644 index 0000000..c17ca5c --- /dev/null +++ b/cross/dns/Cargo.toml @@ -0,0 +1,40 @@ +[package] +authors = [ + "Jim Hodapp", + "Caleb Bourg", + "Glyn Matthews" +] +edition = "2021" +name = "dns" +version = "0.1.0" +description = "Example target application that demonstrates DNS functionality with the Rust-based Espressif ESP32-WROOM WiFi driver crate for RP2040 series microcontroller boards." + +# makes `cargo check --all-targets` work +[[bin]] +name = "dns" +bench = false +doctest = false +test = false + +[dependencies] +defmt = "0.3.0" +defmt-rtt = "0.3.0" +cortex-m = "0.7" +cortex-m-rt = "0.7" +embedded-hal = { version = "0.2", features=["unproven"] } +esp32-wroom-rp = { path = "../../esp32-wroom-rp" } +panic-probe = { version = "0.3.0", features = ["print-rtt"] } + +rp2040-hal = { version = "0.6", features=["rt", "eh1_0_alpha"] } +rp2040-boot2 = { version = "0.2" } +fugit = "0.3" + +[features] +default = ['defmt-default'] +# these features are required by defmt +defmt-default = [] +defmt-trace = [] +defmt-debug = [] +defmt-info = [] +defmt-warn = [] +defmt-error = [] diff --git a/cross/dns/src/main.rs b/cross/dns/src/main.rs new file mode 100644 index 0000000..ec05dda --- /dev/null +++ b/cross/dns/src/main.rs @@ -0,0 +1,158 @@ +//! # ESP32-WROOM-RP Pico Wireless Example +//! +//! This application demonstrates how to use the ESP32-WROOM-RP crate to perform +//! a DNS hostname lookup after setting what DNS server to use. +//! +//! See the `Cargo.toml` file for Copyright and license details. + +#![no_std] +#![no_main] + +extern crate esp32_wroom_rp; + +include!("secrets/secrets.rs"); + +// The macro for our start-up function +use cortex_m_rt::entry; + +// Needed for debug output symbols to be linked in binary image +use defmt_rtt as _; + +use panic_probe as _; + +// Alias for our HAL crate +use rp2040_hal as hal; + +use embedded_hal::spi::MODE_0; +use fugit::RateExtU32; +use hal::clocks::Clock; +use hal::pac; + +use esp32_wroom_rp::network::IpAddress; + +/// The linker will place this boot block at the start of our program image. We +/// need this to help the ROM bootloader get our code up and running. +#[link_section = ".boot2"] +#[used] +pub static BOOT2: [u8; 256] = rp2040_boot2::BOOT_LOADER_W25Q080; + +/// External high-speed crystal on the Raspberry Pi Pico board is 12 MHz. Adjust +/// if your board has a different frequency +const XTAL_FREQ_HZ: u32 = 12_000_000u32; + +/// Entry point to our bare-metal application. +/// +/// The `#[entry]` macro ensures the Cortex-M start-up code calls this function +/// as soon as all global variables are initialized. +#[entry] +fn main() -> ! { + // Grab our singleton objects + let mut pac = pac::Peripherals::take().unwrap(); + let core = pac::CorePeripherals::take().unwrap(); + + // Set up the watchdog driver - needed by the clock setup code + let mut watchdog = hal::Watchdog::new(pac.WATCHDOG); + + // Configure the clocks + let clocks = hal::clocks::init_clocks_and_plls( + XTAL_FREQ_HZ, + pac.XOSC, + pac.CLOCKS, + pac.PLL_SYS, + pac.PLL_USB, + &mut pac.RESETS, + &mut watchdog, + ) + .ok() + .unwrap(); + + let mut delay = cortex_m::delay::Delay::new(core.SYST, clocks.system_clock.freq().to_Hz()); + + // The single-cycle I/O block controls our GPIO pins + let sio = hal::Sio::new(pac.SIO); + + // Set the pins to their default state + let pins = hal::gpio::Pins::new( + pac.IO_BANK0, + pac.PADS_BANK0, + sio.gpio_bank0, + &mut pac.RESETS, + ); + + defmt::info!("ESP32-WROOM-RP DNS resolve example"); + + // These are implicitly used by the spi driver if they are in the correct mode + let _spi_miso = pins.gpio16.into_mode::(); + let _spi_sclk = pins.gpio18.into_mode::(); + let _spi_mosi = pins.gpio19.into_mode::(); + + let spi = hal::Spi::<_, _, 8>::new(pac.SPI0); + + // Exchange the uninitialized SPI driver for an initialized one + let mut spi = spi.init( + &mut pac.RESETS, + clocks.peripheral_clock.freq(), + 8.MHz(), + &MODE_0, + ); + + let mut esp_pins = esp32_wroom_rp::gpio::EspControlPins { + // CS on pin x (GPIO7) + cs: pins.gpio7.into_mode::(), + // GPIO0 on pin x (GPIO2) + gpio0: pins.gpio2.into_mode::(), + // RESETn on pin x (GPIO11) + resetn: pins.gpio11.into_mode::(), + // ACK on pin x (GPIO10) + ack: pins.gpio10.into_mode::(), + }; + + let mut wifi = esp32_wroom_rp::wifi::Wifi::init(&mut spi, &mut esp_pins, &mut delay).unwrap(); + + let result = wifi.join(SSID, PASSPHRASE); + defmt::info!("Join Result: {:?}", result); + + defmt::info!("Entering main loop"); + + loop { + match wifi.get_connection_status() { + Ok(byte) => { + defmt::info!("Get Connection Result: {:?}", byte); + let sleep: u32 = 1500; + delay.delay_ms(sleep); + + if byte == 3 { + defmt::info!("Connected to Network: {:?}", SSID); + + // The IPAddresses of two DNS servers to resolve hostnames with. + // Note that failover from ip1 to ip2 is fully functional. + let ip1: IpAddress = [9, 9, 9, 9]; + let ip2: IpAddress = [8, 8, 8, 8]; + let dns_result = wifi.set_dns(ip1, Some(ip2)); + + defmt::info!("set_dns result: {:?}", dns_result); + + let hostname = "github.com"; + defmt::info!("Doing a DNS resolve for {}", hostname); + + match wifi.resolve(hostname) { + Ok(ip) => { + defmt::info!("Server IP: {:?}", ip); + } + Err(e) => { + defmt::error!("Failed to resolve hostname {}", hostname); + defmt::error!("Err: {}", e); + } + } + + wifi.leave().ok().unwrap(); + } else if byte == 6 { + defmt::info!("Disconnected from Network: {:?}", SSID); + } + } + Err(e) => { + defmt::info!("Failed to Get Connection Result: {:?}", e); + } + } + } +} diff --git a/cross/dns/src/secrets/secrets.rs b/cross/dns/src/secrets/secrets.rs new file mode 100644 index 0000000..d4bd861 --- /dev/null +++ b/cross/dns/src/secrets/secrets.rs @@ -0,0 +1,7 @@ +// +// secrets.rs - stores WiFi secrets like SSID, passphrase, etc shared across +// all example applications +// + +const SSID: &str = "ssid"; +const PASSPHRASE: &str = "passphrase"; diff --git a/esp32-wroom-rp/src/lib.rs b/esp32-wroom-rp/src/lib.rs index 460d266..8179355 100644 --- a/esp32-wroom-rp/src/lib.rs +++ b/esp32-wroom-rp/src/lib.rs @@ -88,9 +88,14 @@ pub mod gpio; /// Fundamental interface for controlling a connected ESP32-WROOM NINA firmware-based Wifi board. pub mod wifi; +/// Responsible for interactions over a WiFi network and also contains related types. +pub mod network; +/// Responsible for interactions with NINA firmware over a data bus. pub mod protocol; + mod spi; +use network::{IpAddress, NetworkError}; use protocol::{ProtocolError, ProtocolInterface}; use defmt::{write, Format, Formatter}; @@ -105,6 +110,9 @@ pub enum Error { Bus, /// Protocol error in communicating with the ESP32 WiFi target Protocol(ProtocolError), + + /// Network related error + Network(NetworkError), } impl Format for Error { @@ -116,6 +124,7 @@ impl Format for Error { "Communication protocol error with ESP32 WiFi target: {}", e ), + Error::Network(e) => write!(fmt, "Network error: {}", e), } } } @@ -126,6 +135,12 @@ impl From for Error { } } +impl From for Error { + fn from(err: network::NetworkError) -> Self { + Error::Network(err) + } +} + /// A structured representation of a connected NINA firmware device's version number (e.g. 1.7.4). #[derive(Debug, Default, Eq, PartialEq)] pub struct FirmwareVersion { @@ -184,19 +199,27 @@ where } fn firmware_version(&mut self) -> Result { - Ok(self.protocol_handler.get_fw_version()?) + self.protocol_handler.get_fw_version() } fn join(&mut self, ssid: &str, passphrase: &str) -> Result<(), Error> { - Ok(self.protocol_handler.set_passphrase(ssid, passphrase)?) + self.protocol_handler.set_passphrase(ssid, passphrase) } fn leave(&mut self) -> Result<(), Error> { - Ok(self.protocol_handler.disconnect()?) + self.protocol_handler.disconnect() } fn get_connection_status(&mut self) -> Result { - Ok(self.protocol_handler.get_conn_status()?) + self.protocol_handler.get_conn_status() + } + + fn set_dns(&mut self, dns1: IpAddress, dns2: Option) -> Result<(), Error> { + self.protocol_handler.set_dns_config(dns1, dns2) + } + + fn resolve(&mut self, hostname: &str) -> Result { + self.protocol_handler.resolve(hostname) } } diff --git a/esp32-wroom-rp/src/network.rs b/esp32-wroom-rp/src/network.rs new file mode 100644 index 0000000..bee39c4 --- /dev/null +++ b/esp32-wroom-rp/src/network.rs @@ -0,0 +1,22 @@ +use defmt::{write, Format, Formatter}; + +/// A four byte array type alias representing an IP address. +pub type IpAddress = [u8; 4]; + +/// Errors that occur due to issues involving communication over +/// WiFi network. +#[derive(PartialEq, Eq, Debug)] +pub enum NetworkError { + /// Failed to resolve a hostname for the provided IP address. + DnsResolveFailed, +} + +impl Format for NetworkError { + fn format(&self, fmt: Formatter) { + match self { + NetworkError::DnsResolveFailed => { + write!(fmt, "Failed to resolve a hostname for the provided IP address") + } + } + } +} diff --git a/esp32-wroom-rp/src/protocol.rs b/esp32-wroom-rp/src/protocol.rs index 4f44bf0..07f3c98 100644 --- a/esp32-wroom-rp/src/protocol.rs +++ b/esp32-wroom-rp/src/protocol.rs @@ -17,6 +17,9 @@ pub(crate) enum NinaCommand { SetPassphrase = 0x11u8, GetConnStatus = 0x20u8, Disconnect = 0x30u8, + SetDNSConfig = 0x15u8, + ReqHostByName = 0x34u8, + GetHostByName = 0x35u8, } pub(crate) trait NinaParam { @@ -225,10 +228,14 @@ impl NinaParam for NinaLargeArrayParam { pub(crate) trait ProtocolInterface { fn init(&mut self); fn reset>(&mut self, delay: &mut D); - fn get_fw_version(&mut self) -> Result; - fn set_passphrase(&mut self, ssid: &str, passphrase: &str) -> Result<(), ProtocolError>; - fn disconnect(&mut self) -> Result<(), ProtocolError>; - fn get_conn_status(&mut self) -> Result; + fn get_fw_version(&mut self) -> Result; + fn set_passphrase(&mut self, ssid: &str, passphrase: &str) -> Result<(), Error>; + fn disconnect(&mut self) -> Result<(), Error>; + fn get_conn_status(&mut self) -> Result; + fn set_dns_config(&mut self, dns1: IpAddress, dns2: Option) -> Result<(), Error>; + fn req_host_by_name(&mut self, hostname: &str) -> Result; + fn get_host_by_name(&mut self) -> Result<[u8; 8], Error>; + fn resolve(&mut self, hostname: &str) -> Result; } #[derive(Debug)] @@ -241,12 +248,19 @@ pub(crate) struct NinaProtocolHandler<'a, B, C> { // TODO: look at Nina Firmware code to understand conditions // that lead to NinaProtocolVersionMismatch +/// Errors related to communication with NINA firmware #[derive(Debug, Eq, PartialEq)] pub enum ProtocolError { + /// TODO: look at Nina Firmware code to understand conditions + /// that lead to NinaProtocolVersionMismatch NinaProtocolVersionMismatch, + /// A timeout occurred. CommunicationTimeout, + /// An invalid NINA command has been sent over the data bus. InvalidCommand, + /// An invalid number of parameters sent over the data bus. InvalidNumberOfParameters, + /// Too many parameters sent over the data bus. TooManyParameters, } @@ -257,7 +271,7 @@ impl Format for ProtocolError { ProtocolError::CommunicationTimeout => write!(fmt, "Communication with ESP32 target timed out."), ProtocolError::InvalidCommand => write!(fmt, "Encountered an invalid command while communicating with ESP32 target."), ProtocolError::InvalidNumberOfParameters => write!(fmt, "Encountered an unexpected number of parameters for a NINA command while communicating with ESP32 target."), - ProtocolError::TooManyParameters => write!(fmt, "Encountered too many parameters for a NINA command while communicating with ESP32 target."), + ProtocolError::TooManyParameters => write!(fmt, "Encountered too many parameters for a NINA command while communicating with ESP32 target.") } } } diff --git a/esp32-wroom-rp/src/spi.rs b/esp32-wroom-rp/src/spi.rs index 673336b..a67bfa9 100644 --- a/esp32-wroom-rp/src/spi.rs +++ b/esp32-wroom-rp/src/spi.rs @@ -6,10 +6,13 @@ use super::protocol::{ ProtocolInterface, }; +use super::network::NetworkError; use super::protocol::operation::Operation; use super::protocol::ProtocolError; use super::{Error, FirmwareVersion, WifiCommon, ARRAY_LENGTH_PLACEHOLDER}; +use super::IpAddress; + use embedded_hal::blocking::delay::DelayMs; use embedded_hal::blocking::spi::Transfer; @@ -82,6 +85,16 @@ where pub fn get_connection_status(&mut self) -> Result { self.common.get_connection_status() } + + /// Sets 1 or 2 DNS servers that are used for network hostname resolution. + pub fn set_dns(&mut self, dns1: IpAddress, dns2: Option) -> Result<(), Error> { + self.common.set_dns(dns1, dns2) + } + + /// Queries the DNS server(s) provided via [set_dns] for the associated IP address to the provided hostname. + pub fn resolve(&mut self, hostname: &str) -> Result { + self.common.resolve(hostname) + } } // All SPI-specific aspects of the NinaProtocolHandler go here in this struct impl @@ -99,7 +112,7 @@ where self.control_pins.reset(delay); } - fn get_fw_version(&mut self) -> Result { + fn get_fw_version(&mut self) -> Result { // TODO: improve the ergonomics around with_no_params() let operation = Operation::new(NinaCommand::GetFwVersion, 1).with_no_params(NinaNoParams::new("")); @@ -111,7 +124,7 @@ where Ok(FirmwareVersion::new(result)) // e.g. 1.7.4 } - fn set_passphrase(&mut self, ssid: &str, passphrase: &str) -> Result<(), ProtocolError> { + fn set_passphrase(&mut self, ssid: &str, passphrase: &str) -> Result<(), Error> { let operation = Operation::new(NinaCommand::SetPassphrase, 1) .param(NinaSmallArrayParam::new(ssid)) .param(NinaSmallArrayParam::new(passphrase)); @@ -122,7 +135,7 @@ where Ok(()) } - fn get_conn_status(&mut self) -> Result { + fn get_conn_status(&mut self) -> Result { let operation = Operation::new(NinaCommand::GetConnStatus, 1).with_no_params(NinaNoParams::new("")); @@ -133,7 +146,7 @@ where Ok(result[0]) } - fn disconnect(&mut self) -> Result<(), ProtocolError> { + fn disconnect(&mut self) -> Result<(), Error> { let dummy_param = NinaByteParam::from_bytes(&[ControlByte::Dummy as u8]); let operation = Operation::new(NinaCommand::Disconnect, 1).param(dummy_param); @@ -143,6 +156,65 @@ where Ok(()) } + + fn set_dns_config(&mut self, ip1: IpAddress, ip2: Option) -> Result<(), Error> { + // FIXME: refactor Operation so it can take different NinaParam types + let operation = Operation::new(NinaCommand::SetDNSConfig, 1) + // FIXME: first param should be able to be a NinaByteParam: + .param(NinaSmallArrayParam::from_bytes(&[1])) + .param(NinaSmallArrayParam::from_bytes(&ip1)) + .param(NinaSmallArrayParam::from_bytes(&ip2.unwrap_or_default())); + + self.execute(&operation)?; + + self.receive(&operation)?; + + Ok(()) + } + + fn req_host_by_name(&mut self, hostname: &str) -> Result { + let operation = + Operation::new(NinaCommand::ReqHostByName, 1).param(NinaSmallArrayParam::new(hostname)); + + self.execute(&operation)?; + + let result = self.receive(&operation)?; + + if result[0] != 1u8 { + return Err(NetworkError::DnsResolveFailed.into()); + } + + Ok(result[0]) + } + + fn get_host_by_name(&mut self) -> Result<[u8; 8], Error> { + let operation = + Operation::new(NinaCommand::GetHostByName, 1).with_no_params(NinaNoParams::new("")); + + self.execute(&operation)?; + + let result = self.receive(&operation)?; + + Ok(result) + } + + fn resolve(&mut self, hostname: &str) -> Result { + self.req_host_by_name(hostname)?; + + let dummy: IpAddress = [255, 255, 255, 255]; + + let result = self.get_host_by_name()?; + + let (ip_slice, _) = result.split_at(4); + let mut ip_address: IpAddress = [0; 4]; + ip_address.clone_from_slice(ip_slice); + + if ip_address != dummy { + Ok(ip_address) + } else { + Err(NetworkError::DnsResolveFailed.into()) + } + } } impl<'a, S, C> NinaProtocolHandler<'a, S, C> @@ -150,7 +222,7 @@ where S: Transfer, C: EspControlInterface, { - fn execute(&mut self, operation: &Operation

) -> Result<(), ProtocolError> { + fn execute(&mut self, operation: &Operation

) -> Result<(), Error> { let mut param_size: u16 = 0; self.control_pins.wait_for_esp_select(); let number_of_params: u8 = if operation.has_params { @@ -170,8 +242,9 @@ where self.send_end_cmd().ok(); // This is to make sure we align correctly - // 4 (start byte, command byte, reply byte, end byte) + the sum of all param lengths - let command_size: u16 = 4u16 + param_size; + // 4 (start byte, command byte, number of params, end byte) + 1 byte for each param + the sum of all param lengths + // See https://github.com/arduino/nina-fw/blob/master/main/CommandHandler.cpp#L2153 for the actual equation. + let command_size: u16 = 4u16 + number_of_params as u16 + param_size; self.pad_to_multiple_of_4(command_size); } self.control_pins.esp_deselect(); @@ -182,7 +255,7 @@ where fn receive( &mut self, operation: &Operation

, - ) -> Result<[u8; ARRAY_LENGTH_PLACEHOLDER], ProtocolError> { + ) -> Result<[u8; ARRAY_LENGTH_PLACEHOLDER], Error> { self.control_pins.wait_for_esp_select(); let result = @@ -193,7 +266,7 @@ where result } - fn send_cmd(&mut self, cmd: &NinaCommand, num_params: u8) -> Result<(), ProtocolError> { + fn send_cmd(&mut self, cmd: &NinaCommand, num_params: u8) -> Result<(), Error> { let buf: [u8; 3] = [ ControlByte::Start as u8, (*cmd as u8) & !(ControlByte::Reply as u8), @@ -215,26 +288,26 @@ where &mut self, cmd: &NinaCommand, num_params: u8, - ) -> Result<[u8; ARRAY_LENGTH_PLACEHOLDER], ProtocolError> { + ) -> Result<[u8; ARRAY_LENGTH_PLACEHOLDER], Error> { self.check_start_cmd()?; let byte_to_check: u8 = *cmd as u8 | ControlByte::Reply as u8; let result = self.read_and_check_byte(&byte_to_check).ok().unwrap(); // Ensure we see a cmd byte if !result { - return Err(ProtocolError::InvalidCommand); + return Err(ProtocolError::InvalidCommand.into()); } let result = self.read_and_check_byte(&num_params).unwrap(); // Ensure we see the number of params we expected to receive back if !result { - return Err(ProtocolError::InvalidNumberOfParameters); + return Err(ProtocolError::InvalidNumberOfParameters.into()); } let num_params_to_read = self.get_byte().ok().unwrap() as usize; // TODO: use a constant instead of inline params max == 8 if num_params_to_read > 8 { - return Err(ProtocolError::TooManyParameters); + return Err(ProtocolError::TooManyParameters.into()); } let mut params: [u8; ARRAY_LENGTH_PLACEHOLDER] = [0; 8]; @@ -259,21 +332,21 @@ where Ok(word[0] as u8) } - fn wait_for_byte(&mut self, wait_byte: u8) -> Result { + fn wait_for_byte(&mut self, wait_byte: u8) -> Result { let retry_limit: u16 = 1000u16; for _ in 0..retry_limit { let byte_read = self.get_byte().ok().unwrap(); if byte_read == ControlByte::Error as u8 { - return Err(ProtocolError::NinaProtocolVersionMismatch); + return Err(ProtocolError::NinaProtocolVersionMismatch.into()); } else if byte_read == wait_byte { return Ok(true); } } - Err(ProtocolError::CommunicationTimeout) + Err(ProtocolError::CommunicationTimeout.into()) } - fn check_start_cmd(&mut self) -> Result { + fn check_start_cmd(&mut self) -> Result { self.wait_for_byte(ControlByte::Start as u8) } @@ -299,7 +372,7 @@ where } fn pad_to_multiple_of_4(&mut self, mut command_size: u16) { - while command_size % 4 == 0 { + while command_size % 4 != 0 { self.get_byte().ok(); command_size += 1; }