1#![allow(missing_docs)]
2#![no_std]
9#![no_main]
10#![cfg(feature = "wifi")]
11#![allow(clippy::future_not_send, reason = "single-threaded")]
12
13use core::convert::Infallible;
14use defmt::info;
15use defmt_rtt as _;
16use device_envoy::button::{PressDuration, PressedTo};
17use device_envoy::button_watch;
18use device_envoy::clock_sync::{
19 ClockSync, ClockSyncStatic, ONE_DAY, ONE_MINUTE, ONE_SECOND, h12_m_s,
20};
21use device_envoy::flash_array::FlashArray;
22use device_envoy::led4::{BlinkState, Led4, Led4Static, OutputArray, circular_outline_animation};
23use device_envoy::wifi_auto::fields::{TimezoneField, TimezoneFieldStatic};
24use device_envoy::wifi_auto::{WifiAuto, WifiAutoEvent};
25use device_envoy::{Error, Result};
26use embassy_executor::Spawner;
27use embassy_futures::select::{Either, select};
28use embassy_rp::gpio::{self, Level};
29use panic_probe as _;
30
31const FAST_MODE_SPEED: f32 = 720.0;
32
33button_watch! {
34 ButtonWatch13 {
35 pin: PIN_13,
36 }
37}
38
39#[embassy_executor::main]
40pub async fn main(spawner: Spawner) -> ! {
41 let err = inner_main(spawner).await.unwrap_err();
42 core::panic!("{err}");
43}
44
45async fn inner_main(spawner: Spawner) -> Result<Infallible> {
46 info!("Starting Wi-Fi 4-digit clock (WifiAuto)");
47 let p = embassy_rp::init(Default::default());
48
49 let [wifi_credentials_flash_block, timezone_flash_block] = FlashArray::<2>::new(p.FLASH)?;
51
52 static TIMEZONE_FIELD_STATIC: TimezoneFieldStatic = TimezoneField::new_static();
54 let timezone_field = TimezoneField::new(&TIMEZONE_FIELD_STATIC, timezone_flash_block);
55
56 let wifi_auto = WifiAuto::new(
58 p.PIN_23, p.PIN_24, p.PIN_25, p.PIN_29, p.PIO0, p.DMA_CH0, wifi_credentials_flash_block,
65 p.PIN_13, PressedTo::Ground,
67 "www.picoclock.net", [timezone_field], spawner,
70 )?;
71
72 let cell_pins = OutputArray::new([
74 gpio::Output::new(p.PIN_1, Level::High),
75 gpio::Output::new(p.PIN_2, Level::High),
76 gpio::Output::new(p.PIN_3, Level::High),
77 gpio::Output::new(p.PIN_4, Level::High),
78 ]);
79
80 let segment_pins = OutputArray::new([
81 gpio::Output::new(p.PIN_5, Level::Low),
82 gpio::Output::new(p.PIN_6, Level::Low),
83 gpio::Output::new(p.PIN_7, Level::Low),
84 gpio::Output::new(p.PIN_8, Level::Low),
85 gpio::Output::new(p.PIN_9, Level::Low),
86 gpio::Output::new(p.PIN_10, Level::Low),
87 gpio::Output::new(p.PIN_11, Level::Low),
88 gpio::Output::new(p.PIN_12, Level::Low),
89 ]);
90
91 static LED4_STATIC: Led4Static = Led4::new_static();
92 let led4 = Led4::new(&LED4_STATIC, cell_pins, segment_pins, spawner)?;
93
94 let led4_ref = &led4;
96 let (stack, button) = wifi_auto
97 .connect(|event| async move {
98 match event {
99 WifiAutoEvent::CaptivePortalReady => {
100 led4_ref.write_text(['j', 'o', 'i', 'n'], BlinkState::BlinkingAndOn);
101 }
102 WifiAutoEvent::Connecting { .. } => {
103 led4_ref.animate_text(circular_outline_animation(true));
104 }
105 WifiAutoEvent::ConnectionFailed => {
106 led4_ref.write_text(['F', 'A', 'I', 'L'], BlinkState::BlinkingButOff);
107 }
108 }
109 Ok(())
110 })
111 .await?;
112
113 led4.write_text(['D', 'O', 'N', 'E'], BlinkState::Solid);
114 info!("WiFi connected");
115
116 let button_watch13 = ButtonWatch13::from_button(button, spawner)?;
118
119 let offset_minutes = timezone_field
121 .offset_minutes()?
122 .ok_or(Error::MissingCustomWifiAutoField)?;
123
124 static CLOCK_SYNC_STATIC: ClockSyncStatic = ClockSync::new_static();
126 let clock_sync = ClockSync::new(
127 &CLOCK_SYNC_STATIC,
128 stack,
129 offset_minutes,
130 Some(ONE_MINUTE),
131 spawner,
132 );
133
134 let mut state = State::HoursMinutes { speed: 1.0 };
136 loop {
137 state = match state {
138 State::HoursMinutes { speed } => {
139 state
140 .execute_hours_minutes(speed, &clock_sync, button_watch13, &led4)
141 .await?
142 }
143 State::MinutesSeconds => {
144 state
145 .execute_minutes_seconds(&clock_sync, button_watch13, &led4)
146 .await?
147 }
148 State::EditOffset => {
149 state
150 .execute_edit_offset(&clock_sync, button_watch13, &timezone_field, &led4)
151 .await?
152 }
153 };
154 }
155}
156
157#[derive(Debug, defmt::Format, Clone, Copy, PartialEq)]
161pub enum State {
162 HoursMinutes { speed: f32 },
163 MinutesSeconds,
164 EditOffset,
165}
166
167impl State {
168 async fn execute_hours_minutes(
169 self,
170 speed: f32,
171 clock_sync: &ClockSync,
172 button_watch13: &ButtonWatch13,
173 led4: &Led4<'_>,
174 ) -> Result<Self> {
175 clock_sync.set_speed(speed).await;
176 let (hours, minutes, _) = h12_m_s(&clock_sync.now_local());
177 led4.write_text(
178 [
179 Self::tens_hours(hours),
180 Self::ones_digit(hours),
181 Self::tens_digit(minutes),
182 Self::ones_digit(minutes),
183 ],
184 BlinkState::Solid,
185 );
186 clock_sync.set_tick_interval(Some(ONE_MINUTE)).await;
187 loop {
188 match select(
189 button_watch13.wait_for_press_duration(),
190 clock_sync.wait_for_tick(),
191 )
192 .await
193 {
194 Either::First(press_duration) => match (press_duration, speed.to_bits()) {
196 (PressDuration::Short, bits) if bits == 1.0f32.to_bits() => {
197 return Ok(Self::MinutesSeconds);
198 }
199 (PressDuration::Short, _) => {
200 return Ok(Self::HoursMinutes { speed: 1.0 });
201 }
202 (PressDuration::Long, _) => {
203 return Ok(Self::EditOffset);
204 }
205 },
206 Either::Second(tick) => {
208 let (hours, minutes, _) = h12_m_s(&tick.local_time);
209 led4.write_text(
210 [
211 Self::tens_hours(hours),
212 Self::ones_digit(hours),
213 Self::tens_digit(minutes),
214 Self::ones_digit(minutes),
215 ],
216 BlinkState::Solid,
217 );
218 }
219 }
220 }
221 }
222
223 async fn execute_minutes_seconds(
224 self,
225 clock_sync: &ClockSync,
226 button_watch13: &ButtonWatch13,
227 led4: &Led4<'_>,
228 ) -> Result<Self> {
229 clock_sync.set_speed(1.0).await;
230 let (_, minutes, seconds) = h12_m_s(&clock_sync.now_local());
231 led4.write_text(
232 [
233 Self::tens_digit(minutes),
234 Self::ones_digit(minutes),
235 Self::tens_digit(seconds),
236 Self::ones_digit(seconds),
237 ],
238 BlinkState::Solid,
239 );
240 clock_sync.set_tick_interval(Some(ONE_SECOND)).await;
241 loop {
242 match select(
243 button_watch13.wait_for_press_duration(),
244 clock_sync.wait_for_tick(),
245 )
246 .await
247 {
248 Either::First(PressDuration::Short) => {
250 return Ok(Self::HoursMinutes {
251 speed: FAST_MODE_SPEED,
252 });
253 }
254 Either::First(PressDuration::Long) => {
255 return Ok(Self::EditOffset);
256 }
257 Either::Second(tick) => {
259 let (_, minutes, seconds) = h12_m_s(&tick.local_time);
260 led4.write_text(
261 [
262 Self::tens_digit(minutes),
263 Self::ones_digit(minutes),
264 Self::tens_digit(seconds),
265 Self::ones_digit(seconds),
266 ],
267 BlinkState::Solid,
268 );
269 }
270 }
271 }
272 }
273
274 async fn execute_edit_offset(
275 self,
276 clock_sync: &ClockSync,
277 button_watch13: &ButtonWatch13,
278 timezone_field: &TimezoneField,
279 led4: &Led4<'_>,
280 ) -> Result<Self> {
281 info!("Entering edit offset mode");
282
283 let (hours, minutes, _) = h12_m_s(&clock_sync.now_local());
285 led4.write_text(
286 [
287 Self::tens_hours(hours),
288 Self::ones_digit(hours),
289 Self::tens_digit(minutes),
290 Self::ones_digit(minutes),
291 ],
292 BlinkState::BlinkingAndOn,
293 );
294
295 let mut offset_minutes = clock_sync.offset_minutes();
297 info!("Current offset: {} minutes", offset_minutes);
298
299 clock_sync.set_tick_interval(None).await; clock_sync.set_speed(1.0).await;
301 loop {
302 info!("Waiting for button press in edit mode");
303 match button_watch13.wait_for_press_duration().await {
304 PressDuration::Short => {
305 info!("Short press detected - incrementing offset");
306 offset_minutes += 60;
308 const ONE_DAY_MINUTES: i32 = ONE_DAY.as_secs() as i32 / 60;
309 if offset_minutes >= ONE_DAY_MINUTES {
310 offset_minutes -= ONE_DAY_MINUTES;
311 }
312 clock_sync.set_offset_minutes(offset_minutes).await;
313 info!("New offset: {} minutes", offset_minutes);
314
315 let (hours, minutes, _) = h12_m_s(&clock_sync.now_local());
317 info!(
318 "Updated time after offset change: {:02}:{:02}",
319 hours, minutes
320 );
321 led4.write_text(
322 [
323 Self::tens_hours(hours),
324 Self::ones_digit(hours),
325 Self::tens_digit(minutes),
326 Self::ones_digit(minutes),
327 ],
328 BlinkState::BlinkingAndOn,
329 );
330 }
331 PressDuration::Long => {
332 info!("Long press detected - saving and exiting edit mode");
333 timezone_field.set_offset_minutes(offset_minutes)?;
335 info!("Offset saved to flash: {} minutes", offset_minutes);
336 return Ok(Self::HoursMinutes { speed: 1.0 });
337 }
338 }
339 }
340 }
341
342 #[inline]
343 #[expect(
344 clippy::arithmetic_side_effects,
345 clippy::integer_division_remainder_used,
346 reason = "Value < 60 ensures division is safe"
347 )]
348 const fn tens_digit(value: u8) -> char {
349 ((value / 10) + b'0') as char
350 }
351
352 #[inline]
353 const fn tens_hours(value: u8) -> char {
354 if value >= 10 { '1' } else { ' ' }
355 }
356
357 #[inline]
358 #[expect(
359 clippy::arithmetic_side_effects,
360 clippy::integer_division_remainder_used,
361 reason = "Value < 60 ensures division is safe"
362 )]
363 const fn ones_digit(value: u8) -> char {
364 ((value % 10) + b'0') as char
365 }
366}