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