Skip to main content

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}