tx2_web/
io.rs

1mod keyboard;
2
3use std::collections::BTreeMap;
4
5use js_sys::Array;
6use serde::Serialize;
7use tracing::{Level, event};
8use wasm_bindgen::prelude::*;
9
10use base::Unsigned6Bit;
11use cpu::*;
12
13use super::context::make_context;
14use super::io::keyboard::Code;
15use super::samples::{sample_binary_echo, sample_binary_hello};
16
17pub use keyboard::{
18    HtmlCanvas2DPainter, KeyPaintError, SWITCH_TO_FAR, SWITCH_TO_NEAR, draw_keyboard,
19};
20
21#[wasm_bindgen]
22pub fn tx2_load_tape(
23    tx2: &mut Tx2,
24    simulated_time: f64,
25    elapsed_time_secs: f64,
26    data: &[u8],
27) -> bool {
28    let context = make_context(simulated_time, elapsed_time_secs);
29    tx2.mount_paper_tape(&context, data.to_vec()).map_or_else(
30        |e: InputEventError| {
31            event!(Level::ERROR, "failed to load paper tape: {e}");
32            false
33        },
34        |f: InputFlagRaised| f.into(),
35    )
36}
37
38pub(crate) struct EmittedCodes {
39    first: u8,
40    second: Option<u8>,
41    far_now_active: bool,
42}
43
44fn try_u6_from_u8(x: u8) -> Result<Unsigned6Bit, String> {
45    fn failed<E: std::error::Error>(e: E) -> String {
46        format!("failed to map a key code into a six-bit number: {e}")
47    }
48    <Unsigned6Bit as std::convert::TryFrom<u8>>::try_from(x).map_err(failed)
49}
50
51impl EmittedCodes {
52    fn try_to_u6_vec(&self) -> Result<Vec<Unsigned6Bit>, String> {
53        let mut result: Vec<Unsigned6Bit> =
54            Vec::with_capacity(if self.second.is_some() { 2 } else { 1 });
55        result.push(try_u6_from_u8(self.first)?);
56        if let Some(second) = self.second {
57            result.push(try_u6_from_u8(second)?);
58        }
59        Ok(result)
60    }
61}
62
63pub(crate) fn hit_detection_rgb_to_code(
64    far_currently_active: bool,
65    r: u8,
66    g: u8,
67    b: u8,
68) -> Result<Option<EmittedCodes>, String> {
69    match Code::hit_detection_rgb_to_code(r, g, b) {
70        Ok(Some(Code::Far(code))) => Ok(Some(if far_currently_active {
71            EmittedCodes {
72                first: code,
73                second: None,
74                far_now_active: true,
75            }
76        } else {
77            EmittedCodes {
78                first: SWITCH_TO_FAR,
79                second: Some(code),
80                far_now_active: true,
81            }
82        })),
83        Ok(Some(Code::Near(code))) => Ok(Some(if far_currently_active {
84            EmittedCodes {
85                first: SWITCH_TO_NEAR,
86                second: Some(code),
87                far_now_active: false,
88            }
89        } else {
90            EmittedCodes {
91                first: code,
92                second: None,
93                far_now_active: false,
94            }
95        })),
96        Ok(None) => Ok(None),
97        Ok(Some(Code::Unknown)) => {
98            event!(
99                Level::DEBUG,
100                "the user hit a key whose key code we don't know"
101            );
102            Ok(None)
103        }
104        Err(e) => Err(e),
105    }
106}
107
108#[wasm_bindgen]
109pub struct KeystrokeOutcome {
110    consumed: bool,
111    flag_raised: bool,
112    far_keyboard_is_active: bool,
113}
114
115#[wasm_bindgen]
116impl KeystrokeOutcome {
117    #[wasm_bindgen(getter)]
118    pub fn consumed(&self) -> bool {
119        self.consumed
120    }
121
122    #[wasm_bindgen(getter)]
123    pub fn flag_raised(&self) -> bool {
124        self.flag_raised
125    }
126
127    #[wasm_bindgen(getter)]
128    pub fn far_keyboard_is_active(&mut self) -> bool {
129        self.far_keyboard_is_active
130    }
131}
132
133#[wasm_bindgen]
134pub fn tx2_lw_keyboard_click(
135    tx2: &mut Tx2,
136    simulated_time: f64,
137    elapsed_time_secs: f64,
138    unit: u8,
139    far_currently_active: bool,
140    rgb: &js_sys::Uint8ClampedArray,
141) -> KeystrokeOutcome {
142    let context = make_context(simulated_time, elapsed_time_secs);
143    match hit_detection_rgb_to_code(
144        far_currently_active,
145        rgb.at(0).unwrap_or(0),
146        rgb.at(1).unwrap_or(0),
147        rgb.at(2).unwrap_or(0),
148    ) {
149        Ok(Some(codes)) => match try_u6_from_u8(unit) {
150            Ok(unit) => match codes.try_to_u6_vec() {
151                Err(e) => {
152                    event!(
153                        Level::ERROR,
154                        "tx2_lw_keyboard_click: failed to convert input key code: {e}"
155                    );
156                    KeystrokeOutcome {
157                        consumed: false,
158                        flag_raised: false,
159                        far_keyboard_is_active: codes.far_now_active,
160                    }
161                }
162                Ok(data) => match tx2.lw_input(&context, unit, &data) {
163                    Ok((consumed, flag_raise)) => KeystrokeOutcome {
164                        consumed,
165                        flag_raised: flag_raise.into(),
166                        far_keyboard_is_active: codes.far_now_active,
167                    },
168                    Err(e) => {
169                        event!(
170                            Level::ERROR,
171                            "tx2_lw_keyboard_click: failed to accept input key: {e}"
172                        );
173                        KeystrokeOutcome {
174                            consumed: false,
175                            flag_raised: false,
176                            far_keyboard_is_active: codes.far_now_active,
177                        }
178                    }
179                },
180            },
181            Err(_) => {
182                event!(
183                    Level::WARN,
184                    "tx2_lw_keyboard_click: unit {unit:o} is out of range"
185                );
186                KeystrokeOutcome {
187                    consumed: false,
188                    flag_raised: false,
189                    far_keyboard_is_active: codes.far_now_active,
190                }
191            }
192        },
193        Ok(None) => {
194            // Click on something that wasn't a key.
195            KeystrokeOutcome {
196                consumed: true,
197                flag_raised: false,
198                far_keyboard_is_active: far_currently_active,
199            }
200        }
201        Err(_) => KeystrokeOutcome {
202            consumed: true,
203            flag_raised: false,
204            far_keyboard_is_active: far_currently_active,
205        },
206    }
207}
208
209#[wasm_bindgen]
210pub fn get_builtin_sample_tape(sample_name: &str) -> Result<Vec<u8>, String> {
211    match sample_name {
212        "echo" => Ok(sample_binary_echo()),
213        "hello" => Ok(sample_binary_hello()),
214        _ => Err(format!("unknown sample file '{sample_name}'")),
215    }
216    .map(|data| data.to_vec())
217}
218
219#[derive(Debug, Serialize)]
220struct SerializableExtendedUnitState {
221    pub flag: bool,
222    pub connected: bool,
223    pub in_maintenance: bool,
224    pub name: String,
225    pub text_info: String,
226    /// status is None for units which are attached but not currently
227    /// connected.
228    pub status: Option<ExtendedConnectedUnitStatus>,
229    pub index_value: i32,
230}
231
232impl From<ExtendedUnitState> for SerializableExtendedUnitState {
233    fn from(input: cpu::ExtendedUnitState) -> Self {
234        SerializableExtendedUnitState {
235            flag: input.flag,
236            connected: input.connected,
237            in_maintenance: input.in_maintenance,
238            name: input.name,
239            text_info: input.text_info,
240            status: input.status,
241            index_value: i32::from(input.index_value), // this is a change of type.
242        }
243    }
244}
245
246#[derive(Debug, Serialize)]
247pub struct UnitState {
248    unit: u8,
249    unit_state: SerializableExtendedUnitState,
250}
251
252#[wasm_bindgen]
253pub fn tx2_drain_device_changes(
254    tx2: &mut Tx2,
255    simulated_system_time_secs: f64,
256    elapsed_time_secs: f64,
257) -> Result<JsValue, String> {
258    let context = make_context(simulated_system_time_secs, elapsed_time_secs);
259    match tx2.drain_device_changes(&context) {
260        Ok(changes) => {
261            let change_map: BTreeMap<u8, UnitState> = changes
262                .into_iter()
263                .map(|(unit, state)| {
264                    (
265                        u8::from(unit),
266                        UnitState {
267                            unit: unit.into(),
268                            unit_state: state.into(),
269                        },
270                    )
271                })
272                .collect();
273            serde_wasm_bindgen::to_value(&change_map).map_err(|e| e.to_string())
274        }
275        Err(e) => Err(e.to_string()),
276    }
277}
278
279#[wasm_bindgen]
280pub fn tx2_device_statuses(
281    tx2: &mut Tx2,
282    simulated_system_time_secs: f64,
283    elapsed_time_secs: f64,
284) -> Array {
285    let context = make_context(simulated_system_time_secs, elapsed_time_secs);
286    match tx2.sequence_statuses(&context) {
287        Err(e) => {
288            panic!("tx2_device_statuses: failed: {e}");
289        }
290        Ok(statuses) => statuses
291            .into_iter()
292            .map(|(unit, status)| {
293                let status: SerializableExtendedUnitState = status.into();
294                event!(
295                    Level::DEBUG,
296                    "tx2_device_statuses: serializable status for unit {unit} is {status:?}"
297                );
298                let tmp = UnitState {
299                    unit: u8::from(unit),
300                    unit_state: status,
301                };
302                match serde_wasm_bindgen::to_value(&tmp) {
303                    Ok(jsval) => jsval,
304                    Err(e) => {
305                        panic!("failed to convert {tmp:?} to JsValue: {e}");
306                    }
307                }
308            })
309            .collect(),
310    }
311}