Skip to main content

openipc_web/
lib.rs

1use js_sys::{Array, Object, Reflect, Uint8Array};
2use openipc_core::ieee80211::WifiFrame;
3use openipc_core::realtek::{parse_rx_aggregate, RxPacketType};
4#[cfg(target_arch = "wasm32")]
5use openipc_core::realtek_tx::RealtekTxOptions;
6use openipc_core::{
7    AdaptiveLinkSender, ChannelId, Codec, DepacketizedFrame, FecCounters, FrameLayout,
8    PipelineEvent, RadioPort, ReceiverPipeline, WfbKeypair, WfbTxKeypair,
9};
10#[cfg(target_arch = "wasm32")]
11use openipc_rtl88xx::{
12    ChannelWidth, DriverOptions, FalseAlarmCounters, Firmware8814Mode, InitReport, InitStatus,
13    IqkReport, MonitorOptions, PhydmDigState, PhydmWatchdogReport, PowerTrackingReport,
14    PowerTrackingState, RadioConfig, RealtekDevice, ThermalBucket,
15};
16use wasm_bindgen::prelude::*;
17
18#[wasm_bindgen(typescript_custom_section)]
19const OPENIPC_VIDEO_FRAME_TYPES: &'static str = r#"
20export type OpenIpcVideoFrame = {
21    data: Uint8Array;
22    codec: "h264" | "h265";
23    codecString: string;
24    isKeyFrame: boolean;
25    timestamp: number;
26};
27
28export type OpenIpcRxTransferProfile = {
29    frames: OpenIpcVideoFrame[];
30    transferBytes: number;
31    packets: number;
32    acceptedPackets: number;
33    droppedPackets: number;
34    crcDropped: number;
35    icvDropped: number;
36    reportDropped: number;
37    ignoredFrames: number;
38    sessions: number;
39    wfbPayloads: number;
40    rtpPackets: number;
41    videoFrames: number;
42    parseMs: number;
43    pipelineMs: number;
44    totalMs: number;
45};
46"#;
47
48#[wasm_bindgen]
49pub struct OpenIpcReceiver {
50    pipeline: ReceiverPipeline,
51}
52
53#[wasm_bindgen]
54impl OpenIpcReceiver {
55    #[wasm_bindgen(constructor)]
56    pub fn new() -> Result<OpenIpcReceiver, JsValue> {
57        Self::with_channel_id(openipc_core::channel::DEFAULT_LINK_ID << 8, 1, 5)
58    }
59
60    #[wasm_bindgen(js_name = withChannelId)]
61    pub fn with_channel_id(
62        channel_id: u32,
63        fec_k: usize,
64        fec_n: usize,
65    ) -> Result<OpenIpcReceiver, JsValue> {
66        let pipeline = ReceiverPipeline::new(
67            ChannelId::new(channel_id),
68            FrameLayout::WithFcs,
69            fec_k,
70            fec_n,
71        )
72        .map_err(|err| JsValue::from_str(&format!("invalid receiver config: {err:?}")))?;
73        Ok(Self { pipeline })
74    }
75
76    #[wasm_bindgen(js_name = withKeypair)]
77    pub fn with_keypair(
78        channel_id: u32,
79        keypair: &[u8],
80        minimum_epoch: u64,
81    ) -> Result<OpenIpcReceiver, JsValue> {
82        let keypair = WfbKeypair::from_bytes(keypair)
83            .map_err(|err| JsValue::from_str(&format!("invalid WFB keypair: {err}")))?;
84        let pipeline = ReceiverPipeline::with_keypair(
85            ChannelId::new(channel_id),
86            FrameLayout::WithFcs,
87            keypair,
88            minimum_epoch,
89        )
90        .map_err(|err| JsValue::from_str(&format!("invalid encrypted receiver config: {err}")))?;
91        Ok(Self { pipeline })
92    }
93
94    #[wasm_bindgen(js_name = pushRtpPacket)]
95    pub fn push_rtp_packet(&mut self, data: &[u8]) -> Option<Uint8Array> {
96        self.pipeline
97            .push_rtp(data)
98            .map(|frame| Uint8Array::from(frame.data.as_slice()))
99    }
100
101    #[wasm_bindgen(
102        js_name = pushRtpPacketDetailed,
103        unchecked_return_type = "OpenIpcVideoFrame | null"
104    )]
105    pub fn push_rtp_packet_detailed(&mut self, data: &[u8]) -> Result<JsValue, JsValue> {
106        match self.pipeline.push_rtp(data) {
107            Some(frame) => Ok(video_frame_object(frame)?.into()),
108            None => Ok(JsValue::NULL),
109        }
110    }
111
112    #[wasm_bindgen(js_name = pushDecryptedFragment)]
113    pub fn push_decrypted_fragment(
114        &mut self,
115        data_nonce_hex: &str,
116        fragment: &[u8],
117    ) -> Result<Array, JsValue> {
118        let data_nonce = parse_hex_u64(data_nonce_hex)?;
119        let events = self
120            .pipeline
121            .push_decrypted_fragment(data_nonce, fragment)
122            .map_err(|err| JsValue::from_str(&format!("WFB fragment rejected: {err:?}")))?;
123
124        let frames = Array::new();
125        for event in events {
126            if let PipelineEvent::VideoFrame(frame) = event {
127                frames.push(&Uint8Array::from(frame.data.as_slice()));
128            }
129        }
130        Ok(frames)
131    }
132
133    #[wasm_bindgen(js_name = pushDecrypted80211Frame)]
134    pub fn push_decrypted_80211_frame(
135        &mut self,
136        frame: &[u8],
137        fragment: &[u8],
138    ) -> Result<Array, JsValue> {
139        let events = self
140            .pipeline
141            .push_decrypted_80211_frame(frame, fragment)
142            .map_err(|err| JsValue::from_str(&format!("802.11 frame rejected: {err:?}")))?;
143        let frames = Array::new();
144        for event in events {
145            if let PipelineEvent::VideoFrame(frame) = event {
146                frames.push(&Uint8Array::from(frame.data.as_slice()));
147            }
148        }
149        Ok(frames)
150    }
151
152    #[wasm_bindgen(js_name = pushEncrypted80211Frame)]
153    pub fn push_encrypted_80211_frame(&mut self, frame: &[u8]) -> Result<Array, JsValue> {
154        let events = self
155            .pipeline
156            .push_80211_frame(frame)
157            .map_err(|err| JsValue::from_str(&format!("802.11 frame rejected: {err}")))?;
158        Ok(video_frames_from_events(events))
159    }
160
161    #[wasm_bindgen(js_name = pushRxTransfer)]
162    pub fn push_rx_transfer(&mut self, transfer: &[u8]) -> Result<Array, JsValue> {
163        let packets = parse_rx_aggregate(transfer)
164            .map_err(|err| JsValue::from_str(&format!("Realtek RX aggregate rejected: {err}")))?;
165        let frames = Array::new();
166        for packet in packets {
167            if packet.attrib.crc_err
168                || packet.attrib.icv_err
169                || packet.attrib.pkt_rpt_type != RxPacketType::NormalRx
170            {
171                continue;
172            }
173            let events = self
174                .pipeline
175                .push_80211_frame(packet.data)
176                .map_err(|err| JsValue::from_str(&format!("OpenIPC frame rejected: {err}")))?;
177            append_video_frames(&frames, events);
178        }
179        Ok(frames)
180    }
181
182    #[wasm_bindgen(
183        js_name = pushRxTransferDetailed,
184        unchecked_return_type = "OpenIpcVideoFrame[]"
185    )]
186    pub fn push_rx_transfer_detailed(&mut self, transfer: &[u8]) -> Result<Array, JsValue> {
187        self.push_rx_transfer_detailed_with_options(transfer, false)
188    }
189
190    #[wasm_bindgen(
191        js_name = pushRxTransferDetailedWithOptions,
192        unchecked_return_type = "OpenIpcVideoFrame[]"
193    )]
194    pub fn push_rx_transfer_detailed_with_options(
195        &mut self,
196        transfer: &[u8],
197        keep_corrupted: bool,
198    ) -> Result<Array, JsValue> {
199        let packets = parse_rx_aggregate(transfer)
200            .map_err(|err| JsValue::from_str(&format!("Realtek RX aggregate rejected: {err}")))?;
201        let frames = Array::new();
202        for packet in packets {
203            if !accept_rx_packet(packet.attrib, keep_corrupted) {
204                continue;
205            }
206            let events = self
207                .pipeline
208                .push_80211_frame(packet.data)
209                .map_err(|err| JsValue::from_str(&format!("OpenIPC frame rejected: {err}")))?;
210            append_video_frame_objects(&frames, events)?;
211        }
212        Ok(frames)
213    }
214
215    #[wasm_bindgen(
216        js_name = pushRxTransferProfiled,
217        unchecked_return_type = "OpenIpcRxTransferProfile"
218    )]
219    pub fn push_rx_transfer_profiled(&mut self, transfer: &[u8]) -> Result<Object, JsValue> {
220        self.push_rx_transfer_profiled_with_options(transfer, false)
221    }
222
223    #[wasm_bindgen(
224        js_name = pushRxTransferProfiledWithOptions,
225        unchecked_return_type = "OpenIpcRxTransferProfile"
226    )]
227    pub fn push_rx_transfer_profiled_with_options(
228        &mut self,
229        transfer: &[u8],
230        keep_corrupted: bool,
231    ) -> Result<Object, JsValue> {
232        let total_start = now_ms();
233        let parse_start = now_ms();
234        let packets = parse_rx_aggregate(transfer)
235            .map_err(|err| JsValue::from_str(&format!("Realtek RX aggregate rejected: {err}")))?;
236        let parse_ms = elapsed_ms(parse_start);
237
238        let frames = Array::new();
239        let mut accepted_packets = 0usize;
240        let mut crc_dropped = 0usize;
241        let mut icv_dropped = 0usize;
242        let mut report_dropped = 0usize;
243        let mut ignored_frames = 0usize;
244        let mut sessions = 0usize;
245        let mut wfb_payloads = 0usize;
246        let mut rtp_packets = 0usize;
247        let mut video_frames = 0usize;
248
249        let pipeline_start = now_ms();
250        let packet_count = packets.len();
251        for packet in packets {
252            if packet.attrib.crc_err && !keep_corrupted {
253                crc_dropped += 1;
254                continue;
255            }
256            if packet.attrib.icv_err && !keep_corrupted {
257                icv_dropped += 1;
258                continue;
259            }
260            if packet.attrib.pkt_rpt_type != RxPacketType::NormalRx {
261                report_dropped += 1;
262                continue;
263            }
264            accepted_packets += 1;
265            let events = self
266                .pipeline
267                .push_80211_frame(packet.data)
268                .map_err(|err| JsValue::from_str(&format!("OpenIPC frame rejected: {err}")))?;
269            for event in events {
270                match event {
271                    PipelineEvent::IgnoredFrame => ignored_frames += 1,
272                    PipelineEvent::SessionEstablished { .. } => sessions += 1,
273                    PipelineEvent::WfbPayload { .. } => wfb_payloads += 1,
274                    PipelineEvent::RtpPacket { .. } => rtp_packets += 1,
275                    PipelineEvent::VideoFrame(frame) => {
276                        video_frames += 1;
277                        frames.push(&video_frame_object(frame)?.into());
278                    }
279                }
280            }
281        }
282        let pipeline_ms = elapsed_ms(pipeline_start);
283
284        let object = Object::new();
285        Reflect::set(&object, &JsValue::from_str("frames"), &frames)?;
286        set_number(&object, "transferBytes", transfer.len() as f64)?;
287        set_number(&object, "packets", packet_count as f64)?;
288        set_number(&object, "acceptedPackets", accepted_packets as f64)?;
289        set_number(
290            &object,
291            "droppedPackets",
292            (crc_dropped + icv_dropped + report_dropped) as f64,
293        )?;
294        set_number(&object, "crcDropped", crc_dropped as f64)?;
295        set_number(&object, "icvDropped", icv_dropped as f64)?;
296        set_number(&object, "reportDropped", report_dropped as f64)?;
297        set_number(&object, "ignoredFrames", ignored_frames as f64)?;
298        set_number(&object, "sessions", sessions as f64)?;
299        set_number(&object, "wfbPayloads", wfb_payloads as f64)?;
300        set_number(&object, "rtpPackets", rtp_packets as f64)?;
301        set_number(&object, "videoFrames", video_frames as f64)?;
302        set_number(&object, "parseMs", parse_ms)?;
303        set_number(&object, "pipelineMs", pipeline_ms)?;
304        set_number(&object, "totalMs", elapsed_ms(total_start))?;
305        Ok(object)
306    }
307
308    #[wasm_bindgen(js_name = fecCounters)]
309    pub fn fec_counters(&self) -> String {
310        counters_json(self.pipeline.fec_counters())
311    }
312}
313
314#[wasm_bindgen]
315pub struct OpenIpcAdaptiveLink {
316    sender: AdaptiveLinkSender,
317    last_counters: FecCounters,
318    rx_channel_id: ChannelId,
319}
320
321#[wasm_bindgen]
322impl OpenIpcAdaptiveLink {
323    #[wasm_bindgen(constructor)]
324    pub fn new(
325        link_id: u32,
326        keypair: &[u8],
327        epoch: u64,
328        fec_k: usize,
329        fec_n: usize,
330    ) -> Result<OpenIpcAdaptiveLink, JsValue> {
331        let keypair = WfbTxKeypair::from_bytes(keypair)
332            .map_err(|err| JsValue::from_str(&format!("invalid adaptive-link keypair: {err}")))?;
333        let sender = AdaptiveLinkSender::new(link_id, keypair, epoch, fec_k, fec_n)
334            .map_err(|err| JsValue::from_str(&format!("invalid adaptive-link config: {err}")))?;
335        Ok(Self {
336            sender,
337            last_counters: FecCounters::default(),
338            rx_channel_id: ChannelId::from_link_port(link_id, RadioPort::Video),
339        })
340    }
341
342    #[wasm_bindgen(js_name = recordRx)]
343    pub fn record_rx(&mut self, now_ms: f64, rssi0: u8, rssi1: u8, snr0: i8, snr1: i8) {
344        self.sender
345            .link_mut()
346            .record_rx(ms_from_js(now_ms), rssi0, rssi1, snr0, snr1);
347    }
348
349    #[wasm_bindgen(js_name = recordRxTransfer)]
350    pub fn record_rx_transfer(&mut self, transfer: &[u8], now_ms: f64) -> Result<(), JsValue> {
351        let packets = parse_rx_aggregate(transfer)
352            .map_err(|err| JsValue::from_str(&format!("Realtek RX aggregate rejected: {err}")))?;
353        let now_ms = ms_from_js(now_ms);
354        for packet in packets {
355            if packet.attrib.crc_err
356                || packet.attrib.icv_err
357                || packet.attrib.pkt_rpt_type != RxPacketType::NormalRx
358            {
359                continue;
360            }
361            if !WifiFrame::parse(packet.data, FrameLayout::WithFcs)
362                .map(|frame| frame.matches_channel_id(self.rx_channel_id))
363                .unwrap_or(false)
364            {
365                continue;
366            }
367            self.sender
368                .record_rx_paths(now_ms, packet.attrib.rssi, packet.attrib.snr);
369        }
370        Ok(())
371    }
372
373    #[wasm_bindgen(js_name = recordReceiverCounters)]
374    pub fn record_receiver_counters(&mut self, receiver: &OpenIpcReceiver, now_ms: f64) {
375        self.record_counter_delta(ms_from_js(now_ms), receiver.pipeline.fec_counters());
376    }
377
378    #[wasm_bindgen(js_name = recordFec)]
379    pub fn record_fec(&mut self, now_ms: f64, total: u32, recovered: u32, lost: u32) {
380        self.sender
381            .record_fec(ms_from_js(now_ms), total, recovered, lost);
382    }
383
384    #[wasm_bindgen(js_name = requestKeyframe)]
385    pub fn request_keyframe(&mut self) {
386        self.sender.link_mut().request_keyframe();
387    }
388
389    #[wasm_bindgen(js_name = setKeyframeRequestMessages)]
390    pub fn set_keyframe_request_messages(&mut self, messages: u32) {
391        self.sender
392            .link_mut()
393            .set_keyframe_request_messages(messages);
394    }
395
396    #[wasm_bindgen(js_name = setVideoStartIdleMs)]
397    pub fn set_video_start_idle_ms(&mut self, idle_ms: u32) {
398        self.sender
399            .link_mut()
400            .set_video_start_idle_ms(idle_ms as u64);
401    }
402
403    #[wasm_bindgen(js_name = tick)]
404    pub fn tick(&mut self, now_ms: f64) -> Result<Array, JsValue> {
405        let frames = self
406            .sender
407            .tick(ms_from_js(now_ms))
408            .map_err(|err| JsValue::from_str(&format!("adaptive-link tick failed: {err}")))?;
409        let out = Array::new();
410        for frame in frames {
411            out.push(&Uint8Array::from(frame.as_slice()));
412        }
413        Ok(out)
414    }
415
416    #[wasm_bindgen(js_name = counters)]
417    pub fn counters(&self) -> String {
418        counters_json(self.last_counters)
419    }
420
421    #[wasm_bindgen(js_name = quality)]
422    pub fn quality(&mut self, now_ms: f64) -> String {
423        let quality = self.sender.link_mut().quality(ms_from_js(now_ms));
424        format!(
425            r#"{{"lostLastSecond":{},"recoveredLastSecond":{},"totalLastSecond":{},"rssi":[{},{}],"snr":[{},{}],"linkScore":[{},{}],"idrCode":"{}"}}"#,
426            quality.lost_last_second,
427            quality.recovered_last_second,
428            quality.total_last_second,
429            quality.rssi[0],
430            quality.rssi[1],
431            quality.snr[0],
432            quality.snr[1],
433            quality.link_score[0],
434            quality.link_score[1],
435            escape_json_str(&quality.idr_code),
436        )
437    }
438
439    fn record_counter_delta(&mut self, now_ms: u64, counters: FecCounters) {
440        let total = counters
441            .total_packets
442            .saturating_sub(self.last_counters.total_packets);
443        let recovered = counters
444            .recovered_packets
445            .saturating_sub(self.last_counters.recovered_packets);
446        let lost = counters
447            .lost_packets
448            .saturating_sub(self.last_counters.lost_packets);
449        self.last_counters = counters;
450        self.sender.record_fec(
451            now_ms,
452            total.min(u32::MAX as u64) as u32,
453            recovered.min(u32::MAX as u64) as u32,
454            lost.min(u32::MAX as u64) as u32,
455        );
456    }
457}
458
459#[cfg(target_arch = "wasm32")]
460#[wasm_bindgen]
461impl OpenIpcAdaptiveLink {
462    #[wasm_bindgen(js_name = tickAndSend)]
463    pub async fn tick_and_send(
464        &mut self,
465        device: &WebUsbRealtekDevice,
466        now_ms: f64,
467        current_channel: u8,
468    ) -> Result<usize, JsValue> {
469        let frames = self
470            .sender
471            .tick(ms_from_js(now_ms))
472            .map_err(|err| JsValue::from_str(&format!("adaptive-link tick failed: {err}")))?;
473        let count = frames.len();
474        for frame in frames {
475            device.send_packet(&frame, current_channel).await?;
476        }
477        Ok(count)
478    }
479}
480
481#[cfg(target_arch = "wasm32")]
482#[wasm_bindgen]
483pub struct WebUsbRealtekDevice {
484    driver: RealtekDevice,
485}
486
487#[cfg(target_arch = "wasm32")]
488#[wasm_bindgen]
489impl WebUsbRealtekDevice {
490    #[wasm_bindgen(js_name = fromWebUsbDevice)]
491    pub async fn from_web_usb_device(
492        device: web_sys::UsbDevice,
493    ) -> Result<WebUsbRealtekDevice, JsValue> {
494        let driver = RealtekDevice::from_web_usb_device(device)
495            .await
496            .map_err(driver_error)?;
497        Ok(Self { driver })
498    }
499
500    #[wasm_bindgen(js_name = fromWebUsbDeviceWithOptions)]
501    pub async fn from_web_usb_device_with_options(
502        device: web_sys::UsbDevice,
503        tx_endpoint_override: i32,
504    ) -> Result<WebUsbRealtekDevice, JsValue> {
505        Self::from_web_usb_device_advanced(device, tx_endpoint_override, -1, -1).await
506    }
507
508    #[wasm_bindgen(js_name = fromWebUsbDeviceAdvanced)]
509    pub async fn from_web_usb_device_advanced(
510        device: web_sys::UsbDevice,
511        tx_endpoint_override: i32,
512        target_vendor_id: i32,
513        target_product_id: i32,
514    ) -> Result<WebUsbRealtekDevice, JsValue> {
515        let driver = RealtekDevice::from_web_usb_device_with_options(
516            device,
517            DriverOptions {
518                tx_endpoint_override: optional_u8(tx_endpoint_override, "txEndpointOverride")?,
519                target_vendor_id: optional_u16(target_vendor_id, "targetVendorId")?,
520                target_product_id: optional_u16(target_product_id, "targetProductId")?,
521                ..DriverOptions::default()
522            },
523        )
524        .await
525        .map_err(driver_error)?;
526        Ok(Self { driver })
527    }
528
529    #[wasm_bindgen(js_name = bulkInEndpoint)]
530    pub fn bulk_in_endpoint(&self) -> u8 {
531        self.driver.bulk_in_ep
532    }
533
534    #[wasm_bindgen(js_name = bulkOutEndpoint)]
535    pub fn bulk_out_endpoint(&self) -> u8 {
536        self.driver.bulk_out_ep
537    }
538
539    #[wasm_bindgen(js_name = initializeMonitor)]
540    pub async fn initialize_monitor(
541        &self,
542        channel: u8,
543        channel_width_mhz: u16,
544        channel_offset: u8,
545    ) -> Result<String, JsValue> {
546        self.initialize_monitor_with_options(channel, channel_width_mhz, channel_offset, false)
547            .await
548    }
549
550    #[wasm_bindgen(js_name = initializeMonitorWithOptions)]
551    pub async fn initialize_monitor_with_options(
552        &self,
553        channel: u8,
554        channel_width_mhz: u16,
555        channel_offset: u8,
556        accept_bad_fcs: bool,
557    ) -> Result<String, JsValue> {
558        let radio = RadioConfig {
559            channel,
560            channel_offset,
561            channel_width: parse_channel_width(channel_width_mhz)?,
562        };
563        let report = self
564            .driver
565            .initialize_monitor_async(radio, accept_bad_fcs)
566            .await
567            .map_err(driver_error)?;
568        Ok(init_report_json(&report))
569    }
570
571    #[wasm_bindgen(js_name = initializeMonitorAdvanced)]
572    pub async fn initialize_monitor_advanced(
573        &self,
574        channel: u8,
575        channel_width_mhz: u16,
576        channel_offset: u8,
577        accept_bad_fcs: bool,
578        skip_tx_power: bool,
579        force_iqk: bool,
580        disable_iqk: bool,
581        firmware_8814_mode: String,
582        firmware_8814_chunk: i32,
583    ) -> Result<String, JsValue> {
584        let radio = RadioConfig {
585            channel,
586            channel_offset,
587            channel_width: parse_channel_width(channel_width_mhz)?,
588        };
589        let mode = if firmware_8814_mode.trim().is_empty() {
590            Firmware8814Mode::Kernel
591        } else {
592            Firmware8814Mode::from_env_value(&firmware_8814_mode).ok_or_else(|| {
593                JsValue::from_str("firmware8814Mode must be \"kernel\" or \"rtw88\"")
594            })?
595        };
596        let options = MonitorOptions {
597            accept_bad_fcs,
598            skip_tx_power,
599            force_iqk,
600            disable_iqk,
601            firmware_8814_mode: mode,
602            firmware_8814_chunk: optional_usize(firmware_8814_chunk, "firmware8814Chunk")?,
603        };
604        let report = self
605            .driver
606            .initialize_monitor_with_options_async(radio, options)
607            .await
608            .map_err(driver_error)?;
609        Ok(init_report_json(&report))
610    }
611
612    #[wasm_bindgen(js_name = readRxTransfer)]
613    pub async fn read_rx_transfer(&self, length: usize) -> Result<Uint8Array, JsValue> {
614        let bytes = self
615            .driver
616            .read_rx_transfer_async(length)
617            .await
618            .map_err(driver_error)?;
619        Ok(Uint8Array::from(bytes.as_slice()))
620    }
621
622    #[wasm_bindgen(js_name = readRxTransfers)]
623    pub async fn read_rx_transfers(
624        &self,
625        length: usize,
626        in_flight: usize,
627    ) -> Result<Array, JsValue> {
628        let transfers = self
629            .driver
630            .read_rx_transfers_async(length, in_flight)
631            .await
632            .map_err(driver_error)?;
633        let out = Array::new();
634        for transfer in transfers {
635            out.push(&Uint8Array::from(transfer.as_slice()));
636        }
637        Ok(out)
638    }
639
640    #[wasm_bindgen(js_name = writeTxTransfer)]
641    pub async fn write_tx_transfer(&self, transfer: &[u8]) -> Result<usize, JsValue> {
642        self.driver
643            .write_tx_transfer_async(transfer)
644            .await
645            .map_err(driver_error)
646    }
647
648    #[wasm_bindgen(js_name = sendPacket)]
649    pub async fn send_packet(
650        &self,
651        radiotap_packet: &[u8],
652        current_channel: u8,
653    ) -> Result<usize, JsValue> {
654        self.send_packet_with_options(radiotap_packet, current_channel, false)
655            .await
656    }
657
658    #[wasm_bindgen(js_name = sendPacketWithOptions)]
659    pub async fn send_packet_with_options(
660        &self,
661        radiotap_packet: &[u8],
662        current_channel: u8,
663        legacy_8812_descriptor: bool,
664    ) -> Result<usize, JsValue> {
665        let chip = self.driver.probe_chip_async().await.map_err(driver_error)?;
666        self.driver
667            .send_packet_async(
668                radiotap_packet,
669                RealtekTxOptions {
670                    current_channel,
671                    is_8814a: chip.family == openipc_rtl88xx::ChipFamily::Rtl8814,
672                    legacy_8812_descriptor,
673                    ..RealtekTxOptions::default()
674                },
675            )
676            .await
677            .map_err(driver_error)
678    }
679
680    #[wasm_bindgen(js_name = setTxPowerOverride)]
681    pub async fn set_tx_power_override(
682        &self,
683        current_channel: u8,
684        power: u8,
685    ) -> Result<(), JsValue> {
686        self.driver
687            .set_tx_power_override_async(current_channel, power)
688            .await
689            .map_err(driver_error)
690    }
691
692    #[wasm_bindgen(js_name = readThermalStatus)]
693    pub async fn read_thermal_status(&self) -> Result<String, JsValue> {
694        let status = self
695            .driver
696            .read_thermal_status_async()
697            .await
698            .map_err(driver_error)?;
699        Ok(format!(
700            r#"{{"raw":{},"baseline":{},"delta":{},"valid":{},"bucket":"{}"}}"#,
701            status.raw,
702            status.baseline,
703            status.delta,
704            status.valid,
705            thermal_bucket_name(status.bucket())
706        ))
707    }
708
709    #[wasm_bindgen(js_name = readQueueDepth8814)]
710    pub async fn read_queue_depth_8814(&self) -> Result<String, JsValue> {
711        let regs = self
712            .driver
713            .read_queue_depth_8814_async()
714            .await
715            .map_err(driver_error)?;
716        Ok(format!(
717            r#"[{},{},{},{},{}]"#,
718            regs[0], regs[1], regs[2], regs[3], regs[4]
719        ))
720    }
721
722    #[wasm_bindgen(js_name = readBbReg)]
723    pub async fn read_bb_reg(&self, register: u16, mask: u32) -> Result<u32, JsValue> {
724        self.driver
725            .read_bb_reg_async(register, mask)
726            .await
727            .map_err(driver_error)
728    }
729
730    #[wasm_bindgen(js_name = readBbDbgport)]
731    pub async fn read_bb_dbgport(&self, selector: u32) -> Result<String, JsValue> {
732        let read = self
733            .driver
734            .read_bb_dbgport_async(selector)
735            .await
736            .map_err(driver_error)?;
737        Ok(format!(
738            r#"{{"selector":{},"value":{},"savedSelector":{},"chipAlive":{}}}"#,
739            read.selector, read.value, read.saved_selector, read.chip_alive
740        ))
741    }
742
743    #[wasm_bindgen(js_name = readFalseAlarmCounters)]
744    pub async fn read_false_alarm_counters(&self) -> Result<String, JsValue> {
745        let counters = self
746            .driver
747            .read_false_alarm_counters_async()
748            .await
749            .map_err(driver_error)?;
750        Ok(false_alarm_counters_json(counters))
751    }
752
753    #[wasm_bindgen(js_name = runIqk)]
754    pub async fn run_iqk(&self, channel: u8) -> Result<String, JsValue> {
755        let chip = self.driver.probe_chip_async().await.map_err(driver_error)?;
756        let report = self
757            .driver
758            .run_iqk_async(chip, channel)
759            .await
760            .map_err(driver_error)?;
761        Ok(iqk_report_json(report))
762    }
763
764    #[wasm_bindgen(js_name = readRegisterU8)]
765    pub async fn read_register_u8(&self, register: u16) -> Result<u8, JsValue> {
766        self.driver
767            .read_u8_async(register)
768            .await
769            .map_err(driver_error)
770    }
771
772    #[wasm_bindgen(js_name = readRegisterU32)]
773    pub async fn read_register_u32(&self, register: u16) -> Result<u32, JsValue> {
774        self.driver
775            .read_u32_async(register)
776            .await
777            .map_err(driver_error)
778    }
779}
780
781#[cfg(target_arch = "wasm32")]
782#[wasm_bindgen]
783pub struct WebUsbPhydmWatchdog {
784    state: PhydmDigState,
785}
786
787#[cfg(target_arch = "wasm32")]
788#[wasm_bindgen]
789impl WebUsbPhydmWatchdog {
790    #[wasm_bindgen(constructor)]
791    pub fn new() -> Self {
792        Self {
793            state: PhydmDigState::default(),
794        }
795    }
796
797    #[wasm_bindgen(js_name = tick)]
798    pub async fn tick(&mut self, device: &WebUsbRealtekDevice) -> Result<String, JsValue> {
799        let report = device
800            .driver
801            .run_phydm_watchdog_tick_async(&mut self.state)
802            .await
803            .map_err(driver_error)?;
804        Ok(phydm_watchdog_report_json(report))
805    }
806}
807
808#[cfg(target_arch = "wasm32")]
809#[wasm_bindgen]
810pub struct WebUsbPowerTracking8812 {
811    state: PowerTrackingState,
812}
813
814#[cfg(target_arch = "wasm32")]
815#[wasm_bindgen]
816impl WebUsbPowerTracking8812 {
817    #[wasm_bindgen(constructor)]
818    pub fn new() -> Self {
819        Self {
820            state: PowerTrackingState::default(),
821        }
822    }
823
824    #[wasm_bindgen(js_name = init)]
825    pub async fn init(&mut self, device: &WebUsbRealtekDevice) -> Result<(), JsValue> {
826        device
827            .driver
828            .init_power_tracking_8812_async(&mut self.state)
829            .await
830            .map_err(driver_error)
831    }
832
833    #[wasm_bindgen(js_name = clear)]
834    pub async fn clear(&mut self, device: &WebUsbRealtekDevice) -> Result<(), JsValue> {
835        device
836            .driver
837            .clear_power_tracking_8812_async(&mut self.state)
838            .await
839            .map_err(driver_error)
840    }
841
842    #[wasm_bindgen(js_name = tick)]
843    pub async fn tick(
844        &mut self,
845        device: &WebUsbRealtekDevice,
846        channel: u8,
847        channel_width_mhz: u16,
848    ) -> Result<String, JsValue> {
849        let report = device
850            .driver
851            .tick_power_tracking_8812_async(
852                &mut self.state,
853                channel,
854                parse_channel_width(channel_width_mhz)?,
855            )
856            .await
857            .map_err(driver_error)?;
858        Ok(power_tracking_report_json(report))
859    }
860}
861
862#[cfg(target_arch = "wasm32")]
863fn thermal_bucket_name(bucket: ThermalBucket) -> &'static str {
864    match bucket {
865        ThermalBucket::Unknown => "unknown",
866        ThermalBucket::Cool => "cool",
867        ThermalBucket::Warm => "warm",
868        ThermalBucket::Hot => "hot",
869        ThermalBucket::Critical => "critical",
870    }
871}
872
873#[cfg(target_arch = "wasm32")]
874fn false_alarm_counters_json(counters: FalseAlarmCounters) -> String {
875    format!(
876        r#"{{"ofdmFail":{},"cckFail":{},"ofdmCca":{},"cckCca":{},"cckCrcOk":{},"cckCrcError":{},"ofdmCrcOk":{},"ofdmCrcError":{},"htCrcOk":{},"htCrcError":{},"vhtCrcOk":{},"vhtCrcError":{},"all":{},"ccaAll":{}}}"#,
877        counters.cnt_ofdm_fail,
878        counters.cnt_cck_fail,
879        counters.cnt_ofdm_cca,
880        counters.cnt_cck_cca,
881        counters.cnt_cck_crc32_ok,
882        counters.cnt_cck_crc32_error,
883        counters.cnt_ofdm_crc32_ok,
884        counters.cnt_ofdm_crc32_error,
885        counters.cnt_ht_crc32_ok,
886        counters.cnt_ht_crc32_error,
887        counters.cnt_vht_crc32_ok,
888        counters.cnt_vht_crc32_error,
889        counters.cnt_all,
890        counters.cnt_cca_all
891    )
892}
893
894#[cfg(target_arch = "wasm32")]
895fn phydm_watchdog_report_json(report: PhydmWatchdogReport) -> String {
896    format!(
897        r#"{{"previousIgi":{},"currentIgi":{},"counters":{}}}"#,
898        report.previous_igi,
899        report.current_igi,
900        false_alarm_counters_json(report.counters)
901    )
902}
903
904#[cfg(target_arch = "wasm32")]
905fn power_tracking_report_json(report: PowerTrackingReport) -> String {
906    format!(
907        r#"{{"enabled":{},"thermalRaw":{},"thermalAverage":{},"eepromThermal":{},"delta":{},"defaultOfdmIndex":{},"finalOfdmIndex":[{},{}],"swingDelta":[{},{}],"applied":{}}}"#,
908        report.enabled,
909        report.thermal_raw,
910        report.thermal_average,
911        report.eeprom_thermal,
912        report.delta,
913        report.default_ofdm_index,
914        report.final_ofdm_index[0],
915        report.final_ofdm_index[1],
916        report.swing_delta[0],
917        report.swing_delta[1],
918        report.applied
919    )
920}
921
922#[cfg(target_arch = "wasm32")]
923fn iqk_report_json(report: IqkReport) -> String {
924    format!(
925        r#"{{"chip":"{}","channel":{},"ran":{}}}"#,
926        report.chip.family.name(),
927        report.channel,
928        report.ran
929    )
930}
931
932#[cfg(target_arch = "wasm32")]
933fn parse_channel_width(width_mhz: u16) -> Result<ChannelWidth, JsValue> {
934    match width_mhz {
935        20 => Ok(ChannelWidth::Mhz20),
936        40 => Ok(ChannelWidth::Mhz40),
937        80 => Ok(ChannelWidth::Mhz80),
938        _ => Err(JsValue::from_str(
939            "unsupported channel width; expected 20, 40, or 80 MHz",
940        )),
941    }
942}
943
944#[cfg(target_arch = "wasm32")]
945fn optional_u8(value: i32, name: &str) -> Result<Option<u8>, JsValue> {
946    if value < 0 {
947        return Ok(None);
948    }
949    u8::try_from(value)
950        .map(Some)
951        .map_err(|_| JsValue::from_str(&format!("{name} is outside 0..255")))
952}
953
954#[cfg(target_arch = "wasm32")]
955fn optional_u16(value: i32, name: &str) -> Result<Option<u16>, JsValue> {
956    if value < 0 {
957        return Ok(None);
958    }
959    u16::try_from(value)
960        .map(Some)
961        .map_err(|_| JsValue::from_str(&format!("{name} is outside 0..65535")))
962}
963
964#[cfg(target_arch = "wasm32")]
965fn optional_usize(value: i32, name: &str) -> Result<Option<usize>, JsValue> {
966    if value < 0 {
967        return Ok(None);
968    }
969    usize::try_from(value)
970        .map(Some)
971        .map_err(|_| JsValue::from_str(&format!("{name} is invalid")))
972}
973
974#[cfg(target_arch = "wasm32")]
975fn init_report_json(report: &InitReport) -> String {
976    let status = match report.status {
977        InitStatus::AlreadyRunning => "already_running",
978        InitStatus::Initialized => "initialized",
979    };
980    format!(
981        r#"{{"chip":"{}","rfPaths":{},"cutVersion":{},"status":"{}","firmwareDownloaded":{}}}"#,
982        report.chip.family.name(),
983        report.chip.total_rf_paths(),
984        report.chip.cut_version,
985        status,
986        report.firmware_downloaded
987    )
988}
989
990#[cfg(target_arch = "wasm32")]
991fn driver_error(err: impl std::fmt::Display) -> JsValue {
992    JsValue::from_str(&err.to_string())
993}
994
995fn video_frames_from_events(events: Vec<PipelineEvent>) -> Array {
996    let frames = Array::new();
997    append_video_frames(&frames, events);
998    frames
999}
1000
1001fn append_video_frames(frames: &Array, events: Vec<PipelineEvent>) {
1002    for event in events {
1003        if let PipelineEvent::VideoFrame(frame) = event {
1004            frames.push(&Uint8Array::from(frame.data.as_slice()));
1005        }
1006    }
1007}
1008
1009fn append_video_frame_objects(frames: &Array, events: Vec<PipelineEvent>) -> Result<(), JsValue> {
1010    for event in events {
1011        if let PipelineEvent::VideoFrame(frame) = event {
1012            frames.push(&video_frame_object(frame)?.into());
1013        }
1014    }
1015    Ok(())
1016}
1017
1018fn accept_rx_packet(attrib: openipc_core::realtek::RxPacketAttrib, keep_corrupted: bool) -> bool {
1019    attrib.pkt_rpt_type == RxPacketType::NormalRx
1020        && (keep_corrupted || (!attrib.crc_err && !attrib.icv_err))
1021}
1022
1023fn video_frame_object(frame: DepacketizedFrame) -> Result<Object, JsValue> {
1024    let object = Object::new();
1025    let codec_string = codec_string(&frame);
1026    Reflect::set(
1027        &object,
1028        &JsValue::from_str("data"),
1029        &Uint8Array::from(frame.data.as_slice()),
1030    )?;
1031    Reflect::set(
1032        &object,
1033        &JsValue::from_str("codec"),
1034        &JsValue::from_str(codec_name(frame.codec)),
1035    )?;
1036    Reflect::set(
1037        &object,
1038        &JsValue::from_str("codecString"),
1039        &JsValue::from_str(&codec_string),
1040    )?;
1041    Reflect::set(
1042        &object,
1043        &JsValue::from_str("isKeyFrame"),
1044        &JsValue::from_bool(frame.is_keyframe),
1045    )?;
1046    Reflect::set(
1047        &object,
1048        &JsValue::from_str("timestamp"),
1049        &JsValue::from_f64(f64::from(frame.timestamp)),
1050    )?;
1051    Ok(object)
1052}
1053
1054fn codec_name(codec: Codec) -> &'static str {
1055    match codec {
1056        Codec::H264 => "h264",
1057        Codec::H265 => "h265",
1058    }
1059}
1060
1061fn codec_string(frame: &DepacketizedFrame) -> String {
1062    match frame.codec {
1063        Codec::H264 => h264_codec_string(&frame.data).unwrap_or_else(|| "avc1.42E01E".to_owned()),
1064        Codec::H265 => "hev1.1.6.L93.B0".to_owned(),
1065    }
1066}
1067
1068fn h264_codec_string(frame: &[u8]) -> Option<String> {
1069    for unit in annex_b_units(frame) {
1070        let nalu = &frame[unit.start..unit.end];
1071        if nalu.len() >= 4 && nalu[0] & 0x1f == 7 {
1072            return Some(format!(
1073                "avc1.{}{}{}",
1074                hex_byte(nalu[1]),
1075                hex_byte(nalu[2]),
1076                hex_byte(nalu[3])
1077            ));
1078        }
1079    }
1080    None
1081}
1082
1083#[derive(Debug, Clone, Copy)]
1084struct AnnexBUnit {
1085    start: usize,
1086    end: usize,
1087}
1088
1089fn annex_b_units(frame: &[u8]) -> Vec<AnnexBUnit> {
1090    let mut starts = Vec::new();
1091    let mut index = 0;
1092    while index + 3 < frame.len() {
1093        let len = start_code_len(frame, index);
1094        if len > 0 {
1095            starts.push(index);
1096            index += len;
1097        } else {
1098            index += 1;
1099        }
1100    }
1101    if starts.is_empty() && !frame.is_empty() {
1102        return vec![AnnexBUnit {
1103            start: 0,
1104            end: frame.len(),
1105        }];
1106    }
1107    starts
1108        .iter()
1109        .enumerate()
1110        .map(|(index, start)| AnnexBUnit {
1111            start: start + start_code_len(frame, *start),
1112            end: starts.get(index + 1).copied().unwrap_or(frame.len()),
1113        })
1114        .collect()
1115}
1116
1117fn start_code_len(frame: &[u8], offset: usize) -> usize {
1118    if frame.get(offset) != Some(&0) || frame.get(offset + 1) != Some(&0) {
1119        return 0;
1120    }
1121    if frame.get(offset + 2) == Some(&1) {
1122        return 3;
1123    }
1124    if frame.get(offset + 2) == Some(&0) && frame.get(offset + 3) == Some(&1) {
1125        return 4;
1126    }
1127    0
1128}
1129
1130fn hex_byte(value: u8) -> String {
1131    format!("{value:02X}")
1132}
1133
1134fn set_number(object: &Object, key: &str, value: f64) -> Result<(), JsValue> {
1135    Reflect::set(object, &JsValue::from_str(key), &JsValue::from_f64(value))?;
1136    Ok(())
1137}
1138
1139fn now_ms() -> f64 {
1140    #[cfg(target_arch = "wasm32")]
1141    {
1142        web_sys::window()
1143            .and_then(|window| window.performance())
1144            .map(|performance| performance.now())
1145            .unwrap_or_else(js_sys::Date::now)
1146    }
1147    #[cfg(not(target_arch = "wasm32"))]
1148    {
1149        0.0
1150    }
1151}
1152
1153fn elapsed_ms(start_ms: f64) -> f64 {
1154    let elapsed = now_ms() - start_ms;
1155    if elapsed.is_finite() && elapsed >= 0.0 {
1156        elapsed
1157    } else {
1158        0.0
1159    }
1160}
1161
1162fn counters_json(counters: FecCounters) -> String {
1163    format!(
1164        r#"{{"totalPackets":{},"recoveredPackets":{},"lostPackets":{},"badPackets":{}}}"#,
1165        counters.total_packets,
1166        counters.recovered_packets,
1167        counters.lost_packets,
1168        counters.bad_packets
1169    )
1170}
1171
1172fn escape_json_str(value: &str) -> String {
1173    let mut out = String::with_capacity(value.len());
1174    for ch in value.chars() {
1175        match ch {
1176            '"' => out.push_str("\\\""),
1177            '\\' => out.push_str("\\\\"),
1178            '\n' => out.push_str("\\n"),
1179            '\r' => out.push_str("\\r"),
1180            '\t' => out.push_str("\\t"),
1181            _ => out.push(ch),
1182        }
1183    }
1184    out
1185}
1186
1187fn ms_from_js(now_ms: f64) -> u64 {
1188    if now_ms.is_finite() && now_ms > 0.0 {
1189        now_ms.min(u64::MAX as f64) as u64
1190    } else {
1191        0
1192    }
1193}
1194
1195#[wasm_bindgen(js_name = supportedUsbFilters)]
1196pub fn supported_usb_filters() -> String {
1197    // Kept as JSON to avoid forcing web-sys types into the Rust API.
1198    r#"[{"vendorId":3034,"productId":34834},{"vendorId":3034,"productId":2065},{"vendorId":3034,"productId":43025},{"vendorId":3034,"productId":47121},{"vendorId":3034,"productId":34835},{"vendorId":9047,"productId":288}]"#.to_owned()
1199}
1200
1201#[cfg(target_arch = "wasm32")]
1202#[wasm_bindgen(js_name = listAuthorizedUsbDevices)]
1203pub async fn list_authorized_usb_devices() -> Result<Array, JsValue> {
1204    let devices = nusb::list_devices()
1205        .await
1206        .map_err(|err| JsValue::from_str(&format!("nusb list_devices failed: {err}")))?;
1207
1208    let out = Array::new();
1209    for device in devices {
1210        let obj = Object::new();
1211        Reflect::set(
1212            &obj,
1213            &JsValue::from_str("vendorId"),
1214            &JsValue::from_f64(device.vendor_id() as f64),
1215        )?;
1216        Reflect::set(
1217            &obj,
1218            &JsValue::from_str("productId"),
1219            &JsValue::from_f64(device.product_id() as f64),
1220        )?;
1221        if let Some(product) = device.product_string() {
1222            Reflect::set(
1223                &obj,
1224                &JsValue::from_str("product"),
1225                &JsValue::from_str(product),
1226            )?;
1227        }
1228        if let Some(manufacturer) = device.manufacturer_string() {
1229            Reflect::set(
1230                &obj,
1231                &JsValue::from_str("manufacturer"),
1232                &JsValue::from_str(manufacturer),
1233            )?;
1234        }
1235        out.push(&obj);
1236    }
1237    Ok(out)
1238}
1239
1240fn parse_hex_u64(input: &str) -> Result<u64, JsValue> {
1241    let trimmed = input.trim();
1242    let hex = trimmed
1243        .strip_prefix("0x")
1244        .or_else(|| trimmed.strip_prefix("0X"))
1245        .unwrap_or(trimmed);
1246    u64::from_str_radix(hex, 16)
1247        .map_err(|err| JsValue::from_str(&format!("invalid nonce hex: {err}")))
1248}
1249
1250#[cfg(test)]
1251mod tests {
1252    use super::*;
1253
1254    #[test]
1255    fn h264_codec_string_comes_from_annex_b_sps() {
1256        let frame = [0, 0, 0, 1, 0x67, 0x64, 0x00, 0x1f, 0xac, 0xd9];
1257        assert_eq!(h264_codec_string(&frame).as_deref(), Some("avc1.64001F"));
1258    }
1259}