Thinking of a general UARTish-over-CoAP protocol, it could look like this – provided we do need live interactivity and not line-buffered operation (it can do the latter but is overkill):
- Separate resources for send and receive. They’re somewhat linked, but could conceptually operate standalone and the same way (but it might be that the RIOT side only implements server mode for both).
- Receiving output from a server happens by GETting or observing the output resource. The output is typically stored in a ring buffer of chunks, and the specified behavior is to send at least one chunk on a GET (but more might be sent at the sender’s discretion). Payload is a CBOR sequence of information chunks with [index, bytecontent] data.
- Indices are into the infinite stream and not the ring buffer (whose size need not be agreed on), and should probably wrap in a defined way similar to observe numbers.
- If the receiver notices a gap, and will just know (and can display) that output was lost.
- Sending to an input of a server happens by POSTing in the same format. Messages don’t need to be confirmable; if there’s loss the server will see that things start beyond the known data and go 4.xx. The client can NON No-Response messages while the user is typing fast, and even send older content along with the latest one like mosh does.
- As echoing is expected, the client can No-Response quite often; if send and receive side are somehow linked (static configuration on the constrained device, discovered by the “terminal client”), it’ll know which latest seen send event is co-acked by the incoming data in a CBOR tag (or just some record that’s not content).
This has some extensibility not needed for a first version:
- A receiver that sees a gap in the observed values (which is perfectly legal due to eventual consistency) can FETCH the missing section – of course that can fail if the server has already forgotten.
- Tagged CBOR data can be used to do what’s done in escape sequences in telnet, like setting baud rate, sending breaks or receiving framing errors.
- dynlink can be used to make the device POST the output somewhere instead.
- Either party can nagle as they like.
- Timing information can be added through tagged CBOR data, in case the receiver needs to know even over a slowly polled connection.
- The main mode of operation here is best-effort delivery of output, and a buffer overrun will never slow down the application (just lose content). A different mode (blocking stdout – is that even a thing in RIOT? A UART might do XOFF or hardware flow control…) can be implemented by configuration, which then needs the POSTed clear-buffer-up-to acks that are already used in the other direction.
Concrete flow example:
> GET /.well-known/core
< </stdio/out>;if=tag:riot.org,2021:ser-out;rt=stdout,
< </stdio/in>;if=tag:riot.org,2021:ser-in;rt=stdin,
< </stdio/in>;rel=pair;anchor="/stdio/out"
> GET (Observe:0) /stdio/out
< [[0, b"This is RIOT version blah\n> "]]
(later notifications indicatd by <~~~~ signs)
> POST (NON; No-Response:2) /stdio/in [[0, b"he"]]
(message lost) <~~~~ [[21, b"blah\n> he"], PairAck(2)]
> POST (NON, No-Response:2) /stdio/in [[0, b"helx\n"]]
<~~~~ [[21, b"blah\n> helx\nCommand not found\n"], PairAck(5)]
> POST (NON, No-Response:2) /stdio/in [[5, b"help\n"]] # user typing really fast now ;-)
<~~~~ [[51, b"Table of contents:\n"...], PairAck(10)]
<~~~~ [[105, b"nformation.\nztimer Introspect what ztimer is doing\n> "], PairAck(10)]
This is the minimal (first items) version. Note that all pairs so far chose to send contiguous chunks instead of multiple ones. On the leg from the console to the constrained device, this may make good practice to avoid the need for the constrained device to seek through the message multiple times; on the other leg that’d allow some optimizations, like sending the current content now and later content if there’s still room in the message, or to store data as memory slices when large litanies like the help content are printed. (That’ll only work with some abstractions in the stdio writing systems, but that can be arranged as things progress). Also, we haven’t lost critical content (only the first part of the interactive session, which was easily corrected by the client sending as much backlog as practical); if one of the observation results had gotten lost of swallowed by a proxy, only a FETCH would have recovered them if still in the backlog.
The link between in and out is for the PairAcks to gain their semantics.
Random selection of later events:
# Hey we're doing DMX without hardware driver (well, almost)
> POST (NON, No-Response:2) /stdio/in [Baud(250000), Break, PauseMs(20), h"ff0000808080"]
# Buffer recovery
> POST (NON, No-Response:2) /stdio/in [[5, b"help\n"]] # user typing really fast now ;-)
(message lost) <~~~~ [[51, b"Table of contents:\n"...], PairAck(10)]
<~~~~ [[105, b"nformation.\nztimer Introspect what ztimer is doing\n> "], PairAck(10)]
> FETCH /stdio/in [51, 105]
# If that didn't fit in a single message, it might trigger block-wise,
# although the server could just as well send what fits and maybe
# the client fetches more later.
< [[51, b"Table of contents:\n"...]]
# Blockling mode
> POST (NON, No-Response:2) /stdio/in [[124, b"help\n"], PairAck(325)]
# Server nagles until ring buffer is full, then sends it as two
# halves because its CBOR serializer doesn't like scatter-gather stuff
<~~~~ [[325, b"Table of contents:\n"...], [475, b"\nifconfig alias for ip\n"....], PairAck(129)]
# Server blocks any printf calls until this comes in
> POST (NON, No-Response:2) /stdio/in [PairAck(425]]
<~~~~ [[425, b"nformation.\nztimer Introspect what ztimer is doing\n> "]]
# I'm just the keyboard, please deliver the output somewhere else
# (that's the most remote use case)
> POST /dynlink <coap://[fe80::42]/lpr>;rel=bind;anchor="/stdio/out"
# Guillemets indicating traffic between RIOT device and line printer
» POST /lpr [[0, b"> "]]
« ACK
> POST /stdio/in [[0, b"help\n"]]
< ACK
» POST /lpr [[2, b"help\nTable of contents..."]]
« ACK