Skip to main content

clock_lcd/
clock_lcd.rs

1#![allow(missing_docs)]
2//! LCD Clock - Event-driven time display with WiFi sync
3
4#![cfg(feature = "wifi")]
5#![no_std]
6#![no_main]
7#![allow(clippy::future_not_send, reason = "single-threaded")]
8
9use core::{convert::Infallible, fmt};
10use defmt::*;
11use defmt_rtt as _;
12use device_envoy::button::PressedTo;
13use device_envoy::char_lcd::{CharLcd, CharLcdStatic};
14use device_envoy::clock_sync::{ClockSync, ClockSyncStatic, ONE_SECOND};
15use device_envoy::flash_array::FlashArray;
16use device_envoy::wifi_auto::WifiAuto;
17use device_envoy::wifi_auto::fields::{TimezoneField, TimezoneFieldStatic};
18use device_envoy::{Error, Result};
19use embassy_executor::Spawner;
20use heapless::String;
21use panic_probe as _;
22
23// ============================================================================
24// Main Orchestrator
25// ============================================================================
26
27#[embassy_executor::main]
28pub async fn main(spawner: Spawner) -> ! {
29    // If it returns, something went wrong.
30    let err = inner_main(spawner).await.unwrap_err();
31    core::panic!("{err}");
32}
33
34async fn inner_main(spawner: Spawner) -> Result<Infallible> {
35    info!("Starting LCD Clock with WiFi");
36
37    // Initialize RP2040 peripherals
38    let p = embassy_rp::init(Default::default());
39
40    // Initialize CharLcd
41    static CHAR_LCD_STATIC: CharLcdStatic = CharLcd::new_static();
42    let char_lcd = CharLcd::new(&CHAR_LCD_STATIC, p.I2C0, p.PIN_5, p.PIN_4, spawner)?;
43
44    // Use two blocks of flash storage: Wi-Fi credentials + timezone
45    let [wifi_credentials_flash_block, timezone_flash_block] = FlashArray::<2>::new(p.FLASH)?;
46
47    // Define timezone field for captive portal
48    static TIMEZONE_FIELD_STATIC: TimezoneFieldStatic = TimezoneField::new_static();
49    let timezone_field = TimezoneField::new(&TIMEZONE_FIELD_STATIC, timezone_flash_block);
50
51    // Set up WiFi via captive portal
52    let wifi_auto = WifiAuto::new(
53        p.PIN_23,  // CYW43 power
54        p.PIN_24,  // CYW43 clock
55        p.PIN_25,  // CYW43 chip select
56        p.PIN_29,  // CYW43 data pin
57        p.PIO0,    // CYW43 PIO interface
58        p.DMA_CH0, // CYW43 DMA channel
59        wifi_credentials_flash_block,
60        p.PIN_13, // Reset button pin
61        PressedTo::Ground,
62        "www.picoclock.net",
63        [timezone_field],
64        spawner,
65    )?;
66
67    // Connect to WiFi
68    let (stack, _button) = wifi_auto.connect(|_event| async move { Ok(()) }).await?;
69
70    // Create ClockSync device with timezone from WiFi portal
71    let timezone_offset_minutes = timezone_field
72        .offset_minutes()?
73        .ok_or(Error::MissingCustomWifiAutoField)?;
74    static CLOCK_SYNC_STATIC: ClockSyncStatic = ClockSync::new_static();
75    let clock_sync = ClockSync::new(
76        &CLOCK_SYNC_STATIC,
77        stack,
78        timezone_offset_minutes,
79        Some(ONE_SECOND),
80        spawner,
81    );
82
83    info!("Entering main event loop");
84
85    // Main orchestrator loop - owns LCD and displays the clock
86    loop {
87        let tick = clock_sync.wait_for_tick().await;
88        let time_info = tick.local_time;
89        let mut text = String::<64>::new();
90        let (hour12, am_pm) = if time_info.hour() == 0 {
91            (12, "AM")
92        } else if time_info.hour() < 12 {
93            (time_info.hour(), "AM")
94        } else if time_info.hour() == 12 {
95            (12, "PM")
96        } else {
97            #[expect(clippy::arithmetic_side_effects, reason = "hour guaranteed 13-23")]
98            {
99                (time_info.hour() - 12, "PM")
100            }
101        };
102        fmt::Write::write_fmt(
103            &mut text,
104            format_args!(
105                "{:2}:{:02}:{:02} {}\n{:04}-{:02}-{:02}",
106                hour12,
107                time_info.minute(),
108                time_info.second(),
109                am_pm,
110                time_info.year(),
111                u8::from(time_info.month()),
112                time_info.day()
113            ),
114        )
115        .map_err(|_| Error::FormatError)?;
116        char_lcd.write_text(text, 0).await;
117    }
118}