1use embedded_graphics::{
57 mono_font::{MonoFont, ascii::FONT_6X10},
58 pixelcolor::Rgb888,
59};
60use std::{cell::RefCell, thread_local, vec::Vec};
61use wasm_bindgen::{JsCast, JsValue, prelude::wasm_bindgen};
62use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement};
63
64use super::{ButtonWasm, ButtonWasmSource, CydTouchWasmSource, CydWasm, next_animation_frame};
65use crate::button::Button;
66use crate::cyd::display::Orientation;
67use crate::wifi_auto::WifiAutoEvent;
68
69const WIFI_CAPTIVE_PORTAL_WAIT_FRAMES: usize = 15;
70const WIFI_CONNECT_WAIT_FRAMES: usize = 90;
71
72const BACKGROUND_COLOR: Rgb888 = Rgb888::new(10, 10, 12); const FOREGROUND_COLOR: Rgb888 = Rgb888::new(230, 230, 230); pub struct CydSimulatorWasm {
78 cyd: CydWasm,
79 button_source: ButtonWasmSource,
80 control: CydSimulatorControlWasm,
81}
82
83#[wasm_bindgen]
86#[derive(Clone)]
87pub struct CydSimulatorControlWasm {
88 touch_source: Option<CydTouchWasmSource>,
89 button_source: ButtonWasmSource,
90 orientation: Orientation,
91}
92
93#[derive(Clone, Copy, Debug, Eq, PartialEq)]
96pub enum WifiConnectOutcome {
97 Connected,
99 ResetRequested,
101}
102
103#[derive(Clone, Copy, Debug, Eq, PartialEq)]
104enum WifiSimulatorPhase {
105 Disconnected,
106 CaptivePortal,
107 Connecting,
108 Connected,
109}
110
111thread_local! {
112 static WIFI_SIMULATOR_PHASES: RefCell<Vec<(&'static str, WifiSimulatorPhase)>> =
113 const { RefCell::new(Vec::new()) };
114}
115
116pub struct WifiSimulatorWasm {
143 storage_namespace: &'static str,
144}
145
146impl WifiSimulatorWasm {
147 #[must_use]
150 pub const fn new(storage_namespace: &'static str) -> Self {
151 Self { storage_namespace }
152 }
153
154 pub fn reset(&self) {
157 set_phase(self.storage_namespace, WifiSimulatorPhase::Disconnected);
158 }
159
160 fn phase(&self) -> WifiSimulatorPhase {
161 WIFI_SIMULATOR_PHASES.with(|phases| {
162 phases
163 .borrow()
164 .iter()
165 .find(|(namespace, _)| *namespace == self.storage_namespace)
166 .map_or(WifiSimulatorPhase::Disconnected, |(_, phase)| *phase)
167 })
168 }
169
170 pub async fn connect<OnEvent, Error>(
173 &self,
174 button: &mut ButtonWasm,
175 mut on_event: OnEvent,
176 ) -> Result<WifiConnectOutcome, Error>
177 where
178 OnEvent: AsyncFnMut(WifiAutoEvent) -> Result<(), Error>,
179 {
180 if self.phase() == WifiSimulatorPhase::Connected {
181 return Ok(WifiConnectOutcome::Connected);
182 }
183
184 set_phase(self.storage_namespace, WifiSimulatorPhase::CaptivePortal);
185 on_event(WifiAutoEvent::CaptivePortalReady).await?;
186 if wait_for_wifi_frames(button, WIFI_CAPTIVE_PORTAL_WAIT_FRAMES).await {
187 return Ok(WifiConnectOutcome::ResetRequested);
188 }
189
190 set_phase(self.storage_namespace, WifiSimulatorPhase::Connecting);
191 on_event(WifiAutoEvent::Connecting {
192 try_index: 0,
193 try_count: 1,
194 })
195 .await?;
196 if wait_for_wifi_frames(button, WIFI_CONNECT_WAIT_FRAMES).await {
197 return Ok(WifiConnectOutcome::ResetRequested);
198 }
199
200 set_phase(self.storage_namespace, WifiSimulatorPhase::Connected);
201 Ok(WifiConnectOutcome::Connected)
202 }
203}
204
205fn set_phase(storage_namespace: &'static str, phase: WifiSimulatorPhase) {
206 WIFI_SIMULATOR_PHASES.with(|phases| {
207 let mut phases = phases.borrow_mut();
208 if let Some((_, current_phase)) = phases
209 .iter_mut()
210 .find(|(namespace, _)| *namespace == storage_namespace)
211 {
212 *current_phase = phase;
213 } else {
214 phases.push((storage_namespace, phase));
215 }
216 });
217}
218
219impl CydSimulatorWasm {
220 pub fn new(canvas: HtmlCanvasElement, orientation: Orientation) -> Result<Self, JsValue> {
223 Self::new_with_style(
224 canvas,
225 orientation,
226 BACKGROUND_COLOR,
227 FOREGROUND_COLOR,
228 &FONT_6X10,
229 )
230 }
231
232 pub fn new_with_style(
235 canvas: HtmlCanvasElement,
236 orientation: Orientation,
237 background_color: Rgb888,
238 foreground_color: Rgb888,
239 font: &'static MonoFont<'static>,
240 ) -> Result<Self, JsValue> {
241 let context = canvas
242 .get_context("2d")?
243 .ok_or_else(|| JsValue::from_str("2D canvas context unavailable"))?
244 .dyn_into::<CanvasRenderingContext2d>()?;
245 canvas.set_width(orientation.width());
246 canvas.set_height(orientation.height());
247
248 let touch_source = CydTouchWasmSource::new();
249 let button_source = ButtonWasmSource::new();
250 let cyd = CydWasm::new(
251 context,
252 orientation,
253 background_color,
254 foreground_color,
255 font,
256 touch_source.clone(),
257 );
258 let control = CydSimulatorControlWasm {
259 touch_source: Some(touch_source),
260 button_source: button_source.clone(),
261 orientation,
262 };
263 Ok(Self {
264 cyd,
265 button_source,
266 control,
267 })
268 }
269
270 pub fn into_parts(self) -> (CydWasm, ButtonWasm, CydSimulatorControlWasm) {
273 let Self {
274 cyd,
275 button_source,
276 control,
277 } = self;
278 (cyd, button_source.button(), control)
279 }
280}
281
282impl CydSimulatorControlWasm {
283 #[must_use]
286 pub const fn orientation(&self) -> Orientation {
287 self.orientation
288 }
289}
290
291async fn wait_for_wifi_frames(button: &ButtonWasm, frame_count: usize) -> bool {
292 for _ in 0..frame_count {
293 if button.is_pressed() {
294 return true;
295 }
296 next_animation_frame().await;
297 }
298 false
299}
300
301#[wasm_bindgen]
302impl CydSimulatorControlWasm {
303 #[wasm_bindgen(js_name = orientation_is_inverted)]
306 pub fn orientation_is_inverted(&self) -> bool {
307 matches!(
308 self.orientation,
309 Orientation::LandscapeInverted | Orientation::PortraitInverted
310 )
311 }
312
313 #[wasm_bindgen(js_name = touch_down)]
316 pub fn touch_down(&self, x: f32, y: f32) {
317 let point = map_to_landscape(self.orientation, x, y);
318 if let Some(touch_source) = &self.touch_source {
319 touch_source.touch_down(point.0, point.1);
320 }
321 }
322
323 #[wasm_bindgen(js_name = touch_move)]
326 pub fn touch_move(&self, x: f32, y: f32) {
327 let point = map_to_landscape(self.orientation, x, y);
328 if let Some(touch_source) = &self.touch_source {
329 touch_source.touch_move(point.0, point.1);
330 }
331 }
332
333 #[wasm_bindgen(js_name = touch_up)]
336 pub fn touch_up(&self) {
337 if let Some(touch_source) = &self.touch_source {
338 touch_source.touch_up();
339 }
340 }
341
342 #[wasm_bindgen(js_name = boot_down)]
345 pub fn boot_down(&self) {
346 self.button_source.press();
347 }
348
349 #[wasm_bindgen(js_name = boot_up)]
352 pub fn boot_up(&self) {
353 self.button_source.release();
354 }
355
356 pub fn reset_transient_state(&self) {
359 if let Some(touch_source) = &self.touch_source {
360 touch_source.touch_up();
361 }
362 self.button_source.release();
363 }
364}
365
366fn map_to_landscape(orientation: Orientation, x: f32, y: f32) -> (f32, f32) {
367 match orientation {
368 Orientation::Landscape => (x, y),
369 Orientation::Portrait => (319.0 - y, x),
370 Orientation::LandscapeInverted => (319.0 - x, 239.0 - y),
371 Orientation::PortraitInverted => (y, 239.0 - x),
372 }
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378
379 #[test]
380 fn orientation_mapping_round_trips() {
381 for orientation in [
382 Orientation::Landscape,
383 Orientation::Portrait,
384 Orientation::LandscapeInverted,
385 Orientation::PortraitInverted,
386 ] {
387 let landscape_point = map_to_landscape(orientation, 37.0, 83.0);
388 let logical_point = match orientation {
389 Orientation::Landscape => landscape_point,
390 Orientation::Portrait => (landscape_point.1, 319.0 - landscape_point.0),
391 Orientation::LandscapeInverted => {
392 (319.0 - landscape_point.0, 239.0 - landscape_point.1)
393 }
394 Orientation::PortraitInverted => (239.0 - landscape_point.1, landscape_point.0),
395 };
396 assert_eq!(logical_point, (37.0, 83.0));
397 }
398 }
399
400 #[test]
401 fn wifi_state_is_scoped_by_storage_namespace() {
402 set_phase("app-a", WifiSimulatorPhase::Connected);
403 assert_eq!(
404 WifiSimulatorWasm::new("app-a").phase(),
405 WifiSimulatorPhase::Connected
406 );
407 assert_eq!(
408 WifiSimulatorWasm::new("app-b").phase(),
409 WifiSimulatorPhase::Disconnected
410 );
411
412 WifiSimulatorWasm::new("app-a").reset();
413 assert_eq!(
414 WifiSimulatorWasm::new("app-a").phase(),
415 WifiSimulatorPhase::Disconnected
416 );
417 assert_eq!(
418 WifiSimulatorWasm::new("app-b").phase(),
419 WifiSimulatorPhase::Disconnected
420 );
421 }
422}