Skip to main content

wifiauto_custom/
wifiauto_custom.rs

1#![allow(missing_docs)]
2//! WiFi auto-provisioning demo with LED display showing Unix time.
3
4#![no_std]
5#![no_main]
6#![cfg(feature = "wifi")]
7#![allow(clippy::future_not_send, reason = "single-threaded")]
8
9use core::{convert::Infallible, panic};
10use defmt::{info, warn};
11use device_envoy::{
12    Result,
13    button::PressedTo,
14    flash_array::FlashArray,
15    led_strip::colors,
16    led2d,
17    led2d::{Led2dFont, layout::LedLayout},
18    wifi_auto::fields::{TextField, TextFieldStatic},
19    wifi_auto::{WifiAuto, WifiAutoEvent},
20};
21use embassy_executor::Spawner;
22use embassy_net::{
23    Stack,
24    dns::DnsQueryType,
25    udp::{PacketMetadata, UdpSocket},
26};
27use embassy_time::{Duration, Timer};
28use {defmt_rtt as _, panic_probe as _};
29
30// Set up LED layout for 12x8 panel (two 12x4 panels stacked)
31const LED_LAYOUT_12X4: LedLayout<48, 12, 4> = LedLayout::serpentine_column_major();
32const LED_LAYOUT_12X8: LedLayout<96, 12, 8> = LED_LAYOUT_12X4.combine_v(LED_LAYOUT_12X4);
33
34// Color palette for display
35const COLORS: &[smart_leds::RGB8] = &[
36    colors::YELLOW,
37    colors::LIME,
38    colors::CYAN,
39    colors::RED,
40    colors::WHITE,
41];
42
43led2d! {
44    Led12x8 {
45        pin: PIN_4,
46        pio: PIO1,
47        dma: DMA_CH1,
48        led_layout: LED_LAYOUT_12X8,
49        font: Led2dFont::Font3x4Trim,
50    }
51}
52
53#[embassy_executor::main]
54async fn main(spawner: Spawner) -> ! {
55    let err = inner_main(spawner).await.unwrap_err();
56    panic!("{err}");
57}
58
59async fn inner_main(spawner: Spawner) -> Result<Infallible> {
60    info!("WiFi Auto LED Display - Starting");
61    let p = embassy_rp::init(Default::default());
62
63    // Set up flash storage for WiFi credentials and device name
64    let [wifi_credentials_flash_block, device_name_flash_block] = FlashArray::<2>::new(p.FLASH)?;
65
66    // Create device name field (max 4 characters)
67    static DEVICE_NAME_STATIC: TextFieldStatic<4> = TextField::new_static();
68    let device_name_field = TextField::new(
69        &DEVICE_NAME_STATIC,
70        device_name_flash_block,
71        "name",
72        "Name",
73        "PICO",
74    );
75
76    // Initialize WifiAuto
77    let wifi_auto = WifiAuto::new(
78        p.PIN_23,  // CYW43 power
79        p.PIN_24,  // CYW43 clock
80        p.PIN_25,  // CYW43 chip select
81        p.PIN_29,  // CYW43 data
82        p.PIO0,    // CYW43 PIO interface (required)
83        p.DMA_CH0, // CYW43 DMA (required)
84        wifi_credentials_flash_block,
85        p.PIN_13, // Button for forced reconfiguration
86        PressedTo::Ground,
87        "PicoTime", // Captive-portal SSID
88        [device_name_field],
89        spawner,
90    )?;
91
92    // Set up LED display (PIO1/DMA_CH1 to avoid conflict with WiFi)
93    let led12x8 = Led12x8::new(p.PIN_4, p.PIO1, p.DMA_CH1, spawner)?;
94
95    // Connect with status on display
96    let led12x8_ref = &led12x8;
97    let (stack, _button) = wifi_auto
98        .connect(|event| async move {
99            match event {
100                WifiAutoEvent::CaptivePortalReady => {
101                    led12x8_ref.write_text("JOIN", COLORS).await?;
102                }
103                WifiAutoEvent::Connecting { .. } => {
104                    led12x8_ref.write_text("...", COLORS).await?;
105                }
106                WifiAutoEvent::ConnectionFailed => {
107                    led12x8_ref.write_text("FAIL", COLORS).await?;
108                }
109            }
110            Ok(())
111        })
112        .await?;
113
114    led12x8.write_text("DONE", COLORS).await?;
115
116    // Get device name from field
117    let device_name = device_name_field.text()?.unwrap_or_default();
118    info!("Device name: {}", device_name.as_str());
119
120    // Show initial state with dashes until time arrives
121    let initial_display = format_two_lines("----", device_name.as_str());
122    led12x8.write_text(&initial_display, COLORS).await?;
123
124    // Main loop: fetch and display time every minute
125    loop {
126        match fetch_ntp_time(stack).await {
127            Ok(unix_seconds) => {
128                // Get last 4 digits of unix timestamp
129                let last_4_digits = (unix_seconds % 10000) as u16;
130                let time_str = format_4_digits(last_4_digits);
131
132                // Display: time on line 1, name on line 2
133                let display_text = format_two_lines(&time_str, device_name.as_str());
134                led12x8.write_text(&display_text, COLORS).await?;
135
136                info!("Time: {} | Name: {}", time_str, device_name.as_str());
137            }
138            Err(msg) => {
139                warn!("NTP fetch failed: {}", msg);
140                // Keep showing dashes with device name on error
141                let error_display = format_two_lines("----", device_name.as_str());
142                led12x8.write_text(&error_display, COLORS).await?;
143            }
144        }
145
146        Timer::after(Duration::from_secs(60)).await;
147    }
148}
149
150/// Format a number as a 4-digit string with leading zeros
151fn format_4_digits(num: u16) -> heapless::String<4> {
152    use core::fmt::Write;
153    let mut s = heapless::String::new();
154    write!(&mut s, "{:04}", num).unwrap();
155    s
156}
157
158/// Format two lines of text separated by newline
159fn format_two_lines(line1: &str, line2: &str) -> heapless::String<9> {
160    use core::fmt::Write;
161    let mut s = heapless::String::new();
162    write!(&mut s, "{}\n{}", line1, line2).unwrap();
163    s
164}
165
166/// Fetch current time from NTP server and return Unix timestamp.
167async fn fetch_ntp_time(stack: &Stack<'static>) -> core::result::Result<i64, &'static str> {
168    const NTP_SERVER: &str = "pool.ntp.org";
169    const NTP_PORT: u16 = 123;
170
171    // DNS lookup
172    let dns_result = stack
173        .dns_query(NTP_SERVER, DnsQueryType::A)
174        .await
175        .map_err(|_| "DNS lookup failed")?;
176    let server_addr = dns_result.first().ok_or("No DNS results")?;
177
178    // Create UDP socket
179    let mut rx_meta = [PacketMetadata::EMPTY; 1];
180    let mut rx_buffer = [0; 128];
181    let mut tx_meta = [PacketMetadata::EMPTY; 1];
182    let mut tx_buffer = [0; 128];
183    let mut socket = UdpSocket::new(
184        *stack,
185        &mut rx_meta,
186        &mut rx_buffer,
187        &mut tx_meta,
188        &mut tx_buffer,
189    );
190
191    socket.bind(0).map_err(|_| "Socket bind failed")?;
192
193    // Build NTP request (48 bytes, version 3, client mode)
194    let mut ntp_request = [0u8; 48];
195    ntp_request[0] = 0x1B; // LI=0, VN=3, Mode=3 (client)
196
197    // Send request
198    socket
199        .send_to(&ntp_request, (*server_addr, NTP_PORT))
200        .await
201        .map_err(|_| "NTP send failed")?;
202
203    // Receive response with timeout
204    let mut response = [0u8; 48];
205    embassy_time::with_timeout(Duration::from_secs(5), socket.recv_from(&mut response))
206        .await
207        .map_err(|_| "NTP receive timeout")?
208        .map_err(|_| "NTP receive failed")?;
209
210    // Extract transmit timestamp from response (bytes 40-43)
211    let ntp_seconds = u32::from_be_bytes([response[40], response[41], response[42], response[43]]);
212
213    // Convert NTP time (seconds since 1900) to Unix time (seconds since 1970)
214    const NTP_TO_UNIX_OFFSET: i64 = 2_208_988_800;
215    let unix_seconds = (ntp_seconds as i64) - NTP_TO_UNIX_OFFSET;
216
217    Ok(unix_seconds)
218}