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, TryFrom};
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::servo_player::{AtEnd, combine, linear, servo_player};
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_time::{Duration, Timer};
29use panic_probe as _;
30
31const FAST_MODE_SPEED: f32 = 720.0;
32
33button_watch! {
34 ButtonWatch13 {
35 pin: PIN_13,
36 }
37}
38
39servo_player! {
41 BottomServoPlayer {
42 pin: PIN_11,
43 max_steps: 30,
44 }
45}
46
47servo_player! {
48 TopServoPlayer {
49 pin: PIN_12,
50 max_steps: 30,
51 }
52}
53
54#[embassy_executor::main]
55pub async fn main(spawner: Spawner) -> ! {
56 let err = inner_main(spawner).await.unwrap_err();
57 core::panic!("{err}");
58}
59
60async fn inner_main(spawner: Spawner) -> Result<Infallible> {
61 info!("Starting Wi-Fi servo clock (WifiAuto)");
62 let p = embassy_rp::init(Default::default());
63
64 let [wifi_credentials_flash_block, timezone_flash_block] = FlashArray::<2>::new(p.FLASH)?;
66
67 static TIMEZONE_FIELD_STATIC: TimezoneFieldStatic = TimezoneField::new_static();
69 let timezone_field = TimezoneField::new(&TIMEZONE_FIELD_STATIC, timezone_flash_block);
70
71 let wifi_auto = WifiAuto::new(
73 p.PIN_23, p.PIN_24, p.PIN_25, p.PIN_29, p.PIO0, p.DMA_CH0, wifi_credentials_flash_block,
80 p.PIN_13, PressedTo::Ground,
82 "PicoServoClock", [timezone_field],
84 spawner,
85 )?;
86
87 let bottom_servo = BottomServoPlayer::new(p.PIN_11, p.PWM_SLICE5, spawner)?;
89 let top_servo = TopServoPlayer::new(p.PIN_12, p.PWM_SLICE6, spawner)?;
90 let servo_display = ServoClockDisplay::new(bottom_servo, top_servo);
91
92 let servo_display_ref = &servo_display;
94 let (stack, button) = wifi_auto
95 .connect(|event| {
96 let servo_display_ref = servo_display_ref;
97 async move {
98 match event {
99 WifiAutoEvent::CaptivePortalReady => {
100 servo_display_ref.show_portal_ready().await;
101 }
102 WifiAutoEvent::Connecting { .. } => servo_display_ref.show_connecting().await,
103 WifiAutoEvent::ConnectionFailed => {
104 }
106 }
107 Ok(())
108 }
109 })
110 .await?;
111
112 info!("WiFi connected");
113
114 let button_watch13 = ButtonWatch13::from_button(button, spawner)?;
116
117 let offset_minutes = timezone_field
119 .offset_minutes()?
120 .ok_or(Error::MissingCustomWifiAutoField)?;
121
122 static CLOCK_SYNC_STATIC: ClockSyncStatic = ClockSync::new_static();
124 let clock_sync = ClockSync::new(
125 &CLOCK_SYNC_STATIC,
126 stack,
127 offset_minutes,
128 Some(ONE_MINUTE),
129 spawner,
130 );
131
132 let mut state = State::HoursMinutes { speed: 1.0 };
134 loop {
135 state = match state {
136 State::HoursMinutes { speed } => {
137 state
138 .execute_hours_minutes(speed, &clock_sync, button_watch13, &servo_display)
139 .await?
140 }
141 State::MinutesSeconds => {
142 state
143 .execute_minutes_seconds(&clock_sync, button_watch13, &servo_display)
144 .await?
145 }
146 State::EditOffset => {
147 state
148 .execute_edit_offset(
149 &clock_sync,
150 button_watch13,
151 &timezone_field,
152 &servo_display,
153 )
154 .await?
155 }
156 };
157 }
158}
159
160#[derive(Debug, defmt::Format, Clone, Copy, PartialEq)]
164pub enum State {
165 HoursMinutes { speed: f32 },
166 MinutesSeconds,
167 EditOffset,
168}
169
170impl State {
171 async fn execute_hours_minutes(
172 self,
173 speed: f32,
174 clock_sync: &ClockSync,
175 button_watch13: &ButtonWatch13,
176 servo_display: &ServoClockDisplay,
177 ) -> Result<Self> {
178 clock_sync.set_speed(speed).await;
179 let (hours, minutes, _) = h12_m_s(&clock_sync.now_local());
180 servo_display.show_hours_minutes(hours, minutes).await;
181 clock_sync.set_tick_interval(Some(ONE_MINUTE)).await;
182 loop {
183 match select(
184 button_watch13.wait_for_press_duration(),
185 clock_sync.wait_for_tick(),
186 )
187 .await
188 {
189 Either::First(press_duration) => match (press_duration, speed.to_bits()) {
191 (PressDuration::Short, bits) if bits == 1.0f32.to_bits() => {
192 return Ok(Self::MinutesSeconds);
193 }
194 (PressDuration::Short, _) => {
195 return Ok(Self::HoursMinutes { speed: 1.0 });
196 }
197 (PressDuration::Long, _) => {
198 return Ok(Self::EditOffset);
199 }
200 },
201 Either::Second(tick) => {
203 let (hours, minutes, _) = h12_m_s(&tick.local_time);
204 servo_display.show_hours_minutes(hours, minutes).await;
205 }
206 }
207 }
208 }
209
210 async fn execute_minutes_seconds(
211 self,
212 clock_sync: &ClockSync,
213 button_watch13: &ButtonWatch13,
214 servo_display: &ServoClockDisplay,
215 ) -> Result<Self> {
216 clock_sync.set_speed(1.0).await;
217 let (_, minutes, seconds) = h12_m_s(&clock_sync.now_local());
218 servo_display.show_minutes_seconds(minutes, seconds).await;
219 clock_sync.set_tick_interval(Some(ONE_SECOND)).await;
220 loop {
221 match select(
222 button_watch13.wait_for_press_duration(),
223 clock_sync.wait_for_tick(),
224 )
225 .await
226 {
227 Either::First(PressDuration::Short) => {
229 return Ok(Self::HoursMinutes {
230 speed: FAST_MODE_SPEED,
231 });
232 }
233 Either::First(PressDuration::Long) => {
234 return Ok(Self::EditOffset);
235 }
236 Either::Second(tick) => {
238 let (_, minutes, seconds) = h12_m_s(&tick.local_time);
239 servo_display.show_minutes_seconds(minutes, seconds).await;
240 }
241 }
242 }
243 }
244
245 async fn execute_edit_offset(
246 self,
247 clock_sync: &ClockSync,
248 button_watch13: &ButtonWatch13,
249 timezone_field: &TimezoneField,
250 servo_display: &ServoClockDisplay,
251 ) -> Result<Self> {
252 info!("Entering edit offset mode");
253 clock_sync.set_speed(1.0).await;
254
255 let (hours, minutes, _) = h12_m_s(&clock_sync.now_local());
257 servo_display
258 .show_hours_minutes_indicator(hours, minutes)
259 .await;
260 const WIGGLE: [(u16, Duration); 2] = [
262 (80, Duration::from_millis(250)),
263 (100, Duration::from_millis(250)),
264 ];
265 servo_display.bottom.animate(WIGGLE, AtEnd::Loop);
266
267 let mut offset_minutes = clock_sync.offset_minutes();
269 info!("Current offset: {} minutes", offset_minutes);
270
271 clock_sync.set_tick_interval(None).await; loop {
273 info!("Waiting for button press in edit mode");
274 match button_watch13.wait_for_press_duration().await {
275 PressDuration::Short => {
276 info!("Short press detected - incrementing offset");
277 offset_minutes += 60;
279 const ONE_DAY_MINUTES: i32 = ONE_DAY.as_secs() as i32 / 60;
280 if offset_minutes >= ONE_DAY_MINUTES {
281 offset_minutes -= ONE_DAY_MINUTES;
282 }
283 clock_sync.set_offset_minutes(offset_minutes).await;
284 info!("New offset: {} minutes", offset_minutes);
285
286 let (hours, minutes, _) = h12_m_s(&clock_sync.now_local());
288 info!(
289 "Updated time after offset change: {:02}:{:02}",
290 hours, minutes
291 );
292 servo_display
293 .show_hours_minutes_indicator(hours, minutes)
294 .await;
295 servo_display.bottom.animate(WIGGLE, AtEnd::Loop);
296 }
297 PressDuration::Long => {
298 info!("Long press detected - saving and exiting edit mode");
299 timezone_field.set_offset_minutes(offset_minutes)?;
301 info!("Offset saved to flash: {} minutes", offset_minutes);
302 return Ok(Self::HoursMinutes { speed: 1.0 });
303 }
304 }
305 }
306 }
307}
308
309struct ServoClockDisplay {
310 bottom: &'static BottomServoPlayer,
311 top: &'static TopServoPlayer,
312}
313
314impl ServoClockDisplay {
315 fn new(bottom: &'static BottomServoPlayer, top: &'static TopServoPlayer) -> Self {
316 Self { bottom, top }
317 }
318
319 async fn show_portal_ready(&self) {
320 self.bottom.set_degrees(90);
321 self.top.set_degrees(90);
322 }
323
324 async fn show_connecting(&self) {
325 const FIVE_SECONDS: Duration = Duration::from_secs(5);
327 const PHASE1: [(u16, Duration); 10] = linear(180 - 18, 0, FIVE_SECONDS);
328 const PHASE2: [(u16, Duration); 2] = linear(0, 180, FIVE_SECONDS);
329 self.top.animate(combine!(PHASE1, PHASE2), AtEnd::Loop);
330 self.bottom.animate(combine!(PHASE2, PHASE1), AtEnd::Loop);
331 }
332
333 async fn show_hours_minutes(&self, hours: u8, minutes: u8) {
334 let left_angle = hours_to_degrees(hours);
335 let right_angle = sixty_to_degrees(minutes);
336 self.set_angles(left_angle, right_angle).await;
337 Timer::after(Duration::from_millis(500)).await;
338 self.bottom.relax();
339 self.top.relax();
340 }
341
342 async fn show_hours_minutes_indicator(&self, hours: u8, minutes: u8) {
343 let left_angle = hours_to_degrees(hours);
344 let right_angle = sixty_to_degrees(minutes);
345 self.set_angles(left_angle, right_angle).await;
346 Timer::after(Duration::from_millis(500)).await;
347 self.bottom.relax();
348 self.top.relax();
349 }
350
351 async fn show_minutes_seconds(&self, minutes: u8, seconds: u8) {
352 let left_angle = sixty_to_degrees(minutes);
353 let right_angle = sixty_to_degrees(seconds);
354 self.set_angles(left_angle, right_angle).await;
355 }
356
357 async fn set_angles(&self, left_degrees: i32, right_degrees: i32) {
358 let physical_left = reflect_degrees(right_degrees);
360 let physical_right = reflect_degrees(left_degrees);
361 let left_angle =
362 u16::try_from(physical_left).expect("servo angles must be between 0 and 180 degrees");
363 let right_angle =
364 u16::try_from(physical_right).expect("servo angles must be between 0 and 180 degrees");
365 self.bottom.set_degrees(left_angle);
366 self.top.set_degrees(right_angle);
367 }
368}
369
370#[inline]
371fn hours_to_degrees(hours: u8) -> i32 {
372 assert!((1..=12).contains(&hours));
373 let normalized_hour = hours % 12;
374 i32::from(normalized_hour) * 180 / 12
375}
376
377#[inline]
378fn sixty_to_degrees(value: u8) -> i32 {
379 assert!(value < 60);
380 i32::from(value) * 180 / 60
381}
382
383#[inline]
384fn reflect_degrees(degrees: i32) -> i32 {
385 assert!((0..=180).contains(°rees));
386 180 - degrees
387}