pamoja_lorawan/packages/multicast.rs
1//! Remote multicast setup, TS005-2.0.0.
2//!
3//! Sending the same firmware image to a thousand devices one at a time would take the band
4//! for days. Instead the server gives them all one multicast address and one key, then opens
5//! a window in which they listen together. This package is how that group is set up: the
6//! server creates it with [`McCommand::McGroupSetupReq`], carrying the group's key wrapped
7//! under a key only the device holds, and later opens a Class C or Class B session on it.
8//!
9//! The keys come down a chain no session key ever leaves:
10//!
11//! - [`mc_root_key`] from the device's own root key, then [`mc_ke_key`] from that: the key
12//! the group keys travel under, which lives as long as the device does.
13//! - [`mc_key`] unwraps the group key the setup carried.
14//! - [`mc_app_s_key`] and [`mc_nwk_s_key`] derive the pair that reads the group's frames,
15//! from the group key and the group's address.
16//!
17//! # Examples
18//!
19//! A server wraps a group key, and the device unwraps it and derives the session:
20//!
21//! ```
22//! use pamoja_lorawan::packages::multicast::{
23//! mc_app_s_key, mc_ke_key, mc_key, mc_nwk_s_key, mc_root_key, wrap_mc_key,
24//! };
25//!
26//! let app_key = [0x2B; 16];
27//! let group_key = [0x77; 16];
28//! let ke_key = mc_ke_key(&mc_root_key(&app_key));
29//!
30//! let wrapped = wrap_mc_key(&ke_key, &group_key);
31//! assert_eq!(mc_key(&ke_key, &wrapped), group_key, "the device gets it back");
32//!
33//! let addr = 0x2601_1BDA;
34//! assert_ne!(mc_app_s_key(&group_key, addr), mc_nwk_s_key(&group_key, addr));
35//! ```
36
37use super::{payload, write, PackageVersion};
38use crate::crypto::Cipher;
39use crate::{Direction, LorawanError};
40
41/// The port the remote multicast setup package is spoken on, section 4.
42pub const PORT: u8 = 200;
43
44/// The identifier of this package, section 4.
45pub const PACKAGE: u8 = 2;
46
47/// The version of this package implemented here, section 4.
48pub const VERSION: u8 = 2;
49
50/// How many multicast groups a device may hold at once, section 4.3.
51pub const GROUPS: usize = 4;
52
53/// The longest command of this package, in bytes.
54pub const MAX_COMMAND: usize = 30;
55
56const CID_PACKAGE_VERSION: u8 = 0x00;
57const CID_GROUP_STATUS: u8 = 0x01;
58const CID_GROUP_SETUP: u8 = 0x02;
59const CID_GROUP_DELETE: u8 = 0x03;
60const CID_CLASS_C_SESSION: u8 = 0x04;
61const CID_CLASS_B_SESSION: u8 = 0x05;
62
63/// Derives a device's multicast root key, section 4.3.
64///
65/// # Arguments
66///
67/// * `root_key` - the device's `GenAppKey` on LoRaWAN 1.0.x, or its `AppKey` on 1.1.
68/// * `lorawan_11` - whether the device follows the 1.1 scheme, which uses a different
69/// constant.
70///
71/// # Returns
72///
73/// The `McRootKey`, which nothing but [`mc_ke_key`] is taken from.
74#[must_use]
75pub fn mc_root_key_for(root_key: &[u8; 16], lorawan_11: bool) -> [u8; 16] {
76 let mut block = [0u8; 16];
77 block[0] = if lorawan_11 { 0x20 } else { 0x00 };
78 Cipher::new(root_key).encrypt_block(&block)
79}
80
81/// Derives a device's multicast root key on the LoRaWAN 1.0.x scheme, section 4.3.
82///
83/// # Arguments
84///
85/// * `gen_app_key` - the `GenAppKey` provisioned into the device.
86///
87/// # Returns
88///
89/// The `McRootKey`.
90#[must_use]
91pub fn mc_root_key(gen_app_key: &[u8; 16]) -> [u8; 16] {
92 mc_root_key_for(gen_app_key, false)
93}
94
95/// Derives the key a group's key travels under, section 4.3.
96///
97/// # Arguments
98///
99/// * `mc_root_key` - the key [`mc_root_key`] derived.
100///
101/// # Returns
102///
103/// The `McKEKey`, which lives as long as the device.
104#[must_use]
105pub fn mc_ke_key(mc_root_key: &[u8; 16]) -> [u8; 16] {
106 Cipher::new(mc_root_key).encrypt_block(&[0u8; 16])
107}
108
109/// Unwraps the group key a setup command carried, section 4.3.
110///
111/// # Arguments
112///
113/// * `mc_ke_key` - the key it travels under.
114/// * `wrapped` - the sixteen bytes the command carried.
115///
116/// # Returns
117///
118/// The group's `McKey`.
119#[must_use]
120pub fn mc_key(mc_ke_key: &[u8; 16], wrapped: &[u8; 16]) -> [u8; 16] {
121 Cipher::new(mc_ke_key).encrypt_block(wrapped)
122}
123
124/// Wraps a group key for a device, which is what a server does before sending it.
125///
126/// # Arguments
127///
128/// * `mc_ke_key` - the device's key encryption key.
129/// * `mc_key` - the group key.
130///
131/// # Returns
132///
133/// The sixteen bytes the setup command carries.
134#[must_use]
135pub fn wrap_mc_key(mc_ke_key: &[u8; 16], mc_key: &[u8; 16]) -> [u8; 16] {
136 Cipher::new(mc_ke_key).decrypt_block(mc_key)
137}
138
139/// Derives the key that reads a group's payloads, section 4.3.
140///
141/// # Arguments
142///
143/// * `mc_key` - the group key.
144/// * `mc_addr` - the group's address.
145///
146/// # Returns
147///
148/// The `McAppSKey`.
149#[must_use]
150pub fn mc_app_s_key(mc_key: &[u8; 16], mc_addr: u32) -> [u8; 16] {
151 session_key(mc_key, 0x01, mc_addr)
152}
153
154/// Derives the key that verifies a group's frames, section 4.3.
155///
156/// # Arguments
157///
158/// * `mc_key` - the group key.
159/// * `mc_addr` - the group's address.
160///
161/// # Returns
162///
163/// The `McNwkSKey`.
164#[must_use]
165pub fn mc_nwk_s_key(mc_key: &[u8; 16], mc_addr: u32) -> [u8; 16] {
166 session_key(mc_key, 0x02, mc_addr)
167}
168
169/// One of the two group session keys.
170fn session_key(mc_key: &[u8; 16], tag: u8, mc_addr: u32) -> [u8; 16] {
171 let mut block = [0u8; 16];
172 block[0] = tag;
173 block[1..5].copy_from_slice(&mc_addr.to_le_bytes());
174 Cipher::new(mc_key).encrypt_block(&block)
175}
176
177/// What a device makes of a session a server asked it to open, table 20.
178#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
179pub struct SessionStatus {
180 /// Which group the session is on.
181 pub mc_group_id: u8,
182 /// The data rate is not one the device has.
183 pub dr_error: bool,
184 /// The frequency is not one the device can use.
185 pub freq_error: bool,
186 /// The device holds no such group.
187 pub group_undefined: bool,
188 /// The session was to start at a time already past.
189 pub start_missed: bool,
190}
191
192impl SessionStatus {
193 /// Whether the device will open the session.
194 ///
195 /// # Returns
196 ///
197 /// `true` when no error bit is set, in which case the answer carries when it starts.
198 #[must_use]
199 pub const fn accepted(&self) -> bool {
200 !self.dr_error && !self.freq_error && !self.group_undefined && !self.start_missed
201 }
202
203 /// Reads the coded status.
204 const fn from_bits(bits: u8) -> SessionStatus {
205 SessionStatus {
206 mc_group_id: bits & 0x03,
207 dr_error: bits & 0x04 != 0,
208 freq_error: bits & 0x08 != 0,
209 group_undefined: bits & 0x10 != 0,
210 start_missed: bits & 0x20 != 0,
211 }
212 }
213
214 /// Writes the coded status.
215 const fn bits(&self) -> u8 {
216 (self.mc_group_id & 0x03)
217 | ((self.dr_error as u8) << 2)
218 | ((self.freq_error as u8) << 3)
219 | ((self.group_undefined as u8) << 4)
220 | ((self.start_missed as u8) << 5)
221 }
222}
223
224/// One command of the remote multicast setup package.
225#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
226pub enum McCommand {
227 /// The server asking which version of this package the device implements.
228 PackageVersionReq,
229 /// The device's answer, section 4.1.
230 PackageVersionAns(PackageVersion),
231 /// The server asking which groups the device holds, section 4.2.
232 McGroupStatusReq {
233 /// Which groups to report on, a bit for each.
234 req_group_mask: u8,
235 },
236 /// The device's answer, section 4.2. The groups themselves follow in the message.
237 McGroupStatusAns {
238 /// Which groups this answer covers, a bit for each.
239 ans_group_mask: u8,
240 /// How many groups the device holds in all.
241 nb_total_groups: u8,
242 },
243 /// One group of a status answer: its identifier and its address.
244 McGroupStatusItem {
245 /// Which group.
246 mc_group_id: u8,
247 /// Its address.
248 mc_addr: u32,
249 },
250 /// The server creating or changing a group, section 4.3.
251 McGroupSetupReq {
252 /// Which group, 0 to 3.
253 mc_group_id: u8,
254 /// The address the group answers to.
255 mc_addr: u32,
256 /// The group key, wrapped under the device's key encryption key.
257 mc_key_encrypted: [u8; 16],
258 /// The first frame counter the device accepts from the group.
259 min_mc_fcnt: u32,
260 /// The last one, which ends the group's life.
261 max_mc_fcnt: u32,
262 },
263 /// The device's answer, section 4.3.
264 McGroupSetupAns {
265 /// Which group.
266 mc_group_id: u8,
267 /// Whether the device holds no group by that identifier.
268 id_error: bool,
269 },
270 /// The server deleting a group, section 4.4.
271 McGroupDeleteReq {
272 /// Which group.
273 mc_group_id: u8,
274 },
275 /// The device's answer, section 4.4.
276 McGroupDeleteAns {
277 /// Which group.
278 mc_group_id: u8,
279 /// Whether the device held no such group.
280 group_undefined: bool,
281 },
282 /// The server opening a Class C window on a group, section 4.5.
283 McClassCSessionReq {
284 /// Which group.
285 mc_group_id: u8,
286 /// When the window opens, in seconds since the GPS epoch.
287 session_time: u32,
288 /// How long it lasts at most: two to the power of this, in seconds.
289 time_out: u8,
290 /// Where the group listens, in hertz.
291 dl_frequency_hz: u32,
292 /// The data rate it listens at.
293 data_rate: u8,
294 },
295 /// The device's answer, section 4.5.
296 McClassCSessionAns {
297 /// What the device made of it.
298 status: SessionStatus,
299 /// How many seconds until the window opens, for a session it will open.
300 time_to_start: Option<u32>,
301 },
302 /// The server opening a Class B window on a group, section 4.6.
303 McClassBSessionReq {
304 /// Which group.
305 mc_group_id: u8,
306 /// When the window opens, a multiple of the beacon period.
307 session_time: u32,
308 /// How long it lasts at most: 128 seconds times two to the power of this.
309 time_out: u8,
310 /// How often the device opens a ping slot inside it.
311 periodicity: u8,
312 /// Where the group listens, in hertz.
313 dl_frequency_hz: u32,
314 /// The data rate it listens at.
315 data_rate: u8,
316 },
317 /// The device's answer, section 4.6.
318 McClassBSessionAns {
319 /// What the device made of it.
320 status: SessionStatus,
321 /// How many seconds until the window opens, for a session it will open.
322 time_to_start: Option<u32>,
323 },
324}
325
326impl McCommand {
327 /// The identifier this command travels under.
328 ///
329 /// # Returns
330 ///
331 /// The identifier, 0x00 to 0x05.
332 #[must_use]
333 pub const fn cid(&self) -> u8 {
334 match self {
335 McCommand::PackageVersionReq | McCommand::PackageVersionAns(_) => CID_PACKAGE_VERSION,
336 McCommand::McGroupStatusReq { .. }
337 | McCommand::McGroupStatusAns { .. }
338 | McCommand::McGroupStatusItem { .. } => CID_GROUP_STATUS,
339 McCommand::McGroupSetupReq { .. } | McCommand::McGroupSetupAns { .. } => {
340 CID_GROUP_SETUP
341 }
342 McCommand::McGroupDeleteReq { .. } | McCommand::McGroupDeleteAns { .. } => {
343 CID_GROUP_DELETE
344 }
345 McCommand::McClassCSessionReq { .. } | McCommand::McClassCSessionAns { .. } => {
346 CID_CLASS_C_SESSION
347 }
348 McCommand::McClassBSessionReq { .. } | McCommand::McClassBSessionAns { .. } => {
349 CID_CLASS_B_SESSION
350 }
351 }
352 }
353
354 /// Which way this command travels.
355 ///
356 /// # Returns
357 ///
358 /// [`Direction::Uplink`] for what a device sends, [`Direction::Downlink`] for what a
359 /// server sends.
360 #[must_use]
361 pub const fn direction(&self) -> Direction {
362 match self {
363 McCommand::PackageVersionAns(_)
364 | McCommand::McGroupStatusAns { .. }
365 | McCommand::McGroupStatusItem { .. }
366 | McCommand::McGroupSetupAns { .. }
367 | McCommand::McGroupDeleteAns { .. }
368 | McCommand::McClassCSessionAns { .. }
369 | McCommand::McClassBSessionAns { .. } => Direction::Uplink,
370 _ => Direction::Downlink,
371 }
372 }
373
374 /// Reads one command from the front of a message.
375 ///
376 /// A status answer reads only its own byte: the groups it covers follow as
377 /// [`McCommand::McGroupStatusItem`] records, five bytes each, which
378 /// [`McCommand::status_item`] reads.
379 ///
380 /// # Arguments
381 ///
382 /// * `direction` - which way the frame carrying it traveled.
383 /// * `bytes` - the message, from this command's identifier on.
384 ///
385 /// # Returns
386 ///
387 /// The command and how many bytes it took.
388 ///
389 /// # Errors
390 ///
391 /// [`LorawanError::MalformedFrame`] when the message ends inside the command, and
392 /// [`LorawanError::UnknownCommand`] for an identifier this package does not define.
393 pub fn parse(direction: Direction, bytes: &[u8]) -> Result<(McCommand, usize), LorawanError> {
394 let cid = *bytes.first().ok_or(LorawanError::MalformedFrame)?;
395 let up = matches!(direction, Direction::Uplink);
396 match (cid, up) {
397 (CID_PACKAGE_VERSION, false) => Ok((McCommand::PackageVersionReq, 1)),
398 (CID_PACKAGE_VERSION, true) => {
399 let (fields, taken) = payload(bytes, 2)?;
400 Ok((
401 McCommand::PackageVersionAns(PackageVersion {
402 package: fields[0],
403 version: fields[1],
404 }),
405 taken,
406 ))
407 }
408 (CID_GROUP_STATUS, false) => {
409 let (fields, taken) = payload(bytes, 1)?;
410 Ok((
411 McCommand::McGroupStatusReq {
412 req_group_mask: fields[0] & 0x0F,
413 },
414 taken,
415 ))
416 }
417 (CID_GROUP_STATUS, true) => {
418 let (fields, taken) = payload(bytes, 1)?;
419 Ok((
420 McCommand::McGroupStatusAns {
421 ans_group_mask: fields[0] & 0x0F,
422 nb_total_groups: (fields[0] >> 4) & 0x07,
423 },
424 taken,
425 ))
426 }
427 (CID_GROUP_SETUP, false) => {
428 let (fields, taken) = payload(bytes, 29)?;
429 let mut key = [0u8; 16];
430 key.copy_from_slice(&fields[5..21]);
431 Ok((
432 McCommand::McGroupSetupReq {
433 mc_group_id: fields[0] & 0x03,
434 mc_addr: u32::from_le_bytes([fields[1], fields[2], fields[3], fields[4]]),
435 mc_key_encrypted: key,
436 min_mc_fcnt: u32::from_le_bytes([
437 fields[21], fields[22], fields[23], fields[24],
438 ]),
439 max_mc_fcnt: u32::from_le_bytes([
440 fields[25], fields[26], fields[27], fields[28],
441 ]),
442 },
443 taken,
444 ))
445 }
446 (CID_GROUP_SETUP, true) => {
447 let (fields, taken) = payload(bytes, 1)?;
448 Ok((
449 McCommand::McGroupSetupAns {
450 mc_group_id: fields[0] & 0x03,
451 id_error: fields[0] & 0x04 != 0,
452 },
453 taken,
454 ))
455 }
456 (CID_GROUP_DELETE, false) => {
457 let (fields, taken) = payload(bytes, 1)?;
458 Ok((
459 McCommand::McGroupDeleteReq {
460 mc_group_id: fields[0] & 0x03,
461 },
462 taken,
463 ))
464 }
465 (CID_GROUP_DELETE, true) => {
466 let (fields, taken) = payload(bytes, 1)?;
467 Ok((
468 McCommand::McGroupDeleteAns {
469 mc_group_id: fields[0] & 0x03,
470 group_undefined: fields[0] & 0x04 != 0,
471 },
472 taken,
473 ))
474 }
475 (CID_CLASS_C_SESSION, false) => {
476 let (fields, taken) = payload(bytes, 10)?;
477 Ok((
478 McCommand::McClassCSessionReq {
479 mc_group_id: fields[0] & 0x03,
480 session_time: u32::from_le_bytes([
481 fields[1], fields[2], fields[3], fields[4],
482 ]),
483 time_out: fields[5] & 0x0F,
484 dl_frequency_hz: frequency(&fields[6..9]),
485 data_rate: fields[9],
486 },
487 taken,
488 ))
489 }
490 (CID_CLASS_B_SESSION, false) => {
491 let (fields, taken) = payload(bytes, 10)?;
492 Ok((
493 McCommand::McClassBSessionReq {
494 mc_group_id: fields[0] & 0x03,
495 session_time: u32::from_le_bytes([
496 fields[1], fields[2], fields[3], fields[4],
497 ]),
498 time_out: fields[5] & 0x0F,
499 periodicity: (fields[5] >> 4) & 0x07,
500 dl_frequency_hz: frequency(&fields[6..9]),
501 data_rate: fields[9],
502 },
503 taken,
504 ))
505 }
506 (CID_CLASS_C_SESSION | CID_CLASS_B_SESSION, true) => {
507 let (fields, _) = payload(bytes, 1)?;
508 let status = SessionStatus::from_bits(fields[0]);
509 let (time_to_start, taken) = if status.accepted() {
510 let (fields, taken) = payload(bytes, 4)?;
511 (
512 Some(u32::from_le_bytes([fields[1], fields[2], fields[3], 0])),
513 taken,
514 )
515 } else {
516 (None, 2)
517 };
518 Ok((
519 if cid == CID_CLASS_C_SESSION {
520 McCommand::McClassCSessionAns {
521 status,
522 time_to_start,
523 }
524 } else {
525 McCommand::McClassBSessionAns {
526 status,
527 time_to_start,
528 }
529 },
530 taken,
531 ))
532 }
533 _ => Err(LorawanError::UnknownCommand(cid)),
534 }
535 }
536
537 /// Reads one group record of a status answer, section 4.2.
538 ///
539 /// # Arguments
540 ///
541 /// * `bytes` - the message from the record on, five bytes or more.
542 ///
543 /// # Returns
544 ///
545 /// The record and how many bytes it took.
546 ///
547 /// # Errors
548 ///
549 /// [`LorawanError::MalformedFrame`] when the message ends inside the record.
550 pub fn status_item(bytes: &[u8]) -> Result<(McCommand, usize), LorawanError> {
551 let fields = bytes.get(..5).ok_or(LorawanError::MalformedFrame)?;
552 Ok((
553 McCommand::McGroupStatusItem {
554 mc_group_id: fields[0] & 0x03,
555 mc_addr: u32::from_le_bytes([fields[1], fields[2], fields[3], fields[4]]),
556 },
557 5,
558 ))
559 }
560
561 /// Writes this command out.
562 ///
563 /// # Arguments
564 ///
565 /// * `out` - where to write it, at least [`MAX_COMMAND`] bytes for any command.
566 ///
567 /// # Returns
568 ///
569 /// How many bytes were written.
570 ///
571 /// # Errors
572 ///
573 /// [`LorawanError::PayloadTooLong`] when the buffer is too small.
574 pub fn encode(&self, out: &mut [u8]) -> Result<usize, LorawanError> {
575 match self {
576 McCommand::PackageVersionReq => write(out, CID_PACKAGE_VERSION, &[]),
577 McCommand::PackageVersionAns(version) => write(
578 out,
579 CID_PACKAGE_VERSION,
580 &[version.package, version.version],
581 ),
582 McCommand::McGroupStatusReq { req_group_mask } => {
583 write(out, CID_GROUP_STATUS, &[req_group_mask & 0x0F])
584 }
585 McCommand::McGroupStatusAns {
586 ans_group_mask,
587 nb_total_groups,
588 } => write(
589 out,
590 CID_GROUP_STATUS,
591 &[(ans_group_mask & 0x0F) | ((nb_total_groups & 0x07) << 4)],
592 ),
593 McCommand::McGroupStatusItem {
594 mc_group_id,
595 mc_addr,
596 } => {
597 let addr = mc_addr.to_le_bytes();
598 let room = out.get_mut(..5).ok_or(LorawanError::PayloadTooLong)?;
599 room[0] = mc_group_id & 0x03;
600 room[1..5].copy_from_slice(&addr);
601 Ok(5)
602 }
603 McCommand::McGroupSetupReq {
604 mc_group_id,
605 mc_addr,
606 mc_key_encrypted,
607 min_mc_fcnt,
608 max_mc_fcnt,
609 } => {
610 let mut fields = [0u8; 29];
611 fields[0] = mc_group_id & 0x03;
612 fields[1..5].copy_from_slice(&mc_addr.to_le_bytes());
613 fields[5..21].copy_from_slice(mc_key_encrypted);
614 fields[21..25].copy_from_slice(&min_mc_fcnt.to_le_bytes());
615 fields[25..29].copy_from_slice(&max_mc_fcnt.to_le_bytes());
616 write(out, CID_GROUP_SETUP, &fields)
617 }
618 McCommand::McGroupSetupAns {
619 mc_group_id,
620 id_error,
621 } => write(
622 out,
623 CID_GROUP_SETUP,
624 &[(mc_group_id & 0x03) | (u8::from(*id_error) << 2)],
625 ),
626 McCommand::McGroupDeleteReq { mc_group_id } => {
627 write(out, CID_GROUP_DELETE, &[mc_group_id & 0x03])
628 }
629 McCommand::McGroupDeleteAns {
630 mc_group_id,
631 group_undefined,
632 } => write(
633 out,
634 CID_GROUP_DELETE,
635 &[(mc_group_id & 0x03) | (u8::from(*group_undefined) << 2)],
636 ),
637 McCommand::McClassCSessionReq {
638 mc_group_id,
639 session_time,
640 time_out,
641 dl_frequency_hz,
642 data_rate,
643 } => {
644 let fields = session_fields(
645 *mc_group_id,
646 *session_time,
647 time_out & 0x0F,
648 *dl_frequency_hz,
649 *data_rate,
650 );
651 write(out, CID_CLASS_C_SESSION, &fields)
652 }
653 McCommand::McClassBSessionReq {
654 mc_group_id,
655 session_time,
656 time_out,
657 periodicity,
658 dl_frequency_hz,
659 data_rate,
660 } => {
661 let fields = session_fields(
662 *mc_group_id,
663 *session_time,
664 (time_out & 0x0F) | ((periodicity & 0x07) << 4),
665 *dl_frequency_hz,
666 *data_rate,
667 );
668 write(out, CID_CLASS_B_SESSION, &fields)
669 }
670 McCommand::McClassCSessionAns {
671 status,
672 time_to_start,
673 } => session_answer(out, CID_CLASS_C_SESSION, *status, *time_to_start),
674 McCommand::McClassBSessionAns {
675 status,
676 time_to_start,
677 } => session_answer(out, CID_CLASS_B_SESSION, *status, *time_to_start),
678 }
679 }
680}
681
682/// Reads a three-byte frequency, in hundreds of hertz as `NewChannelReq` codes it.
683fn frequency(bytes: &[u8]) -> u32 {
684 u32::from_le_bytes([bytes[0], bytes[1], bytes[2], 0]) * 100
685}
686
687/// The fields a session request carries, which the two classes share.
688fn session_fields(
689 mc_group_id: u8,
690 session_time: u32,
691 coded_timeout: u8,
692 dl_frequency_hz: u32,
693 data_rate: u8,
694) -> [u8; 10] {
695 let mut fields = [0u8; 10];
696 fields[0] = mc_group_id & 0x03;
697 fields[1..5].copy_from_slice(&session_time.to_le_bytes());
698 fields[5] = coded_timeout;
699 let coded = (dl_frequency_hz / 100).to_le_bytes();
700 fields[6..9].copy_from_slice(&coded[..3]);
701 fields[9] = data_rate;
702 fields
703}
704
705/// Writes a session answer, which carries its start only when the device will open it.
706fn session_answer(
707 out: &mut [u8],
708 cid: u8,
709 status: SessionStatus,
710 time_to_start: Option<u32>,
711) -> Result<usize, LorawanError> {
712 match time_to_start.filter(|_| status.accepted()) {
713 Some(start) => {
714 let coded = start.to_le_bytes();
715 write(out, cid, &[status.bits(), coded[0], coded[1], coded[2]])
716 }
717 None => write(out, cid, &[status.bits()]),
718 }
719}