1use pamoja_lora::region::{ChannelPlan, Modulation, RelayChannel};
5use pamoja_lora::LinkSettings;
6
7use super::ack::{CadPeriodicity, CadToRx, XtalAccuracy};
8use super::filter::JoinFilter;
9use super::forward::WorChannel;
10use super::limits::{CounterReset, ForwardLimits, TokenBucket, UNLIMITED_DEVICE_RATE};
11use super::trusted::{TrustedDevice, TrustedDevices};
12use crate::device::EndDevice;
13use crate::mac::{relay_second_channel, MacCommand};
14
15const DETECTION_SYMBOLS: u64 = 2;
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
21pub struct RelaySettings {
22 pub xtal_accuracy: XtalAccuracy,
24 pub cad_to_rx: CadToRx,
26}
27
28impl RelaySettings {
29 #[must_use]
40 pub const fn new(xtal_accuracy: XtalAccuracy, cad_to_rx: CadToRx) -> RelaySettings {
41 RelaySettings {
42 xtal_accuracy,
43 cad_to_rx,
44 }
45 }
46}
47
48#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
51pub struct RelayConfig {
52 pub cad_periodicity: CadPeriodicity,
54 pub default_channel: RelayChannel,
56 pub second_channel: Option<RelayChannel>,
58}
59
60impl RelayConfig {
61 #[must_use]
72 pub const fn new(
73 cad_periodicity: CadPeriodicity,
74 default_channel: RelayChannel,
75 ) -> RelayConfig {
76 RelayConfig {
77 cad_periodicity,
78 default_channel,
79 second_channel: None,
80 }
81 }
82
83 #[must_use]
94 pub fn region_default(plan: &ChannelPlan) -> Option<RelayConfig> {
95 Some(RelayConfig::new(
96 CadPeriodicity::Ms1000,
97 plan.relay_channel(0)?,
98 ))
99 }
100
101 #[must_use]
111 pub const fn with_second_channel(mut self, channel: RelayChannel) -> RelayConfig {
112 self.second_channel = Some(channel);
113 self
114 }
115
116 #[must_use]
126 pub const fn channel(&self, channel: WorChannel) -> Option<RelayChannel> {
127 match channel {
128 WorChannel::Default => Some(self.default_channel),
129 WorChannel::Second => self.second_channel,
130 }
131 }
132}
133
134#[derive(Clone, Copy, Debug, PartialEq, Eq)]
137pub(crate) struct RelayState {
138 pub(crate) settings: RelaySettings,
139 pub(crate) config: Option<RelayConfig>,
140 pub(crate) anchor_us: Option<u64>,
141 pub(crate) reloads: u64,
142 pub(crate) filter: JoinFilter,
143 pub(crate) trusted: TrustedDevices,
144 pub(crate) limits: ForwardLimits,
145}
146
147impl RelayState {
148 pub(crate) const fn new(settings: RelaySettings) -> RelayState {
149 RelayState {
150 settings,
151 config: None,
152 anchor_us: None,
153 reloads: 0,
154 filter: JoinFilter::new(),
155 trusted: TrustedDevices::new(),
156 limits: ForwardLimits::new(),
157 }
158 }
159
160 pub(crate) fn run(&mut self, config: RelayConfig) {
163 if self.config.is_none() {
164 self.anchor_us = None;
165 }
166 self.config = Some(config);
167 }
168
169 pub(crate) fn stop(&mut self) {
171 self.config = None;
172 self.anchor_us = None;
173 }
174
175 pub(crate) fn reload(&mut self, now_us: u64) {
177 let Some(anchor) = self.anchor_us else {
178 return;
179 };
180 let hours = now_us.saturating_sub(anchor) / super::limits::RELOAD_PERIOD_US;
181 if hours > self.reloads {
182 let earned = hours - self.reloads;
183 self.limits.reload(earned);
184 self.trusted.reload(earned);
185 self.reloads = hours;
186 }
187 }
188
189 pub(crate) fn command(
191 &mut self,
192 command: &MacCommand,
193 device: &EndDevice,
194 ) -> Option<MacCommand> {
195 match *command {
196 MacCommand::RelayConfReq {
197 enabled,
198 cad_periodicity,
199 default_channel_index,
200 second_channel_index,
201 second_channel_data_rate,
202 second_channel_ack_offset,
203 second_channel_frequency_hz,
204 } => {
205 if !enabled {
206 self.stop();
207 return Some(MacCommand::RelayConfAns {
208 cad_periodicity_ack: true,
209 default_channel_index_ack: true,
210 second_channel_index_ack: true,
211 second_channel_data_rate_ack: true,
212 second_channel_ack_offset_ack: true,
213 second_channel_frequency_ack: true,
214 });
215 }
216 let plan = device.plan();
217 let periodicity = CadPeriodicity::from_code(cad_periodicity);
218 let default_channel = plan
219 .relay_channel(default_channel_index)
220 .filter(|channel| channel_ok(device, channel));
221 let second_channel_index_ack = second_channel_index <= 1;
222 let (data_rate_ack, ack_offset_ack, frequency_ack) = if second_channel_index == 1 {
223 let data_rate_ack = wor_link(plan, second_channel_data_rate).is_some();
224 let frequency_ack = device.tunes(second_channel_frequency_hz);
225 let ack_offset_ack = relay_second_channel(
226 1,
227 second_channel_data_rate,
228 second_channel_ack_offset,
229 second_channel_frequency_hz,
230 )
231 .is_some_and(|channel| device.tunes(channel.ack_frequency_hz));
232 (data_rate_ack, ack_offset_ack, frequency_ack)
233 } else {
234 (true, true, true)
235 };
236 let second_channel = relay_second_channel(
237 second_channel_index,
238 second_channel_data_rate,
239 second_channel_ack_offset,
240 second_channel_frequency_hz,
241 );
242 let scans_fit = match (periodicity, default_channel) {
243 (Some(periodicity), Some(default_channel)) => {
244 let mut config = RelayConfig::new(periodicity, default_channel);
245 config.second_channel = second_channel;
246 scans_fit(plan, &config, self.settings.cad_to_rx)
247 }
248 _ => true,
249 };
250 let answer = MacCommand::RelayConfAns {
251 cad_periodicity_ack: periodicity.is_some() && scans_fit,
252 default_channel_index_ack: default_channel.is_some(),
253 second_channel_index_ack,
254 second_channel_data_rate_ack: data_rate_ack,
255 second_channel_ack_offset_ack: ack_offset_ack,
256 second_channel_frequency_ack: frequency_ack,
257 };
258 if let (Some(periodicity), Some(default_channel), true) = (
259 periodicity,
260 default_channel,
261 scans_fit
262 && second_channel_index_ack
263 && data_rate_ack
264 && ack_offset_ack
265 && frequency_ack,
266 ) {
267 self.run(RelayConfig {
268 cad_periodicity: periodicity,
269 default_channel,
270 second_channel,
271 });
272 }
273 Some(answer)
274 }
275 MacCommand::FilterListReq {
276 index,
277 action,
278 eui_len,
279 ref eui,
280 } => Some(self.filter.apply(index, action, eui_len, eui)),
281 MacCommand::UpdateUplinkListReq {
282 index,
283 reload_rate,
284 bucket_size,
285 dev_addr,
286 wfcnt,
287 ref root_wor_s_key,
288 } => {
289 let bucket = TokenBucket::coded(reload_rate, bucket_size, UNLIMITED_DEVICE_RATE);
290 self.trusted.set(
291 index,
292 TrustedDevice::new(dev_addr, root_wor_s_key, wfcnt, bucket),
293 );
294 Some(MacCommand::UpdateUplinkListAns)
295 }
296 MacCommand::CtrlUplinkListReq { index, action } => {
297 let wfcnt = self.trusted.get(index).map(TrustedDevice::last_wfcnt);
298 if wfcnt.is_some() && action == 1 {
299 self.trusted.remove(index);
300 }
301 Some(MacCommand::CtrlUplinkListAns {
302 index_ack: wfcnt.is_some(),
303 wfcnt: wfcnt.unwrap_or(0),
304 })
305 }
306 MacCommand::ConfigureFwdLimitReq {
307 reset_limit_counters,
308 join_request_reload_rate,
309 notify_reload_rate,
310 global_uplink_reload_rate,
311 overall_reload_rate,
312 join_request_bucket_size,
313 notify_bucket_size,
314 global_uplink_bucket_size,
315 overall_bucket_size,
316 } => {
317 self.limits.configure(
318 CounterReset::from_code(reset_limit_counters),
319 [
320 join_request_reload_rate,
321 notify_reload_rate,
322 global_uplink_reload_rate,
323 overall_reload_rate,
324 ],
325 [
326 join_request_bucket_size,
327 notify_bucket_size,
328 global_uplink_bucket_size,
329 overall_bucket_size,
330 ],
331 );
332 Some(MacCommand::ConfigureFwdLimitAns)
333 }
334 _ => None,
335 }
336 }
337}
338
339pub(crate) fn wor_link(plan: &ChannelPlan, data_rate: u8) -> Option<LinkSettings> {
341 match plan.downlink_data_rate(data_rate)?.modulation {
342 Modulation::LoRa {
343 spreading_factor,
344 bandwidth_hz,
345 } => Some(LinkSettings::new(spreading_factor, bandwidth_hz)),
346 _ => None,
347 }
348}
349
350pub(crate) fn channel_ok(device: &EndDevice, channel: &RelayChannel) -> bool {
352 wor_link(device.plan(), channel.data_rate).is_some()
353 && device.tunes(channel.wor_frequency_hz)
354 && device.tunes(channel.ack_frequency_hz)
355}
356
357pub(crate) fn config_ok(device: &EndDevice, config: &RelayConfig, cad_to_rx: CadToRx) -> bool {
360 channel_ok(device, &config.default_channel)
361 && config
362 .second_channel
363 .is_none_or(|channel| channel_ok(device, &channel))
364 && scans_fit(device.plan(), config, cad_to_rx)
365}
366
367fn scans_fit(plan: &ChannelPlan, config: &RelayConfig, cad_to_rx: CadToRx) -> bool {
371 let period = config.cad_periodicity.period_us();
372 let interval = if config.second_channel.is_some() {
373 period / 2
374 } else {
375 period
376 };
377 [Some(config.default_channel), config.second_channel]
378 .into_iter()
379 .flatten()
380 .all(|channel| {
381 wor_link(plan, channel.data_rate).is_none_or(|link| {
382 (DETECTION_SYMBOLS + u64::from(cad_to_rx.symbols())) * link.symbol_time_us()
383 <= interval
384 })
385 })
386}