1use js_sys::{Array, Object, Reflect, Uint8Array};
2use openipc_core::realtek::{parse_rx_aggregate_with_kind, RxDescriptorKind};
3use openipc_core::{
4 ChannelId, FrameLayout, PayloadRouteId, RadioPort, ReceiverBatchOptions, ReceiverRuntime,
5 RtpPayloadTap, WfbKeypair,
6};
7use wasm_bindgen::prelude::*;
8
9use crate::js::{counters_json, elapsed_ms, now_ms, parse_hex_u64, raw_payload_object, set_number};
10use crate::video::video_frame_object;
11
12const VIDEO_ROUTE_ID: PayloadRouteId = PayloadRouteId::new(1);
13const TELEMETRY_ROUTE_ID: PayloadRouteId = PayloadRouteId::new(2);
14const DEFAULT_KEY_SLOT: u64 = 0;
15
16#[wasm_bindgen]
17pub struct OpenIpcReceiver {
19 pub(crate) runtime: ReceiverRuntime,
20 rx_descriptor_kind: RxDescriptorKind,
21}
22
23impl OpenIpcReceiver {
24 pub(crate) fn video_fec_counters(&self) -> openipc_core::FecCounters {
25 self.runtime.video_fec_counters()
26 }
27}
28
29#[wasm_bindgen]
30impl OpenIpcReceiver {
31 #[wasm_bindgen(constructor)]
32 pub fn new() -> Result<OpenIpcReceiver, JsValue> {
34 Self::with_channel_id(openipc_core::channel::DEFAULT_LINK_ID << 8, 1, 5)
35 }
36
37 #[wasm_bindgen(js_name = withChannelId)]
38 pub fn with_channel_id(
40 channel_id: u32,
41 fec_k: usize,
42 fec_n: usize,
43 ) -> Result<OpenIpcReceiver, JsValue> {
44 let runtime = ReceiverRuntime::with_plain_video_route(
45 FrameLayout::WithFcs,
46 VIDEO_ROUTE_ID,
47 ChannelId::new(channel_id),
48 DEFAULT_KEY_SLOT,
49 fec_k,
50 fec_n,
51 )
52 .map_err(|err| JsValue::from_str(&format!("invalid receiver config: {err}")))?;
53 Ok(Self {
54 runtime,
55 rx_descriptor_kind: RxDescriptorKind::Jaguar1,
56 })
57 }
58
59 #[wasm_bindgen(js_name = withKeypair)]
60 pub fn with_keypair(
62 channel_id: u32,
63 keypair: &[u8],
64 minimum_epoch: u64,
65 ) -> Result<OpenIpcReceiver, JsValue> {
66 let keypair = WfbKeypair::from_bytes(keypair)
67 .map_err(|err| JsValue::from_str(&format!("invalid WFB keypair: {err}")))?;
68 let telemetry_channel_id =
69 ChannelId::from_link_port(channel_id >> 8, RadioPort::TelemetryRx).raw();
70 openipc_receiver_with_keypair_and_telemetry_channel_inner(
71 channel_id,
72 telemetry_channel_id,
73 keypair,
74 minimum_epoch,
75 )
76 }
77
78 #[wasm_bindgen(js_name = withKeypairOnly)]
79 pub fn with_keypair_only(
81 channel_id: u32,
82 keypair: &[u8],
83 minimum_epoch: u64,
84 ) -> Result<OpenIpcReceiver, JsValue> {
85 let keypair = WfbKeypair::from_bytes(keypair)
86 .map_err(|err| JsValue::from_str(&format!("invalid WFB keypair: {err}")))?;
87 let runtime = ReceiverRuntime::with_keyed_video_route(
88 FrameLayout::WithFcs,
89 VIDEO_ROUTE_ID,
90 ChannelId::new(channel_id),
91 DEFAULT_KEY_SLOT,
92 keypair,
93 minimum_epoch,
94 )
95 .map_err(|err| JsValue::from_str(&format!("invalid encrypted receiver config: {err}")))?;
96 Ok(OpenIpcReceiver {
97 runtime,
98 rx_descriptor_kind: RxDescriptorKind::Jaguar1,
99 })
100 }
101
102 #[wasm_bindgen(js_name = setRxDescriptorKind)]
103 pub fn set_rx_descriptor_kind(&mut self, kind: &str) -> Result<(), JsValue> {
105 self.rx_descriptor_kind = parse_rx_descriptor_kind(kind)?;
106 Ok(())
107 }
108
109 #[wasm_bindgen(js_name = withKeypairAndMavlinkChannel)]
110 pub fn with_keypair_and_mavlink_channel(
115 channel_id: u32,
116 mavlink_channel_id: u32,
117 keypair: &[u8],
118 minimum_epoch: u64,
119 ) -> Result<OpenIpcReceiver, JsValue> {
120 Self::with_keypair_and_telemetry_channel(
121 channel_id,
122 mavlink_channel_id,
123 keypair,
124 minimum_epoch,
125 )
126 }
127
128 #[wasm_bindgen(js_name = withKeypairAndTelemetryChannel)]
129 pub fn with_keypair_and_telemetry_channel(
131 channel_id: u32,
132 telemetry_channel_id: u32,
133 keypair: &[u8],
134 minimum_epoch: u64,
135 ) -> Result<OpenIpcReceiver, JsValue> {
136 let keypair = WfbKeypair::from_bytes(keypair)
137 .map_err(|err| JsValue::from_str(&format!("invalid WFB keypair: {err}")))?;
138 openipc_receiver_with_keypair_and_telemetry_channel_inner(
139 channel_id,
140 telemetry_channel_id,
141 keypair,
142 minimum_epoch,
143 )
144 }
145
146 #[wasm_bindgen(js_name = pushRtpPacket)]
147 pub fn push_rtp_packet(&mut self, data: &[u8]) -> Option<Uint8Array> {
149 self.runtime
150 .rtp_mut()
151 .push(data)
152 .ok()
153 .flatten()
154 .map(|frame| Uint8Array::from(frame.data.as_slice()))
155 }
156
157 #[wasm_bindgen(
158 js_name = pushRtpPacketDetailed,
159 unchecked_return_type = "OpenIpcVideoFrame | null"
160 )]
161 pub fn push_rtp_packet_detailed(&mut self, data: &[u8]) -> Result<JsValue, JsValue> {
163 match self.runtime.rtp_mut().push(data).ok().flatten() {
164 Some(frame) => Ok(video_frame_object(frame)?.into()),
165 None => Ok(JsValue::NULL),
166 }
167 }
168
169 #[wasm_bindgen(js_name = pushDecryptedFragment)]
170 pub fn push_decrypted_fragment(
172 &mut self,
173 data_nonce_hex: &str,
174 fragment: &[u8],
175 ) -> Result<Array, JsValue> {
176 let data_nonce = parse_hex_u64(data_nonce_hex)?;
177 let batch = self
178 .runtime
179 .push_decrypted_fragment(
180 self.runtime.video_runtime(),
181 data_nonce,
182 fragment,
183 &ReceiverBatchOptions::default(),
184 )
185 .map_err(|err| JsValue::from_str(&format!("WFB fragment rejected: {err}")))?;
186 Ok(frame_bytes_array(batch.frames))
187 }
188
189 #[wasm_bindgen(js_name = pushDecrypted80211Frame)]
190 pub fn push_decrypted_80211_frame(
192 &mut self,
193 frame: &[u8],
194 fragment: &[u8],
195 ) -> Result<Array, JsValue> {
196 let batch = self
197 .runtime
198 .push_decrypted_80211_frame(
199 self.runtime.video_runtime(),
200 frame,
201 fragment,
202 &ReceiverBatchOptions::default(),
203 )
204 .map_err(|err| JsValue::from_str(&format!("802.11 frame rejected: {err}")))?;
205 Ok(frame_bytes_array(batch.frames))
206 }
207
208 #[wasm_bindgen(js_name = pushEncrypted80211Frame)]
209 pub fn push_encrypted_80211_frame(&mut self, frame: &[u8]) -> Result<Array, JsValue> {
211 let batch = self
212 .runtime
213 .push_80211_frame(frame, &ReceiverBatchOptions::default())
214 .map_err(|err| JsValue::from_str(&format!("802.11 frame rejected: {err}")))?;
215 Ok(frame_bytes_array(batch.frames))
216 }
217
218 #[wasm_bindgen(js_name = pushRxTransfer)]
219 pub fn push_rx_transfer(&mut self, transfer: &[u8]) -> Result<Array, JsValue> {
221 let batch = self
222 .runtime
223 .push_rx_transfer(transfer, &ReceiverBatchOptions::default())
224 .map_err(|err| JsValue::from_str(&format!("Realtek RX aggregate rejected: {err}")))?;
225 Ok(frame_bytes_array(batch.frames))
226 }
227
228 #[wasm_bindgen(
229 js_name = pushRxTransferDetailed,
230 unchecked_return_type = "OpenIpcVideoFrame[]"
231 )]
232 pub fn push_rx_transfer_detailed(&mut self, transfer: &[u8]) -> Result<Array, JsValue> {
234 self.push_rx_transfer_detailed_with_options(transfer, false)
235 }
236
237 #[wasm_bindgen(
238 js_name = pushRxTransferDetailedWithOptions,
239 unchecked_return_type = "OpenIpcVideoFrame[]"
240 )]
241 pub fn push_rx_transfer_detailed_with_options(
243 &mut self,
244 transfer: &[u8],
245 keep_corrupted: bool,
246 ) -> Result<Array, JsValue> {
247 let batch = self
248 .runtime
249 .push_rx_transfer(
250 transfer,
251 &ReceiverBatchOptions {
252 accept_corrupted: keep_corrupted,
253 ..ReceiverBatchOptions::default()
254 },
255 )
256 .map_err(|err| JsValue::from_str(&format!("Realtek RX aggregate rejected: {err}")))?;
257 frame_objects_array(batch.frames)
258 }
259
260 #[wasm_bindgen(
261 js_name = pushRxTransferProfiled,
262 unchecked_return_type = "OpenIpcRxTransferProfile"
263 )]
264 pub fn push_rx_transfer_profiled(&mut self, transfer: &[u8]) -> Result<Object, JsValue> {
266 self.push_rx_transfer_profiled_with_options(transfer, false)
267 }
268
269 #[wasm_bindgen(
270 js_name = pushRxTransferProfiledWithOptions,
271 unchecked_return_type = "OpenIpcRxTransferProfile"
272 )]
273 pub fn push_rx_transfer_profiled_with_options(
275 &mut self,
276 transfer: &[u8],
277 keep_corrupted: bool,
278 ) -> Result<Object, JsValue> {
279 self.push_rx_transfer_profiled_inner(
280 transfer,
281 keep_corrupted,
282 &[TELEMETRY_ROUTE_ID.raw() as u32],
283 &[],
284 )
285 }
286
287 #[wasm_bindgen(
288 js_name = pushRxTransferProfiledWithRouteIds,
289 unchecked_return_type = "OpenIpcRxTransferProfile"
290 )]
291 pub fn push_rx_transfer_profiled_with_route_ids(
293 &mut self,
294 transfer: &[u8],
295 keep_corrupted: bool,
296 raw_route_ids: &[u32],
297 ) -> Result<Object, JsValue> {
298 self.push_rx_transfer_profiled_inner(transfer, keep_corrupted, raw_route_ids, &[])
299 }
300
301 #[wasm_bindgen(
302 js_name = pushRxTransferProfiledWithRouteIdsAndRtpTaps,
303 unchecked_return_type = "OpenIpcRxTransferProfile"
304 )]
305 pub fn push_rx_transfer_profiled_with_route_ids_and_rtp_taps(
307 &mut self,
308 transfer: &[u8],
309 keep_corrupted: bool,
310 raw_route_ids: &[u32],
311 rtp_tap_route_ids: &[u32],
312 rtp_tap_payload_types: &[u8],
313 ) -> Result<Object, JsValue> {
314 if rtp_tap_route_ids.len() != rtp_tap_payload_types.len() {
315 return Err(JsValue::from_str(
316 "RTP tap route id and payload type arrays must have the same length",
317 ));
318 }
319 let rtp_payload_taps = rtp_tap_route_ids
320 .iter()
321 .zip(rtp_tap_payload_types.iter())
322 .map(|(route_id, payload_type)| RtpPayloadTap {
323 route_id: PayloadRouteId::new(*route_id as u64),
324 payload_type: *payload_type,
325 })
326 .collect::<Vec<_>>();
327 self.push_rx_transfer_profiled_inner(
328 transfer,
329 keep_corrupted,
330 raw_route_ids,
331 &rtp_payload_taps,
332 )
333 }
334
335 #[wasm_bindgen(js_name = addKeyedRoute)]
336 pub fn add_keyed_route(
338 &mut self,
339 route_id: u32,
340 channel_id: u32,
341 keypair: &[u8],
342 minimum_epoch: u64,
343 ) -> Result<(), JsValue> {
344 let keypair = WfbKeypair::from_bytes(keypair)
345 .map_err(|err| JsValue::from_str(&format!("invalid WFB keypair: {err}")))?;
346 self.runtime
347 .add_keyed_route(
348 PayloadRouteId::new(route_id as u64),
349 ChannelId::new(channel_id),
350 DEFAULT_KEY_SLOT,
351 keypair,
352 minimum_epoch,
353 )
354 .map_err(|err| JsValue::from_str(&format!("invalid route config: {err}")))?;
355 Ok(())
356 }
357
358 #[wasm_bindgen(js_name = fecCounters)]
359 pub fn fec_counters(&self) -> String {
361 counters_json(self.video_fec_counters())
362 }
363}
364
365impl OpenIpcReceiver {
366 fn push_rx_transfer_profiled_inner(
367 &mut self,
368 transfer: &[u8],
369 keep_corrupted: bool,
370 raw_route_ids: &[u32],
371 rtp_payload_taps: &[RtpPayloadTap],
372 ) -> Result<Object, JsValue> {
373 let total_start = now_ms();
374 let parse_start = now_ms();
375 let packets = parse_rx_aggregate_with_kind(transfer, self.rx_descriptor_kind)
376 .map_err(|err| JsValue::from_str(&format!("Realtek RX aggregate rejected: {err}")))?;
377 let parse_ms = elapsed_ms(parse_start);
378
379 let raw_payload_routes = raw_route_ids
380 .iter()
381 .map(|id| PayloadRouteId::new(*id as u64))
382 .collect();
383 let pipeline_start = now_ms();
384 let batch = self.runtime.push_rx_packets(
385 packets,
386 &ReceiverBatchOptions {
387 accept_corrupted: keep_corrupted,
388 raw_payload_routes,
389 rtp_payload_taps: rtp_payload_taps.to_vec(),
390 },
391 );
392 let pipeline_ms = elapsed_ms(pipeline_start);
393 let counters = batch.counters;
394 let frames = frame_objects_array(batch.frames)?;
395 let raw_payloads = raw_payload_array(batch.raw_payloads)?;
396
397 let object = Object::new();
398 Reflect::set(&object, &JsValue::from_str("frames"), &frames)?;
399 Reflect::set(&object, &JsValue::from_str("rawPayloads"), &raw_payloads)?;
400 Reflect::set(
401 &object,
402 &JsValue::from_str("mavlinkPayloads"),
403 &raw_payloads,
404 )?;
405 set_number(
406 &object,
407 "rawPayloadCount",
408 counters.raw_payload_count as f64,
409 )?;
410 set_number(
411 &object,
412 "rawPayloadBytes",
413 counters.raw_payload_bytes as f64,
414 )?;
415 set_number(&object, "transferBytes", transfer.len() as f64)?;
416 set_number(&object, "packets", counters.packets as f64)?;
417 set_number(&object, "acceptedPackets", counters.accepted_packets as f64)?;
418 set_number(&object, "droppedPackets", counters.dropped_packets as f64)?;
419 set_number(&object, "crcDropped", counters.crc_dropped as f64)?;
420 set_number(&object, "icvDropped", counters.icv_dropped as f64)?;
421 set_number(&object, "reportDropped", counters.report_dropped as f64)?;
422 set_number(&object, "ignoredFrames", counters.ignored_frames as f64)?;
423 set_number(&object, "sessions", counters.sessions as f64)?;
424 set_number(&object, "wfbPayloads", counters.wfb_payloads as f64)?;
425 set_number(&object, "rtpPackets", counters.rtp_packets as f64)?;
426 set_number(&object, "videoFrames", counters.video_frames as f64)?;
427 set_number(
428 &object,
429 "mavlinkPayloadCount",
430 counters.raw_payload_count as f64,
431 )?;
432 set_number(&object, "mavlinkBytes", counters.raw_payload_bytes as f64)?;
433 set_number(&object, "parseMs", parse_ms)?;
434 set_number(&object, "pipelineMs", pipeline_ms)?;
435 set_number(&object, "totalMs", elapsed_ms(total_start))?;
436 Ok(object)
437 }
438}
439
440fn openipc_receiver_with_keypair_and_telemetry_channel_inner(
441 channel_id: u32,
442 telemetry_channel_id: u32,
443 keypair: WfbKeypair,
444 minimum_epoch: u64,
445) -> Result<OpenIpcReceiver, JsValue> {
446 let mut runtime = ReceiverRuntime::with_keyed_video_route(
447 FrameLayout::WithFcs,
448 VIDEO_ROUTE_ID,
449 ChannelId::new(channel_id),
450 DEFAULT_KEY_SLOT,
451 keypair,
452 minimum_epoch,
453 )
454 .map_err(|err| JsValue::from_str(&format!("invalid encrypted receiver config: {err}")))?;
455 runtime
456 .add_keyed_route(
457 TELEMETRY_ROUTE_ID,
458 ChannelId::new(telemetry_channel_id),
459 DEFAULT_KEY_SLOT,
460 keypair,
461 minimum_epoch,
462 )
463 .map_err(|err| JsValue::from_str(&format!("invalid MAVLink receiver config: {err}")))?;
464 Ok(OpenIpcReceiver {
465 runtime,
466 rx_descriptor_kind: RxDescriptorKind::Jaguar1,
467 })
468}
469
470pub(crate) fn parse_rx_descriptor_kind(kind: &str) -> Result<RxDescriptorKind, JsValue> {
471 match kind {
472 "jaguar1" | "rtl8812" | "rtl8821" | "rtl8814" => Ok(RxDescriptorKind::Jaguar1),
473 "jaguar3" | "rtl8812cu" | "rtl8822c" | "rtl8822cu" => Ok(RxDescriptorKind::Jaguar3),
474 _ => Err(JsValue::from_str(
475 "unsupported RX descriptor kind; expected jaguar1 or jaguar3",
476 )),
477 }
478}
479
480fn frame_bytes_array(frames: Vec<openipc_core::DepacketizedFrame>) -> Array {
481 let out = Array::new();
482 for frame in frames {
483 out.push(&Uint8Array::from(frame.data.as_slice()));
484 }
485 out
486}
487
488fn frame_objects_array(frames: Vec<openipc_core::DepacketizedFrame>) -> Result<Array, JsValue> {
489 let out = Array::new();
490 for frame in frames {
491 out.push(&video_frame_object(frame)?.into());
492 }
493 Ok(out)
494}
495
496fn raw_payload_array(payloads: Vec<openipc_core::RoutePayload>) -> Result<Array, JsValue> {
497 let out = Array::new();
498 for payload in payloads {
499 out.push(&raw_payload_object(payload)?.into());
500 }
501 Ok(out)
502}