1use js_sys::{Array, Uint8Array};
2use openipc_core::ieee80211::WifiFrame;
3use openipc_core::realtek::{parse_rx_aggregate_with_kind, RxDescriptorKind, RxPacketType};
4use openipc_core::{
5 AdaptiveLinkSender, ChannelId, FecCounters, FrameLayout, RadioPort, WfbTxKeypair,
6};
7use wasm_bindgen::prelude::*;
8
9use crate::js::{counters_json, escape_json_str, ms_from_js};
10use crate::receiver::{parse_rx_descriptor_kind, OpenIpcReceiver};
11#[cfg(target_arch = "wasm32")]
12use crate::webusb::WebUsbRealtekDevice;
13
14#[wasm_bindgen]
15pub struct OpenIpcAdaptiveLink {
20 sender: AdaptiveLinkSender,
21 last_counters: FecCounters,
22 rx_channel_id: ChannelId,
23 rx_descriptor_kind: RxDescriptorKind,
24}
25
26#[wasm_bindgen]
27impl OpenIpcAdaptiveLink {
28 #[wasm_bindgen(constructor)]
29 pub fn new(
31 link_id: u32,
32 keypair: &[u8],
33 epoch: u64,
34 fec_k: usize,
35 fec_n: usize,
36 ) -> Result<OpenIpcAdaptiveLink, JsValue> {
37 let keypair = WfbTxKeypair::from_bytes(keypair)
38 .map_err(|err| JsValue::from_str(&format!("invalid adaptive-link keypair: {err}")))?;
39 let sender = AdaptiveLinkSender::new(link_id, keypair, epoch, fec_k, fec_n)
40 .map_err(|err| JsValue::from_str(&format!("invalid adaptive-link config: {err}")))?;
41 Ok(Self {
42 sender,
43 last_counters: FecCounters::default(),
44 rx_channel_id: ChannelId::from_link_port(link_id, RadioPort::Video),
45 rx_descriptor_kind: RxDescriptorKind::Jaguar1,
46 })
47 }
48
49 #[wasm_bindgen(js_name = setRxDescriptorKind)]
50 pub fn set_rx_descriptor_kind(&mut self, kind: &str) -> Result<(), JsValue> {
52 self.rx_descriptor_kind = parse_rx_descriptor_kind(kind)?;
53 Ok(())
54 }
55
56 #[wasm_bindgen(js_name = recordRx)]
57 pub fn record_rx(&mut self, now_ms: f64, rssi0: u8, rssi1: u8, snr0: i8, snr1: i8) {
59 self.sender
60 .link_mut()
61 .record_rx(ms_from_js(now_ms), rssi0, rssi1, snr0, snr1);
62 }
63
64 #[wasm_bindgen(js_name = recordRxTransfer)]
65 pub fn record_rx_transfer(&mut self, transfer: &[u8], now_ms: f64) -> Result<(), JsValue> {
67 let packets = parse_rx_aggregate_with_kind(transfer, self.rx_descriptor_kind)
68 .map_err(|err| JsValue::from_str(&format!("Realtek RX aggregate rejected: {err}")))?;
69 let now_ms = ms_from_js(now_ms);
70 for packet in packets {
71 if packet.attrib.crc_err
72 || packet.attrib.icv_err
73 || packet.attrib.pkt_rpt_type != RxPacketType::NormalRx
74 {
75 continue;
76 }
77 if !WifiFrame::parse(packet.data, FrameLayout::WithFcs)
78 .map(|frame| frame.matches_channel_id(self.rx_channel_id))
79 .unwrap_or(false)
80 {
81 continue;
82 }
83 self.sender
84 .record_rx_paths(now_ms, packet.attrib.rssi, packet.attrib.snr);
85 }
86 Ok(())
87 }
88
89 #[wasm_bindgen(js_name = recordReceiverCounters)]
90 pub fn record_receiver_counters(&mut self, receiver: &OpenIpcReceiver, now_ms: f64) {
92 self.record_counter_delta(ms_from_js(now_ms), receiver.video_fec_counters());
93 }
94
95 #[wasm_bindgen(js_name = recordFec)]
96 pub fn record_fec(&mut self, now_ms: f64, total: u32, recovered: u32, lost: u32) {
98 self.sender
99 .record_fec(ms_from_js(now_ms), total, recovered, lost);
100 }
101
102 #[wasm_bindgen(js_name = requestKeyframe)]
103 pub fn request_keyframe(&mut self) {
105 self.sender.link_mut().request_keyframe();
106 }
107
108 #[wasm_bindgen(js_name = setKeyframeRequestMessages)]
109 pub fn set_keyframe_request_messages(&mut self, messages: u32) {
111 self.sender
112 .link_mut()
113 .set_keyframe_request_messages(messages);
114 }
115
116 #[wasm_bindgen(js_name = setVideoStartIdleMs)]
117 pub fn set_video_start_idle_ms(&mut self, idle_ms: u32) {
119 self.sender
120 .link_mut()
121 .set_video_start_idle_ms(idle_ms as u64);
122 }
123
124 #[wasm_bindgen(js_name = tick)]
125 pub fn tick(&mut self, now_ms: f64) -> Result<Array, JsValue> {
127 let frames = self
128 .sender
129 .tick(ms_from_js(now_ms))
130 .map_err(|err| JsValue::from_str(&format!("adaptive-link tick failed: {err}")))?;
131 let out = Array::new();
132 for frame in frames {
133 out.push(&Uint8Array::from(frame.as_slice()));
134 }
135 Ok(out)
136 }
137
138 #[wasm_bindgen(js_name = counters)]
139 pub fn counters(&self) -> String {
141 counters_json(self.last_counters)
142 }
143
144 #[wasm_bindgen(js_name = quality)]
145 pub fn quality(&mut self, now_ms: f64) -> String {
147 let quality = self.sender.link_mut().quality(ms_from_js(now_ms));
148 format!(
149 r#"{{"lostLastSecond":{},"recoveredLastSecond":{},"totalLastSecond":{},"rssi":[{},{}],"snr":[{},{}],"linkScore":[{},{}],"idrCode":"{}"}}"#,
150 quality.lost_last_second,
151 quality.recovered_last_second,
152 quality.total_last_second,
153 quality.rssi[0],
154 quality.rssi[1],
155 quality.snr[0],
156 quality.snr[1],
157 quality.link_score[0],
158 quality.link_score[1],
159 escape_json_str(&quality.idr_code),
160 )
161 }
162
163 fn record_counter_delta(&mut self, now_ms: u64, counters: FecCounters) {
164 let total = counters
165 .total_packets
166 .saturating_sub(self.last_counters.total_packets);
167 let recovered = counters
168 .recovered_packets
169 .saturating_sub(self.last_counters.recovered_packets);
170 let lost = counters
171 .lost_packets
172 .saturating_sub(self.last_counters.lost_packets);
173 self.last_counters = counters;
174 self.sender.record_fec(
175 now_ms,
176 total.min(u32::MAX as u64) as u32,
177 recovered.min(u32::MAX as u64) as u32,
178 lost.min(u32::MAX as u64) as u32,
179 );
180 }
181}
182
183#[cfg(target_arch = "wasm32")]
184#[wasm_bindgen]
185impl OpenIpcAdaptiveLink {
186 #[wasm_bindgen(js_name = tickAndSend)]
187 pub async fn tick_and_send(
189 &mut self,
190 device: &WebUsbRealtekDevice,
191 now_ms: f64,
192 current_channel: u8,
193 ) -> Result<usize, JsValue> {
194 let frames = self
195 .sender
196 .tick(ms_from_js(now_ms))
197 .map_err(|err| JsValue::from_str(&format!("adaptive-link tick failed: {err}")))?;
198 let count = frames.len();
199 for frame in frames {
200 device.send_packet(&frame, current_channel).await?;
201 }
202 Ok(count)
203 }
204}