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 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}