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
26impl OpenIpcAdaptiveLink {
27 #[cfg(target_arch = "wasm32")]
28 pub(crate) fn record_rx_paths(&mut self, now_ms: u64, rssi: [u8; 4], snr: [i8; 4]) {
29 self.sender.record_rx_paths(now_ms, rssi, snr);
30 }
31
32 #[cfg(target_arch = "wasm32")]
33 pub(crate) fn record_counters(&mut self, now_ms: u64, counters: FecCounters) {
34 self.record_counter_delta(now_ms, counters);
35 }
36}
37
38#[wasm_bindgen]
39impl OpenIpcAdaptiveLink {
40 #[wasm_bindgen(constructor)]
41 pub fn new(
43 link_id: u32,
44 keypair: &[u8],
45 epoch: u64,
46 fec_k: usize,
47 fec_n: usize,
48 ) -> Result<OpenIpcAdaptiveLink, JsValue> {
49 let keypair = WfbTxKeypair::from_bytes(keypair)
50 .map_err(|err| JsValue::from_str(&format!("invalid adaptive-link keypair: {err}")))?;
51 let sender = AdaptiveLinkSender::new(link_id, keypair, epoch, fec_k, fec_n)
52 .map_err(|err| JsValue::from_str(&format!("invalid adaptive-link config: {err}")))?;
53 Ok(Self {
54 sender,
55 last_counters: FecCounters::default(),
56 rx_channel_id: ChannelId::from_link_port(link_id, RadioPort::Video),
57 rx_descriptor_kind: RxDescriptorKind::Jaguar1,
58 })
59 }
60
61 #[wasm_bindgen(js_name = setRxDescriptorKind)]
62 pub fn set_rx_descriptor_kind(&mut self, kind: &str) -> Result<(), JsValue> {
64 self.rx_descriptor_kind = parse_rx_descriptor_kind(kind)?;
65 Ok(())
66 }
67
68 #[wasm_bindgen(js_name = recordRx)]
69 pub fn record_rx(&mut self, now_ms: f64, rssi0: u8, rssi1: u8, snr0: i8, snr1: i8) {
71 self.sender
72 .link_mut()
73 .record_rx(ms_from_js(now_ms), rssi0, rssi1, snr0, snr1);
74 }
75
76 #[wasm_bindgen(js_name = recordRxTransfer)]
77 pub fn record_rx_transfer(&mut self, transfer: &[u8], now_ms: f64) -> Result<(), JsValue> {
79 let packets = parse_rx_aggregate_with_kind(transfer, self.rx_descriptor_kind)
80 .map_err(|err| JsValue::from_str(&format!("Realtek RX aggregate rejected: {err}")))?;
81 let now_ms = ms_from_js(now_ms);
82 for packet in packets {
83 if packet.attrib.crc_err
84 || packet.attrib.icv_err
85 || packet.attrib.pkt_rpt_type != RxPacketType::NormalRx
86 {
87 continue;
88 }
89 if !WifiFrame::parse(packet.data, FrameLayout::WithFcs)
90 .map(|frame| frame.matches_channel_id(self.rx_channel_id))
91 .unwrap_or(false)
92 {
93 continue;
94 }
95 self.sender
96 .record_rx_paths(now_ms, packet.attrib.rssi, packet.attrib.snr);
97 }
98 Ok(())
99 }
100
101 #[wasm_bindgen(js_name = recordReceiverCounters)]
102 pub fn record_receiver_counters(&mut self, receiver: &OpenIpcReceiver, now_ms: f64) {
104 self.record_counter_delta(ms_from_js(now_ms), receiver.video_fec_counters());
105 }
106
107 #[wasm_bindgen(js_name = recordFec)]
108 pub fn record_fec(&mut self, now_ms: f64, total: u32, recovered: u32, lost: u32) {
110 self.sender
111 .record_fec(ms_from_js(now_ms), total, recovered, lost);
112 }
113
114 #[wasm_bindgen(js_name = requestKeyframe)]
115 pub fn request_keyframe(&mut self) {
117 self.sender.link_mut().request_keyframe();
118 }
119
120 #[wasm_bindgen(js_name = setKeyframeRequestMessages)]
121 pub fn set_keyframe_request_messages(&mut self, messages: u32) {
123 self.sender
124 .link_mut()
125 .set_keyframe_request_messages(messages);
126 }
127
128 #[wasm_bindgen(js_name = setVideoStartIdleMs)]
129 pub fn set_video_start_idle_ms(&mut self, idle_ms: u32) {
131 self.sender
132 .link_mut()
133 .set_video_start_idle_ms(idle_ms as u64);
134 }
135
136 #[wasm_bindgen(js_name = tick)]
137 pub fn tick(&mut self, now_ms: f64) -> Result<Array, JsValue> {
139 let frames = self
140 .sender
141 .tick(ms_from_js(now_ms))
142 .map_err(|err| JsValue::from_str(&format!("adaptive-link tick failed: {err}")))?;
143 let out = Array::new();
144 for frame in frames {
145 out.push(&Uint8Array::from(frame.as_slice()));
146 }
147 Ok(out)
148 }
149
150 #[wasm_bindgen(js_name = counters)]
151 pub fn counters(&self) -> String {
153 counters_json(self.last_counters)
154 }
155
156 #[wasm_bindgen(js_name = quality)]
157 pub fn quality(&mut self, now_ms: f64) -> String {
159 let quality = self.sender.link_mut().quality(ms_from_js(now_ms));
160 format!(
161 r#"{{"lostLastSecond":{},"recoveredLastSecond":{},"totalLastSecond":{},"rssi":[{},{}],"snr":[{},{}],"linkScore":[{},{}],"idrCode":"{}"}}"#,
162 quality.lost_last_second,
163 quality.recovered_last_second,
164 quality.total_last_second,
165 quality.rssi[0],
166 quality.rssi[1],
167 quality.snr[0],
168 quality.snr[1],
169 quality.link_score[0],
170 quality.link_score[1],
171 escape_json_str(&quality.idr_code),
172 )
173 }
174
175 fn record_counter_delta(&mut self, now_ms: u64, counters: FecCounters) {
176 let total = counters
177 .total_packets
178 .saturating_sub(self.last_counters.total_packets);
179 let recovered = counters
180 .recovered_packets
181 .saturating_sub(self.last_counters.recovered_packets);
182 let lost = counters
183 .lost_packets
184 .saturating_sub(self.last_counters.lost_packets);
185 self.last_counters = counters;
186 self.sender.record_fec(
187 now_ms,
188 total.min(u32::MAX as u64) as u32,
189 recovered.min(u32::MAX as u64) as u32,
190 lost.min(u32::MAX as u64) as u32,
191 );
192 }
193}
194
195#[cfg(target_arch = "wasm32")]
196#[wasm_bindgen]
197impl OpenIpcAdaptiveLink {
198 #[wasm_bindgen(js_name = tickAndSend)]
199 pub async fn tick_and_send(
201 &mut self,
202 device: &WebUsbRealtekDevice,
203 now_ms: f64,
204 current_channel: u8,
205 ) -> Result<usize, JsValue> {
206 self.tick_and_send_for_radio(device, now_ms, current_channel, 20)
207 .await
208 }
209
210 #[wasm_bindgen(js_name = tickAndSendForRadio)]
211 pub async fn tick_and_send_for_radio(
213 &mut self,
214 device: &WebUsbRealtekDevice,
215 now_ms: f64,
216 current_channel: u8,
217 channel_width_mhz: u16,
218 ) -> Result<usize, JsValue> {
219 let frames = self
220 .sender
221 .tick(ms_from_js(now_ms))
222 .map_err(|err| JsValue::from_str(&format!("adaptive-link tick failed: {err}")))?;
223 let count = frames.len();
224 for frame in frames {
225 device
226 .send_packet_for_radio(&frame, current_channel, channel_width_mhz, false)
227 .await?;
228 }
229 Ok(count)
230 }
231}