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
82                .render(message)
83                .map_err(|_| coap_message_utils::Error::internal_server_error())?,
84            BuildResponseError::Own(e) => e
85                .render(message)
86                .map_err(|_| coap_message_utils::Error::internal_server_error())?,
87        }
88        Ok(())
89    }
90}
91
92use RequestedScrollbufPosition::*;
93
94impl<'a, const N: usize> coap_handler::Handler for BufferHandler<'a, N> {
95    type RequestData = RequestedScrollbufPosition;
96
97    type ExtractRequestError = Error;
98    type BuildResponseError<M: MinimalWritableMessage> = BuildResponseError<M::UnionError>;
99
100    fn extract_request_data<M: ReadableMessage>(
101        &mut self,
102        req: &M,
103    ) -> Result<RequestedScrollbufPosition, Error> {
104        use coap_message::MessageOption;
105        use coap_message_utils::OptionsExt;
106
107        req.options()
108            .filter(|o| {
109                !(o.number() == option::ACCEPT
110                    && o.value_uint::<u16>()
111                        == Some(content_format::from_str("application/cbor-seq").unwrap()))
112            })
113            // Not processing Content-Format: it's elective anyway
114            .ignore_elective_others()?;
115
116        Ok(match req.code().into() {
117            code::GET => FromStart,
118            code::FETCH => StartingAt(Wrapping({
119                let payload = req.payload();
120                // Quick and dirty parsing of a single CBOR uint up to 32bit
121                let (start, bytes_consumed) = match payload.get(0) {
122                    Some(i @ 0..=23) => (Some((*i).into()), 1),
123                    Some(24) => (payload.get(1).map(|&n| n.into()), 2),
124                    Some(25) => (
125                        payload
126                            .get(1..3)
127                            .map(|b| u16::from_be_bytes(b.try_into().unwrap()))
128                            .map(u32::from),
129                        3,
130                    ),
131                    Some(26) => (
132                        payload
133                            .get(1..5)
134                            .map(|b| u32::from_be_bytes(b.try_into().unwrap())),
135                        5,
136                    ),
137                    Some(_) => return Err(Error::bad_request_with_rbep(0)),
138                    None => return Err(Error::bad_request()),
139                };
140                let Some(start) = start else {
141                    return Err(Error::bad_request_with_rbep(payload.len()));
142                };
143                if bytes_consumed != payload.len() {
144                    return Err(Error::bad_request_with_rbep(bytes_consumed));
145                }
146                start
147            })),
148            _ => return Err(Error::method_not_allowed()),
149        })
150    }
151
152    fn estimate_length(&mut self, _: &<Self as coap_handler::Handler>::RequestData) -> usize {
153        1100
154    }
155    fn build_response<M: MutableWritableMessage>(
156        &mut self,
157        res: &mut M,
158        mode: RequestedScrollbufPosition,
159    ) -> Result<(), Self::BuildResponseError<M>> {
160        res.set_code(M::Code::new(code::CONTENT).map_err(|e| BuildResponseError::Stack(e.into()))?);
161
162        res.add_option_uint(
163            M::OptionNumber::new(option::CONTENT_FORMAT)
164                .map_err(|e| BuildResponseError::Stack(e.into()))?,
165            content_format::from_str("application/cbor-seq").unwrap(),
166        )
167        .map_err(|e| BuildResponseError::Stack(e.into()))?;
168
169        let len = res.available_space() - 1;
170        let msg_buf = res
171            .payload_mut_with_len(len)
172            .map_err(|e| BuildResponseError::Stack(e.into()))?;
173
174        assert!(len > 8);
175        msg_buf[0] = 0x1a; // u32; we'll get the cursor later
176        msg_buf[5] = 0x59; // byte string of u16 length; we'll get the length later
177
178        let data_area = &mut msg_buf[8..];
179
180        let (cursor, bytes) = match mode {
181            FromStart => match self.0.read_earliest(data_area) {
182                Ok((cursor, bytes)) => (cursor, bytes),
183                _ => {
184                    // This is always "it's currently locked", as read_earliest can try to access
185                    // lost data.
186                    return Err(BuildResponseError::Own(
187                        Error::service_unavailable().with_max_age(0),
188                    ));
189                }
190            },
191            StartingAt(n) => match self.0.read_from_cursor(n, data_area) {
192                Ok(bytes) => (n, bytes),
193                Err(scroll_ring::ReadErr::BufferUnavailable) => {
194                    return Err(BuildResponseError::Own(
195                        Error::service_unavailable().with_max_age(0),
196                    ));
197                }
198                Err(scroll_ring::ReadErr::DataUnavailable) => {
199                    // It's either not available any more or not available yet, and given the ring
200                    // semantics that's kind of the same.
201                    return Err(BuildResponseError::Own(Error::bad_request()));
202                }
203            },
204        };
205
206        let bytes_written = core::cmp::min(bytes, data_area.len());
207        msg_buf[1..5].copy_from_slice(&cursor.0.to_be_bytes());
208        msg_buf[6..8].copy_from_slice(&(bytes_written as u16).to_be_bytes());
209        res.truncate(8 + bytes_written)
210            .map_err(|e| BuildResponseError::Stack(e.into()))?;
211
212        Ok(())
213    }
214}
215
216#[doc(hidden)]
217pub struct BufferHandlerRecord(());
218
219impl<'a, const N: usize> coap_handler::Reporting for BufferHandler<'a, N> {
220    type Record<'b> = BufferHandlerRecord where Self: 'b;
221    type Reporter<'b> = core::iter::Once<BufferHandlerRecord> where Self: 'b;
222    fn report(&self) -> Self::Reporter<'_> {
223        core::iter::once(BufferHandlerRecord(()))
224    }
225}
226
227impl<'a> coap_handler::Record for BufferHandlerRecord {
228    type PathElement = &'static &'static str;
229    type PathElements = core::iter::Empty<&'static &'static str>;
230    type Attributes = core::iter::Once<coap_handler::Attribute>;
231
232    fn path(&self) -> Self::PathElements {
233        core::iter::empty()
234    }
235    fn rel(&self) -> Option<&str> {
236        None
237    }
238    fn attributes(&self) -> Self::Attributes {
239        core::iter::once(coap_handler::Attribute::Interface(
240            "tag:riot-os.org,2021:ser-out",
241        ))
242    }
243}