1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
//! Embassy adapters (`embassy` feature): async chunk-drain decoding over
//! the sync core, plus a periodic-TX ticker helper.
//!
//! The sync, allocation-free `no_std` core is the source of truth; this
//! module contains **no decode logic** — only orchestration. The shape is
//! the same bare-metal pattern the [`crate::ring`] docs describe,
//! transposed to embassy tasks:
//!
//! * a DMA/ADC interrupt (or an embassy intake task) pushes `i16` PCM
//! samples into a [`SampleRing`];
//! * an embassy decode task awaits chunks from a [`SampleSource`] and
//! drains them through a caller-provided [`TncReceiver`] via bounded
//! `push_i16` calls, with a **yield point between chunks** so
//! same-priority tasks (sensors, housekeeping) get the core;
//! * decoded frames are delivered to a callback while still borrowed
//! (the receiver's [`RxFrame`] is lending — copy what you keep);
//! * transmit scheduling runs on an [`embassy_time::Ticker`] via
//! [`TxTicker`].
//!
//! # Dependencies, justified
//!
//! The only library dependency this feature adds is `embassy-time`
//! (no_std, alloc-free), pulled in solely for [`TxTicker`]'s periodic
//! scheduling; the platform HAL supplies the underlying time driver at
//! link time. The decode path needs **no** embassy crate at all: it is
//! plain `async` Rust, so it runs on the embassy executor — or any
//! other — without further glue. No executor crate is a library
//! dependency; host tests and the worked example use a dev-dependency
//! executor.
//!
//! # Usage sketch
//!
//! ```no_run
//! use core::cell::RefCell;
//! use yodel::SampleRing;
//! use yodel::embassy::{SampleSource, TxTicker, run_decoder};
//! use yodel::tnc::{DefaultTncReceiver, TncConfig, TncReceiver};
//!
//! /// Drains a task-shared ring; on a real target the ISR is the
//! /// producer and the shared cell is a critical-section mutex.
//! struct RingSource<'a, const N: usize> {
//! ring: &'a RefCell<SampleRing<N>>,
//! }
//!
//! impl<const N: usize> SampleSource for RingSource<'_, N> {
//! async fn next_chunk(&mut self, buf: &mut [i16]) -> usize {
//! loop {
//! let n = self.ring.borrow_mut().pop_slice(buf);
//! if n > 0 {
//! return n;
//! }
//! // Nothing buffered: sleep one DMA half-buffer period.
//! embassy_time::Timer::after_millis(5).await;
//! }
//! }
//! }
//!
//! async fn decode_task(ring: &RefCell<SampleRing<1024>>, cfg: TncConfig) {
//! let mut rx: DefaultTncReceiver = TncReceiver::new(cfg).unwrap();
//! let mut source = RingSource { ring };
//! let mut chunk = [0i16; 128];
//! run_decoder(&mut source, &mut rx, &mut chunk, |frame| {
//! let _ = frame.info(); // copy out what you keep
//! })
//! .await;
//! }
//!
//! async fn beacon_task() {
//! let mut tick = TxTicker::every(embassy_time::Duration::from_secs(30));
//! loop {
//! tick.ready().await;
//! // build the packet with TncTransmitter, hand samples to the DAC
//! }
//! }
//! ```
use Future;
use Pin;
use ;
use crate;
use crateSampleRing;
/// An async producer of PCM sample chunks: the seam between the
/// platform's intake (DMA ring, ADC ISR, channel) and [`run_decoder`].
///
/// `next_chunk` fills a prefix of `buf` with the oldest pending samples
/// and returns how many it wrote. Returning `0` means *end of stream*
/// and stops the decoder — a live radio source never returns `0` (await
/// until samples arrive instead); finite sources (tests, replays)
/// return `0` when exhausted.
/// Drains `source` through `receiver` in bounded chunks, yielding to
/// the executor between chunks, until the source reports end of stream.
///
/// Each chunk costs at most `chunk.len()` constant-cost
/// [`TncReceiver::push_i16`] calls, so `chunk.len()` is the decode
/// task's latency knob: smaller chunks yield more often. Every decoded
/// frame is handed to `on_frame` while still borrowed from the
/// receiver; copy out (e.g. via [`crate::tnc::OwnedFrame`]) anything
/// that must outlive the callback. All decode semantics — recovery
/// policy, chain voting, stats — are exactly the sync core's; this
/// function only moves samples.
///
/// Returns the total number of samples decoded.
pub async
/// Periodic transmit scheduling on `embassy-time`: a thin wrapper over
/// [`embassy_time::Ticker`] that keeps the beacon cadence steady
/// (missed deadlines are skipped, not bunched).
///
/// (No `Debug` impl: the wrapped `embassy_time::Ticker` has none.)
/// Yields to the executor once: returns `Pending` on the first poll
/// (after scheduling a wake) and `Ready` on the next.