coap_scroll_ring_server/
lib.rs

1//! An implemenmtation of a [coap_handler::Handler] around a [scroll_ring::Buffer]
2//!
3//! ## Usage
4//!
5//! Wrap a buffer (typically some tasks's linear text output) through
6//! [`BufferHandler::new(&buf)`](BufferHandler::new) and place it [`.at(&["output"],
7//! ...)`](coap_handler_implementations::HandlerBuilder::at) some CoAP handler tree.
8//!
9//! ## Examples
10//!
11//! This crate comes with no example, but the
12//! [coap-message-demos](https://crates.io/crates/coap-message-demos) crate makes ample use of it
13//! for its logging adapter.
14//!
15//! ## Tools
16//!
17//! In the `tools/` directory of this crate there is a Python script, which should be useful for
18//! speedy collection of a ring buffer server's output.
19#![no_std]
20
21use coap_message::{
22    Code as _, MinimalWritableMessage, MutableWritableMessage, OptionNumber as _, ReadableMessage,
23};
24use coap_message_utils::Error;
25use coap_numbers::{code, content_format, option};
26use core::num::Wrapping;
27
28/// CoAP handler for reading from a [Buffer](scroll_ring::Buffer)
29///
30/// This implements a variant of the
31/// [proposed](https://forum.riot-os.org/t/coap-remote-shell/3340/5) stdio handling. It deviates in
32/// some details, in particular:
33///
34/// * It uses CBOR streams rather than a response array, and no array pairs.
35/// * It always sends offset and a single data item (effecively leaving it open whether the pattern
36///   would be continued `[5, "hello ", 11, "world"]` or `[5, "hello ", "world"]`).
37/// * GET always reports the oldest available data (that's probably underspecified in the original
38///   proposal)
39/// * GETs return immediately.
40/// * FETCH only takes a single integer: where to start.
41/// * No paired channel.
42pub struct BufferHandler<'a, const N: usize>(&'a scroll_ring::Buffer<N>);
43
44impl<'a, const N: usize> BufferHandler<'a, N> {
45    /// Construct a handler for a buffer
46    ///
47    /// This adds nothing to the buffer other than the selection of the CoAP serialization; if it
48    /// were not for separation of concerns, the [coap_handler] implementation could just as well
49    /// be on the buffer itself.
50    pub fn new(buffer: &'a scroll_ring::Buffer<N>) -> Self {
51        Self(buffer)
52    }
53}
54
55#[doc(hidden)]
56pub enum RequestedScrollbufPosition {
57    FromStart,
58    StartingAt(Wrapping<u32>),
59}
60
61#[doc(hidden)]
62#[derive(Debug)]
63// This might be a candidate for inclusion in coap_handler_implementations.
64pub enum BuildResponseError<SE: coap_message::error::RenderableOnMinimal + core::fmt::Debug> {
65    Own(Error),
66    Stack(SE),
67}
68
69impl<SE: coap_message::error::RenderableOnMinimal + core::fmt::Debug>
70    coap_message::error::RenderableOnMinimal for BuildResponseError<SE>
71{
72    // We could try to come up with something more elaborate, but this is already an error type, so
73    // it'd only be rendered as a double fault. If this were made available more generally, it
74    // could try to be more precise here.
75    type Error<IE: coap_message::error::RenderableOnMinimal + core::fmt::Debug> = Error;
76    fn render<M: MinimalWritableMessage>(
77        self,
78        message: &mut M,
79    ) -> Result<(), Self::Error<M::UnionError>> {
80        match self {
81            BuildResponseError::Stack(se) => se.render(message).map_err(|_| {
82                coap_message_utils::Error::internal_server_error()
83                    .with_title("Stack error rendering failed")
84            })?,
85            BuildResponseError::Own(e) => e.render(message).map_err(|_| {
86                coap_message_utils::Error::internal_server_error()
87                    .with_title("Own error rendering failed")
88            })?,
89        }
90        Ok(())
91    }
92}
93
94use RequestedScrollbufPosition::*;
95
96impl<'a, const N: usize> coap_handler::Handler for BufferHandler<'a, N> {
97    type RequestData = RequestedScrollbufPosition;
98
99    type ExtractRequestError = Error;
100    type BuildResponseError<M: MinimalWritableMessage> = BuildResponseError<M::UnionError>;
101
102    fn extract_request_data<M: ReadableMessage>(
103        &mut self,
104        req: &M,
105    ) -> Result<RequestedScrollbufPosition, Error> {
106        use coap_message::MessageOption;
107        use coap_message_utils::OptionsExt;
108
109        req.options()
110            .filter(|o| {
111                !(o.number() == option::ACCEPT
112                    && o.value_uint::<u16>()
113                        == Some(content_format::from_str("application/cbor-seq").unwrap()))
114            })
115            // Not processing Content-Format: it's elective anyway
116            .ignore_elective_others()?;
117
118        Ok(match req.code().into() {
119            code::GET => FromStart,
120            code::FETCH => StartingAt(Wrapping({
121                let payload = req.payload();
122                // Quick and dirty parsing of a single CBOR uint up to 32bit
123                let (start, bytes_consumed) = match payload.get(0) {
124                    Some(i @ 0..=23) => (Some((*i).into()), 1),
125                    Some(24) => (payload.get(1).map(|&n| n.into()), 2),
126                    Some(25) => (
127                        payload
128                            .get(1..3)
129                            .map(|b| u16::from_be_bytes(b.try_into().unwrap()))
130                            .map(u32::from),
131                        3,
132                    ),
133                    Some(26) => (
134                        payload
135                            .get(1..5)
136                            .map(|b| u32::from_be_bytes(b.try_into().unwrap())),
137                        5,
138                    ),
139                    Some(_) => {
140                        return Err(
141                            Error::bad_request_with_rbep(0).with_title("Expected cursor position")
142                        )
143                    }
144                    None => return Err(Error::bad_request().with_title("Expected some CBOR")),
145                };
146                let Some(start) = start else {
147                    return Err(Error::bad_request_with_rbep(payload.len())
148                        .with_title("Integer out of range"));
149                };
150                if bytes_consumed != payload.len() {
151                    return Err(Error::bad_request_with_rbep(bytes_consumed)
152                        .with_title("Data after CBOR item"));
153                }
154                start
155            })),
156            _ => return Err(Error::method_not_allowed()),
157        })
158    }
159
160    fn estimate_length(&mut self, _: &<Self as coap_handler::Handler>::RequestData) -> usize {
161        1100
162    }
163    fn build_response<M: MutableWritableMessage>(
164        &mut self,
165        res: &mut M,
166        mode: RequestedScrollbufPosition,
167    ) -> Result<(), Self::BuildResponseError<M>> {
168        res.set_code(M::Code::new(code::CONTENT).map_err(|e| BuildResponseError::Stack(e.into()))?);
169
170        res.add_option_uint(
171            M::OptionNumber::new(option::CONTENT_FORMAT)
172                .map_err(|e| BuildResponseError::Stack(e.into()))?,
173            content_format::from_str("application/cbor-seq").unwrap(),
174        )
175        .map_err(|e| BuildResponseError::Stack(e.into()))?;
176
177        // N + 8 as in data_area. Using an upper bound is useful on stacks that don't provide one
178        // of their own. (Those may be buggy because even CoAP-over-TCP should respect the CSM, but
179        // there's nothing fundamentally limiting the size on the general transport).
180        let len = core::cmp::min(res.available_space() - 1, N + 8);
181        let msg_buf = res
182            .payload_mut_with_len(len)
183            .map_err(|e| BuildResponseError::Stack(e.into()))?;
184
185        assert!(len > 8);
186        msg_buf[0] = 0x1a; // u32; we'll get the cursor later
187        msg_buf[5] = 0x59; // byte string of u16 length; we'll get the length later
188
189        let data_area = &mut msg_buf[8..];
190
191        let (cursor, bytes) = match mode {
192            FromStart => match self.0.read_earliest(data_area) {
193                Ok((cursor, bytes)) => (cursor, bytes),
194                _ => {
195                    // This is always "it's currently locked", as read_earliest can try to access
196                    // lost data.
197                    return Err(BuildResponseError::Own(
198                        Error::service_unavailable()
199                            .with_max_age(0)
200                            .with_title("Buffer busy"),
201                    ));
202                }
203            },
204            StartingAt(n) => match self.0.read_from_cursor(n, data_area) {
205                Ok(bytes) => (n, bytes),
206                Err(scroll_ring::ReadErr::BufferUnavailable) => {
207                    return Err(BuildResponseError::Own(
208                        Error::service_unavailable()
209                            .with_max_age(0)
210                            .with_title("Buffer busy"),
211                    ));
212                }
213                Err(scroll_ring::ReadErr::DataUnavailable) => {
214                    // It's either not available any more or not available yet, and given the ring
215                    // semantics that's kind of the same.
216                    return Err(BuildResponseError::Own(
217                        Error::bad_request().with_title("Data unavailable"),
218                    ));
219                }
220            },
221        };
222
223        let bytes_written = core::cmp::min(bytes, data_area.len());
224        msg_buf[1..5].copy_from_slice(&cursor.0.to_be_bytes());
225        msg_buf[6..8].copy_from_slice(&(bytes_written as u16).to_be_bytes());
226        res.truncate(8 + bytes_written)
227            .map_err(|e| BuildResponseError::Stack(e.into()))?;
228
229        Ok(())
230    }
231}
232
233#[doc(hidden)]
234pub struct BufferHandlerRecord(());
235
236impl<'a, const N: usize> coap_handler::Reporting for BufferHandler<'a, N> {
237    type Record<'b>
238        = BufferHandlerRecord
239    where
240        Self: 'b;
241    type Reporter<'b>
242        = core::iter::Once<BufferHandlerRecord>
243    where
244        Self: 'b;
245    fn report(&self) -> Self::Reporter<'_> {
246        core::iter::once(BufferHandlerRecord(()))
247    }
248}
249
250impl<'a> coap_handler::Record for BufferHandlerRecord {
251    type PathElement = &'static &'static str;
252    type PathElements = core::iter::Empty<&'static &'static str>;
253    type Attributes = core::iter::Once<coap_handler::Attribute>;
254
255    fn path(&self) -> Self::PathElements {
256        core::iter::empty()
257    }
258    fn rel(&self) -> Option<&str> {
259        None
260    }
261    fn attributes(&self) -> Self::Attributes {
262        core::iter::once(coap_handler::Attribute::Interface(
263            "tag:riot-os.org,2021:ser-out",
264        ))
265    }
266}