1use std::net::SocketAddr;
30
31use dstar_gateway_core::EncodeError;
32use dstar_gateway_core::codec::{dcs, dextra, dplus};
33use dstar_gateway_core::header::DStarHeader;
34use dstar_gateway_core::types::{Module, ProtocolKind, StreamId};
35use dstar_gateway_core::voice::VoiceFrame;
36
37#[derive(Debug, Clone)]
45pub struct CrossProtocolEvent {
46 pub source_protocol: ProtocolKind,
48 pub source_peer: SocketAddr,
52 pub module: Module,
54 pub event: VoiceEvent,
56 pub cached_header: Option<DStarHeader>,
59}
60
61#[derive(Debug, Clone)]
68pub enum VoiceEvent {
69 StreamStart {
71 header: DStarHeader,
73 stream_id: StreamId,
75 },
76 Frame {
78 stream_id: StreamId,
80 seq: u8,
82 frame: VoiceFrame,
84 },
85 StreamEnd {
87 stream_id: StreamId,
89 seq: u8,
91 },
92}
93
94#[non_exhaustive]
96#[derive(Debug, thiserror::Error)]
97pub enum TranscodeError {
98 #[error("encode failed: {0}")]
100 Encode(#[from] EncodeError),
101 #[error("cached header is required for DCS transcoding but was None")]
106 MissingCachedHeader,
107}
108
109pub fn transcode_voice(
129 target: ProtocolKind,
130 event: &VoiceEvent,
131 cached_header: Option<&DStarHeader>,
132 out: &mut [u8],
133) -> Result<usize, TranscodeError> {
134 match target {
139 ProtocolKind::DExtra => transcode_dextra(event, out),
140 ProtocolKind::DPlus => transcode_dplus(event, out),
141 ProtocolKind::Dcs => transcode_dcs(event, cached_header, out),
142 _ => Err(TranscodeError::Encode(EncodeError::BufferTooSmall {
143 need: 0,
144 have: 0,
145 })),
146 }
147}
148
149fn transcode_dextra(event: &VoiceEvent, out: &mut [u8]) -> Result<usize, TranscodeError> {
150 match event {
151 VoiceEvent::StreamStart { header, stream_id } => {
152 let n = dextra::encode_voice_header(out, *stream_id, header)?;
153 Ok(n)
154 }
155 VoiceEvent::Frame {
156 stream_id,
157 seq,
158 frame,
159 } => {
160 let n = dextra::encode_voice_data(out, *stream_id, *seq, frame)?;
161 Ok(n)
162 }
163 VoiceEvent::StreamEnd { stream_id, seq } => {
164 let n = dextra::encode_voice_eot(out, *stream_id, *seq)?;
165 Ok(n)
166 }
167 }
168}
169
170fn transcode_dplus(event: &VoiceEvent, out: &mut [u8]) -> Result<usize, TranscodeError> {
171 match event {
172 VoiceEvent::StreamStart { header, stream_id } => {
173 let n = dplus::encode_voice_header(out, *stream_id, header)?;
174 Ok(n)
175 }
176 VoiceEvent::Frame {
177 stream_id,
178 seq,
179 frame,
180 } => {
181 let n = dplus::encode_voice_data(out, *stream_id, *seq, frame)?;
182 Ok(n)
183 }
184 VoiceEvent::StreamEnd { stream_id, seq } => {
185 let n = dplus::encode_voice_eot(out, *stream_id, *seq)?;
186 Ok(n)
187 }
188 }
189}
190
191fn transcode_dcs(
192 event: &VoiceEvent,
193 cached_header: Option<&DStarHeader>,
194 out: &mut [u8],
195) -> Result<usize, TranscodeError> {
196 let header = cached_header.ok_or(TranscodeError::MissingCachedHeader)?;
199 match event {
200 VoiceEvent::StreamStart { stream_id, .. } => {
201 let frame = VoiceFrame::silence();
209 let n = dcs::encode_voice(out, header, *stream_id, 0, &frame, false)?;
210 Ok(n)
211 }
212 VoiceEvent::Frame {
213 stream_id,
214 seq,
215 frame,
216 } => {
217 let n = dcs::encode_voice(out, header, *stream_id, *seq, frame, false)?;
218 Ok(n)
219 }
220 VoiceEvent::StreamEnd { stream_id, seq } => {
221 let frame = VoiceFrame::silence();
226 let n = dcs::encode_voice(out, header, *stream_id, *seq, &frame, true)?;
227 Ok(n)
228 }
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use dstar_gateway_core::types::{Callsign, Suffix};
236
237 const fn sid() -> StreamId {
238 match StreamId::new(0xCAFE) {
239 Some(s) => s,
240 None => unreachable!(),
241 }
242 }
243
244 fn test_header() -> DStarHeader {
245 DStarHeader {
246 flag1: 0,
247 flag2: 0,
248 flag3: 0,
249 rpt2: Callsign::from_wire_bytes(*b"REF030 G"),
250 rpt1: Callsign::from_wire_bytes(*b"REF030 C"),
251 ur_call: Callsign::from_wire_bytes(*b"CQCQCQ "),
252 my_call: Callsign::from_wire_bytes(*b"W1AW "),
253 my_suffix: Suffix::EMPTY,
254 }
255 }
256
257 fn test_frame() -> VoiceFrame {
258 VoiceFrame {
259 ambe: [0x11; 9],
260 slow_data: [0x22; 3],
261 }
262 }
263
264 type TestResult = Result<(), Box<dyn std::error::Error>>;
265
266 #[test]
267 fn transcode_dextra_to_dplus_header_matches_reference_encoding() -> TestResult {
268 let header = test_header();
269 let event = VoiceEvent::StreamStart {
270 header,
271 stream_id: sid(),
272 };
273 let mut out = [0u8; 128];
274 let n = transcode_voice(ProtocolKind::DPlus, &event, Some(&header), &mut out)?;
275 assert_eq!(n, 58, "DPlus voice header is 58 bytes");
276 let mut reference = [0u8; 128];
279 let m = dplus::encode_voice_header(&mut reference, sid(), &header)?;
280 assert_eq!(m, n);
281 assert_eq!(&out[..n], &reference[..m]);
282 Ok(())
283 }
284
285 #[test]
286 fn transcode_dplus_to_dcs_requires_cached_header() {
287 let event = VoiceEvent::Frame {
288 stream_id: sid(),
289 seq: 3,
290 frame: test_frame(),
291 };
292 let mut out = [0u8; 128];
293 let result = transcode_voice(ProtocolKind::Dcs, &event, None, &mut out);
294 assert!(
295 matches!(result, Err(TranscodeError::MissingCachedHeader)),
296 "expected MissingCachedHeader, got {result:?}"
297 );
298 }
299
300 #[test]
301 fn transcode_dcs_to_dextra_frame_uses_same_ambe_bytes() -> TestResult {
302 let frame = test_frame();
303 let event = VoiceEvent::Frame {
304 stream_id: sid(),
305 seq: 5,
306 frame,
307 };
308 let mut out = [0u8; 128];
309 let n = transcode_voice(ProtocolKind::DExtra, &event, None, &mut out)?;
310 assert_eq!(n, 27, "DExtra voice data is 27 bytes");
311 assert_eq!(&out[15..24], &frame.ambe);
313 assert_eq!(&out[24..27], &frame.slow_data);
315 assert_eq!(out[14], 5);
317 Ok(())
318 }
319
320 #[test]
321 fn transcode_dplus_to_dextra_frame_preserves_seq_and_stream_id() -> TestResult {
322 let frame = test_frame();
323 let event = VoiceEvent::Frame {
324 stream_id: sid(),
325 seq: 7,
326 frame,
327 };
328 let mut out = [0u8; 128];
329 let n = transcode_voice(ProtocolKind::DExtra, &event, None, &mut out)?;
330 assert_eq!(n, 27);
331 assert_eq!(out[12], 0xFE);
333 assert_eq!(out[13], 0xCA);
334 assert_eq!(out[14], 7);
335 Ok(())
336 }
337
338 #[test]
339 fn transcode_dextra_to_dcs_frame_embeds_cached_header() -> TestResult {
340 let header = test_header();
341 let frame = test_frame();
342 let event = VoiceEvent::Frame {
343 stream_id: sid(),
344 seq: 4,
345 frame,
346 };
347 let mut out = [0u8; 128];
348 let n = transcode_voice(ProtocolKind::Dcs, &event, Some(&header), &mut out)?;
349 assert_eq!(n, 100, "DCS voice is 100 bytes");
350 assert_eq!(&out[..4], b"0001");
352 assert_eq!(&out[31..39], header.my_call.as_bytes());
354 assert_eq!(out[43], 0xFE);
356 assert_eq!(out[44], 0xCA);
357 assert_eq!(out[45], 4);
359 assert_eq!(&out[46..55], &frame.ambe);
361 Ok(())
362 }
363
364 #[test]
365 fn transcode_dextra_eot_has_0x40_bit_set() -> TestResult {
366 let event = VoiceEvent::StreamEnd {
367 stream_id: sid(),
368 seq: 20,
369 };
370 let mut out = [0u8; 128];
371 let n = transcode_voice(ProtocolKind::DExtra, &event, None, &mut out)?;
372 assert_eq!(n, 27);
373 assert_eq!(out[14] & 0x40, 0x40, "EOT bit set on seq byte");
374 Ok(())
375 }
376
377 #[test]
378 fn transcode_buffer_too_small_is_error() {
379 let event = VoiceEvent::Frame {
380 stream_id: sid(),
381 seq: 1,
382 frame: test_frame(),
383 };
384 let mut out = [0u8; 4];
385 let result = transcode_voice(ProtocolKind::DExtra, &event, None, &mut out);
386 assert!(
387 matches!(result, Err(TranscodeError::Encode(_))),
388 "expected Encode error, got {result:?}"
389 );
390 }
391}