Skip to main content

twine_codec/dataset/operational_dataset/
mod.rs

1// Copyright (c) 2025 Jake Swensen
2// SPDX-License-Identifier: MPL-2.0
3//
4// This Source Code Form is subject to the terms of the Mozilla Public
5// License, v. 2.0. If a copy of the MPL was not distributed with this
6// file, You can obtain one at http://mozilla.org/MPL/2.0/.
7
8use core::str::FromStr;
9
10use twine_tlv::TlvCollection;
11
12use crate::{
13    dataset::{
14        ActiveTimestamp, DelayTimer, ExtendedPanId, MeshLocalPrefix, NetworkKey, NetworkName,
15        PendingTimestamp, Pskc, SecurityPolicy, Timestamp,
16    },
17    radio::{Channel, ChannelMask, PanId},
18    TwineCodecError,
19};
20
21mod iter;
22pub use iter::OperationalDatasetIter;
23
24const OPERATIONAL_DATASET_MAX_SIZE: usize = 256;
25
26macro_rules! decode_type {
27    ($name:ident, $decode_type:ty) => {
28        pub fn $name(&self) -> Option<$decode_type> {
29            self.collection.decode_type_unchecked::<$decode_type>()
30        }
31    };
32}
33
34#[derive(Debug)]
35pub struct OperationalDataset {
36    collection: TlvCollection<OPERATIONAL_DATASET_MAX_SIZE>,
37}
38
39impl OperationalDataset {
40    /// Generate a new random Active Operational Dataset
41    #[cfg(any(test, feature = "std"))]
42    pub fn random() -> Result<Self, TwineCodecError> {
43        let mut collection = TlvCollection::default();
44
45        use crate::dataset::timestamp::Authoritative;
46        let active_timestamp = Timestamp::now(Authoritative(false));
47        let _ = collection.push(ActiveTimestamp::from(active_timestamp))?;
48
49        let channel = Channel::random();
50        let _ = collection.push(channel)?;
51
52        // todo: wake_up_channel
53
54        let channel_mask = ChannelMask::default();
55        let _ = collection.push(channel_mask)?;
56
57        let xpan = ExtendedPanId::random();
58        let _ = collection.push(xpan)?;
59
60        let mesh_local_prefix = MeshLocalPrefix::random_ula();
61        let _ = collection.push(mesh_local_prefix)?;
62
63        let network_key = NetworkKey::random();
64        let _ = collection.push(network_key)?;
65
66        let pan_id = PanId::random();
67        let network_name = alloc::format!("Twine-{:x}", pan_id.get());
68        let _ = collection.push(NetworkName::from_str(&network_name)?)?;
69
70        let _ = collection.push(pan_id)?;
71
72        let pskc = Pskc::random();
73        let _ = collection.push(pskc)?;
74
75        let security_policy = SecurityPolicy::default();
76        let _ = collection.push(security_policy)?;
77
78        Ok(Self { collection })
79    }
80
81    pub fn active_timestamp(&self) -> Option<Timestamp> {
82        self.collection
83            .decode_type_unchecked::<ActiveTimestamp>()
84            .map(Timestamp::from)
85    }
86
87    pub fn set_active_timestamp(&mut self, timestamp: Timestamp) -> Result<(), TwineCodecError> {
88        let active_timestamp = ActiveTimestamp::from(timestamp);
89        self.collection.replace_or_push(active_timestamp)?;
90        Ok(())
91    }
92
93    pub fn pending_timestamp(&self) -> Option<Timestamp> {
94        self.collection
95            .decode_type_unchecked::<PendingTimestamp>()
96            .map(Timestamp::from)
97    }
98
99    decode_type!(delay_timer, DelayTimer);
100    decode_type!(channel, Channel);
101    // todo: wake_up_channel
102    decode_type!(pan_id, PanId);
103    decode_type!(channel_mask, ChannelMask);
104    decode_type!(extended_pan_id, ExtendedPanId);
105    decode_type!(network_name, NetworkName);
106    decode_type!(pskc, Pskc);
107    decode_type!(network_key, NetworkKey);
108    decode_type!(mesh_local_prefix, MeshLocalPrefix);
109    decode_type!(security_policy, SecurityPolicy);
110
111    #[cfg(any(test, feature = "std"))]
112    pub fn pretty_fmt(&self) {
113        std::println!("Operational Dataset: {:?}", self);
114        self.iter().for_each(|item| std::println!("{item:?}"));
115    }
116
117    pub fn iter(&self) -> OperationalDatasetIter<'_> {
118        OperationalDatasetIter {
119            inner: (&self.collection).into_iter(),
120        }
121    }
122
123    #[cfg(any(test, feature = "alloc"))]
124    pub fn as_hex_string(&self) -> alloc::string::String {
125        let mut hex_string = alloc::string::String::new();
126        for tlv in &self.collection {
127            hex_string.push_str(&hex::encode(tlv));
128        }
129        hex_string
130    }
131}
132
133impl FromStr for OperationalDataset {
134    type Err = TwineCodecError;
135
136    fn from_str(s: &str) -> Result<Self, Self::Err> {
137        // Ensure even number of characters
138        if (s.len() & 1) != 0 {
139            return Err(TwineCodecError::HexDecodeError);
140        }
141
142        let n = s.len() / 2;
143        let mut buffer = [0_u8; OPERATIONAL_DATASET_MAX_SIZE];
144
145        // Ensure buffer is large enough
146        if n > buffer.len() {
147            return Err(TwineCodecError::HexDecodeError);
148        }
149
150        hex::decode_to_slice(s, &mut buffer[..n]).map_err(|_| TwineCodecError::HexDecodeError)?;
151        let collection = TlvCollection::new_from_static(buffer);
152
153        Ok(Self { collection })
154    }
155}
156
157impl core::fmt::Display for OperationalDataset {
158    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
159        self.iter().try_fold((), |_, item| writeln!(f, "{item}"))
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use crate::{dataset::timestamp::Authoritative, SecurityPolicyBuilder};
166
167    use super::*;
168
169    #[test]
170    fn success_from_str() {
171        let dataset_str = "0e080000000000010000000300000c4a0300001335060004001fffe002081bb896bef533a5850708fd48b2e8c34e7dc70510e9b948988752752873570d09ada4d0be030f4f70656e5468726561642d623364650102b3de0410f9f07ed37fbb6828fb3b26b63bdea3c30c0402a0f7f8";
172        let dataset = OperationalDataset::from_str(dataset_str).unwrap();
173
174        let active_timestamp = dataset.active_timestamp().unwrap();
175        let channel = dataset.channel().unwrap();
176        // wake_up_channel
177        // channel_mask
178        let xpan = dataset.extended_pan_id().unwrap();
179        let mesh_local_prefix: MeshLocalPrefix = dataset.mesh_local_prefix().unwrap();
180        let network_key = dataset.network_key().unwrap();
181        let network_name = dataset.network_name().unwrap();
182        let pan_id = dataset.pan_id().unwrap();
183        let pskc = dataset.pskc().unwrap();
184        let security_policy = dataset.security_policy().unwrap();
185
186        assert_eq!(
187            active_timestamp,
188            Timestamp::from((1, 1, Authoritative(false)))
189        );
190        assert_eq!(channel, Channel::new(0, 12));
191        // wake up channel
192        // channel mask
193        assert_eq!(
194            xpan,
195            ExtendedPanId::from([0x1b, 0xb8, 0x96, 0xbe, 0xf5, 0x33, 0xa5, 0x85])
196        );
197        assert_eq!(
198            mesh_local_prefix,
199            MeshLocalPrefix::from([0xfd, 0x48, 0xb2, 0xe8, 0xc3, 0x4e, 0x7d, 0xc7])
200        );
201        assert_eq!(
202            network_key,
203            NetworkKey::from(u128::from_be_bytes([
204                0xe9, 0xb9, 0x48, 0x98, 0x87, 0x52, 0x75, 0x28, 0x73, 0x57, 0x0d, 0x09, 0xad, 0xa4,
205                0xd0, 0xbe
206            ]))
207        );
208        assert_eq!(
209            network_name,
210            NetworkName::from_str("OpenThread-b3de").unwrap()
211        );
212        assert_eq!(pan_id, PanId::from(0xb3de));
213        assert_eq!(
214            pskc,
215            Pskc::from([
216                0xf9, 0xf0, 0x7e, 0xd3, 0x7f, 0xbb, 0x68, 0x28, 0xfb, 0x3b, 0x26, 0xb6, 0x3b, 0xde,
217                0xa3, 0xc3
218            ])
219        );
220        assert_eq!(
221            security_policy,
222            SecurityPolicyBuilder::with_default_policy()
223                .build()
224                .unwrap()
225        );
226    }
227}