Skip to main content

clock_led4/
clock_led4.rs

1#![allow(missing_docs)]
2//! Wi-Fi enabled 4-digit clock that provisions credentials through `WifiAuto`.
3//!
4//! This example demonstrates how to pair the shared captive-portal workflow with the
5//! `ClockLed4` state machine. The `WifiAuto` helper owns Wi-Fi onboarding while the
6//! clock display reflects progress and, once connected, continues handling user input.
7
8#![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    // Use two blocks of flash storage: Wi-Fi credentials + timezone
50    let [wifi_credentials_flash_block, timezone_flash_block] = FlashArray::<2>::new(p.FLASH)?;
51
52    // Define HTML to ask for timezone on the captive portal.
53    static TIMEZONE_FIELD_STATIC: TimezoneFieldStatic = TimezoneField::new_static();
54    let timezone_field = TimezoneField::new(&TIMEZONE_FIELD_STATIC, timezone_flash_block);
55
56    // Set up Wifi via a captive portal. The button pin is used to reset stored credentials.
57    let wifi_auto = WifiAuto::new(
58        p.PIN_23,  // CYW43 power
59        p.PIN_24,  // CYW43 clock
60        p.PIN_25,  // CYW43 chip select
61        p.PIN_29,  // CYW43 data pin
62        p.PIO0,    // CYW43 PIO interface
63        p.DMA_CH0, // CYW43 DMA channel
64        wifi_credentials_flash_block,
65        p.PIN_13, // Reset button pin
66        PressedTo::Ground,
67        "www.picoclock.net", // Captive-portal SSID
68        [timezone_field],    // Custom fields to ask for
69        spawner,
70    )?;
71
72    // Set up the LED4 display.
73    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    // Connect Wi-Fi, using the clock display for status.
95    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    // Convert the Button from WifiAuto into a ButtonWatch for background monitoring
117    let button_watch13 = ButtonWatch13::from_button(button, spawner)?;
118
119    // Read the timezone offset, an extra field that WiFi portal saved to flash.
120    let offset_minutes = timezone_field
121        .offset_minutes()?
122        .ok_or(Error::MissingCustomWifiAutoField)?;
123
124    // Create a ClockSync device that knows its timezone offset.
125    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    // Start in HH:MM mode
135    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// State machine for 4-digit LED clock display modes and transitions.
158
159/// Display states for the 4-digit LED clock.
160#[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                // Button pushes
195                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                // Clock tick
207                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                // Button pushes
249                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                // Clock tick
258                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        // Blink current hours and minutes
284        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        // Get the current offset minutes from clock (source of truth)
296        let mut offset_minutes = clock_sync.offset_minutes();
297        info!("Current offset: {} minutes", offset_minutes);
298
299        clock_sync.set_tick_interval(None).await; // Disable ticks in edit mode
300        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                    // Increment the offset by 1 hour
307                    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                    // Update display (atomic already updated, can use now_local)
316                    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                    // Save to flash and exit edit mode
334                    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}