1#![allow(missing_docs)]
2#![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
30const 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
34const 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 let [wifi_credentials_flash_block, device_name_flash_block] = FlashArray::<2>::new(p.FLASH)?;
65
66 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 let wifi_auto = WifiAuto::new(
78 p.PIN_23, p.PIN_24, p.PIN_25, p.PIN_29, p.PIO0, p.DMA_CH0, wifi_credentials_flash_block,
85 p.PIN_13, PressedTo::Ground,
87 "PicoTime", [device_name_field],
89 spawner,
90 )?;
91
92 let led12x8 = Led12x8::new(p.PIN_4, p.PIO1, p.DMA_CH1, spawner)?;
94
95 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 let device_name = device_name_field.text()?.unwrap_or_default();
118 info!("Device name: {}", device_name.as_str());
119
120 let initial_display = format_two_lines("----", device_name.as_str());
122 led12x8.write_text(&initial_display, COLORS).await?;
123
124 loop {
126 match fetch_ntp_time(stack).await {
127 Ok(unix_seconds) => {
128 let last_4_digits = (unix_seconds % 10000) as u16;
130 let time_str = format_4_digits(last_4_digits);
131
132 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 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
150fn 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
158fn 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
166async 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 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 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 let mut ntp_request = [0u8; 48];
195 ntp_request[0] = 0x1B; socket
199 .send_to(&ntp_request, (*server_addr, NTP_PORT))
200 .await
201 .map_err(|_| "NTP send failed")?;
202
203 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 let ntp_seconds = u32::from_be_bytes([response[40], response[41], response[42], response[43]]);
212
213 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}