rustot/provisioning/
topics.rs

1use core::fmt::Display;
2use core::fmt::Write;
3use core::str::FromStr;
4
5use heapless::String;
6use mqttrust::{Mqtt, QoS, SubscribeTopic};
7
8use super::Error;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11#[cfg_attr(feature = "defmt", derive(defmt::Format))]
12pub enum Direction {
13    Incoming,
14    Outgoing,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq)]
18#[cfg_attr(feature = "defmt", derive(defmt::Format))]
19pub enum PayloadFormat {
20    #[cfg(feature = "provision_cbor")]
21    Cbor,
22    Json,
23}
24
25impl Display for PayloadFormat {
26    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
27        match self {
28            #[cfg(feature = "provision_cbor")]
29            Self::Cbor => write!(f, "cbor"),
30            Self::Json => write!(f, "json"),
31        }
32    }
33}
34
35impl FromStr for PayloadFormat {
36    type Err = ();
37
38    fn from_str(s: &str) -> Result<Self, Self::Err> {
39        match s {
40            #[cfg(feature = "provision_cbor")]
41            "cbor" => Ok(Self::Cbor),
42            "json" => Ok(Self::Json),
43            _ => Err(()),
44        }
45    }
46}
47
48#[derive(Debug, Clone, PartialEq)]
49#[cfg_attr(feature = "defmt", derive(defmt::Format))]
50pub enum Topic<'a> {
51    // ---- Outgoing Topics
52    /// `$aws/provisioning-templates/<templateName>/provision/<payloadFormat>`
53    RegisterThing(&'a str, PayloadFormat),
54
55    /// $aws/certificates/create/<payloadFormat>
56    CreateKeysAndCertificate(PayloadFormat),
57
58    /// $aws/certificates/create-from-csr/<payloadFormat>
59    CreateCertificateFromCsr(PayloadFormat),
60
61    // ---- Incoming Topics
62    /// `$aws/provisioning-templates/<templateName>/provision/<payloadFormat>/accepted`
63    RegisterThingAccepted(&'a str, PayloadFormat),
64
65    /// `$aws/provisioning-templates/<templateName>/provision/<payloadFormat>/rejected`
66    RegisterThingRejected(&'a str, PayloadFormat),
67
68    /// `$aws/certificates/create/<payloadFormat>/accepted`
69    CreateKeysAndCertificateAccepted(PayloadFormat),
70
71    /// `$aws/certificates/create/<payloadFormat>/rejected`
72    CreateKeysAndCertificateRejected(PayloadFormat),
73
74    /// `$aws/certificates/create-from-csr/<payloadFormat>/accepted`
75    CreateCertificateFromCsrAccepted(PayloadFormat),
76
77    /// `$aws/certificates/create-from-csr/<payloadFormat>/rejected`
78    CreateCertificateFromCsrRejected(PayloadFormat),
79}
80
81impl<'a> Topic<'a> {
82    const CERT_PREFIX: &'static str = "$aws/certificates";
83    const PROVISIONING_PREFIX: &'static str = "$aws/provisioning-templates";
84
85    pub fn check(s: &'a str) -> bool {
86        s.starts_with(Self::CERT_PREFIX) || s.starts_with(Self::PROVISIONING_PREFIX)
87    }
88
89    pub fn from_str(s: &'a str) -> Option<Self> {
90        let tt = s.splitn(6, '/').collect::<heapless::Vec<&str, 6>>();
91        match (tt.get(0), tt.get(1)) {
92            (Some(&"$aws"), Some(&"provisioning-templates")) => {
93                // This is a register thing topic, now figure out which one.
94
95                match (tt.get(2), tt.get(3), tt.get(4), tt.get(5)) {
96                    (
97                        Some(template_name),
98                        Some(&"provision"),
99                        Some(payload_format),
100                        Some(&"accepted"),
101                    ) => Some(Topic::RegisterThingAccepted(
102                        *template_name,
103                        PayloadFormat::from_str(payload_format).ok()?,
104                    )),
105                    (
106                        Some(template_name),
107                        Some(&"provision"),
108                        Some(payload_format),
109                        Some(&"rejected"),
110                    ) => Some(Topic::RegisterThingRejected(
111                        *template_name,
112                        PayloadFormat::from_str(payload_format).ok()?,
113                    )),
114                    _ => None,
115                }
116            }
117            (Some(&"$aws"), Some(&"certificates")) => {
118                // This is a register thing topic, now figure out which one.
119
120                match (tt.get(2), tt.get(3), tt.get(4)) {
121                    (Some(&"create"), Some(payload_format), Some(&"accepted")) => {
122                        Some(Topic::CreateKeysAndCertificateAccepted(
123                            PayloadFormat::from_str(payload_format).ok()?,
124                        ))
125                    }
126                    (Some(&"create"), Some(payload_format), Some(&"rejected")) => {
127                        Some(Topic::CreateKeysAndCertificateRejected(
128                            PayloadFormat::from_str(payload_format).ok()?,
129                        ))
130                    }
131                    (Some(&"create-from-csr"), Some(payload_format), Some(&"accepted")) => {
132                        Some(Topic::CreateCertificateFromCsrAccepted(
133                            PayloadFormat::from_str(payload_format).ok()?,
134                        ))
135                    }
136                    (Some(&"create-from-csr"), Some(payload_format), Some(&"rejected")) => {
137                        Some(Topic::CreateCertificateFromCsrRejected(
138                            PayloadFormat::from_str(payload_format).ok()?,
139                        ))
140                    }
141                    _ => None,
142                }
143            }
144            _ => None,
145        }
146    }
147
148    pub fn direction(&self) -> Direction {
149        if matches!(
150            self,
151            Topic::RegisterThing(_, _)
152                | Topic::CreateKeysAndCertificate(_)
153                | Topic::CreateCertificateFromCsr(_)
154        ) {
155            Direction::Outgoing
156        } else {
157            Direction::Incoming
158        }
159    }
160
161    pub fn format<const L: usize>(&self) -> Result<String<L>, Error> {
162        let mut topic_path = String::new();
163        match self {
164            Self::RegisterThing(template_name, payload_format) => {
165                topic_path.write_fmt(format_args!(
166                    "{}/{}/provision/{}",
167                    Self::PROVISIONING_PREFIX,
168                    template_name,
169                    payload_format,
170                ))
171            }
172            Topic::RegisterThingAccepted(template_name, payload_format) => {
173                topic_path.write_fmt(format_args!(
174                    "{}/{}/provision/{}/accepted",
175                    Self::PROVISIONING_PREFIX,
176                    template_name,
177                    payload_format,
178                ))
179            }
180            Topic::RegisterThingRejected(template_name, payload_format) => {
181                topic_path.write_fmt(format_args!(
182                    "{}/{}/provision/{}/rejected",
183                    Self::PROVISIONING_PREFIX,
184                    template_name,
185                    payload_format,
186                ))
187            }
188
189            Topic::CreateKeysAndCertificate(payload_format) => topic_path.write_fmt(format_args!(
190                "{}/create/{}",
191                Self::CERT_PREFIX,
192                payload_format,
193            )),
194
195            Topic::CreateKeysAndCertificateAccepted(payload_format) => topic_path.write_fmt(
196                format_args!("{}/create/{}/accepted", Self::CERT_PREFIX, payload_format),
197            ),
198            Topic::CreateKeysAndCertificateRejected(payload_format) => topic_path.write_fmt(
199                format_args!("{}/create/{}/rejected", Self::CERT_PREFIX, payload_format),
200            ),
201
202            Topic::CreateCertificateFromCsr(payload_format) => topic_path.write_fmt(format_args!(
203                "{}/create-from-csr/{}",
204                Self::CERT_PREFIX,
205                payload_format,
206            )),
207            Topic::CreateCertificateFromCsrAccepted(payload_format) => topic_path.write_fmt(
208                format_args!("{}/create-from-csr/{}", Self::CERT_PREFIX, payload_format),
209            ),
210            Topic::CreateCertificateFromCsrRejected(payload_format) => topic_path.write_fmt(
211                format_args!("{}/create-from-csr/{}", Self::CERT_PREFIX, payload_format),
212            ),
213        }
214        .map_err(|_| Error::Overflow)?;
215
216        Ok(topic_path)
217    }
218}
219
220#[derive(Default)]
221pub struct Subscribe<'a, const N: usize> {
222    topics: heapless::Vec<(Topic<'a>, QoS), N>,
223}
224
225impl<'a, const N: usize> Subscribe<'a, N> {
226    pub fn new() -> Self {
227        Self::default()
228    }
229
230    pub fn topic(self, topic: Topic<'a>, qos: QoS) -> Self {
231        // Ignore attempts to subscribe to outgoing topics
232        if topic.direction() != Direction::Incoming {
233            return self;
234        }
235
236        if self.topics.iter().any(|(t, _)| t == &topic) {
237            return self;
238        }
239
240        let mut topics = self.topics;
241        topics.push((topic, qos)).ok();
242
243        Self { topics }
244    }
245
246    pub fn topics(self) -> Result<heapless::Vec<(heapless::String<128>, QoS), N>, Error> {
247        self.topics
248            .iter()
249            .map(|(topic, qos)| Ok((topic.clone().format()?, *qos)))
250            .collect()
251    }
252
253    pub fn send<M: Mqtt>(self, mqtt: &M) -> Result<(), Error> {
254        if self.topics.is_empty() {
255            return Ok(());
256        }
257
258        let topic_paths = self.topics()?;
259
260        debug!("Subscribing! {:?}", topic_paths);
261
262        let topics: heapless::Vec<_, N> = topic_paths
263            .iter()
264            .map(|(s, qos)| SubscribeTopic {
265                topic_path: s.as_str(),
266                qos: *qos,
267            })
268            .collect();
269
270        for t in topics.chunks(5) {
271            mqtt.subscribe(t)?;
272        }
273        Ok(())
274    }
275}
276
277#[derive(Default)]
278pub struct Unsubscribe<'a, const N: usize> {
279    topics: heapless::Vec<Topic<'a>, N>,
280}
281
282impl<'a, const N: usize> Unsubscribe<'a, N> {
283    pub fn new() -> Self {
284        Self::default()
285    }
286
287    pub fn topic(self, topic: Topic<'a>) -> Self {
288        // Ignore attempts to subscribe to outgoing topics
289        if topic.direction() != Direction::Incoming {
290            return self;
291        }
292
293        if self.topics.iter().any(|t| t == &topic) {
294            return self;
295        }
296
297        let mut topics = self.topics;
298        topics.push(topic).ok();
299        Self { topics }
300    }
301
302    pub fn topics(self) -> Result<heapless::Vec<heapless::String<256>, N>, Error> {
303        self.topics
304            .iter()
305            .map(|topic| topic.clone().format())
306            .collect()
307    }
308
309    pub fn send<M: Mqtt>(self, mqtt: &M) -> Result<(), Error> {
310        if self.topics.is_empty() {
311            return Ok(());
312        }
313
314        let topic_paths = self.topics()?;
315        let topics: heapless::Vec<_, N> = topic_paths.iter().map(|s| s.as_str()).collect();
316
317        for t in topics.chunks(5) {
318            mqtt.unsubscribe(t)?;
319        }
320
321        Ok(())
322    }
323}