host.command_frame¶
Pluggable shell command framing for ShellSession.
otto drives a remote shell by wrapping each command in unique sentinels — a
BEGIN marker, the command itself, a way to recover the exit code, and an END
marker — then parsing the echoed byte stream back into (output, retcode).
How that wrapping and parsing is done is the shell’s dialect, and it
differs per target:
a POSIX bash shell bakes
$?into the END marker;the Zephyr RTOS shell has no
$?and no genericecho, so it appends a stockretvalcommand whose output carries the code on its own line, and parses output positionally (the shell prints its prompt after every executed line);a Zephyr 2.7 target has neither —
retvalonly exists from 3.x — so a custom frame is needed (seetodo/command_frame_protocol.md).
This module makes the dialect a first-class, composable strategy: a
CommandFrame is a small stateless value object that a session
holds rather than is. The per-session sentinels (unique per connection so
two sessions can’t cross-talk) live on the session and are passed to the frame
as a SessionMarkers value object — keeping the frame pure and
unit-testable without a live session, exactly like
EmbeddedFileSystem.
Built-in frames¶
BashFrame("bash") — POSIX bash; used by SSH/telnet/local unix sessions.ZephyrFrame("zephyr") — the stock Zephyrfs/retvalshell (3.7 / 4.4 LTS).
A project can register additional dialects via register_command_frame()
from a .otto init module — the same extension hook
otto.host.embedded_filesystem.register_filesystem() follows.
- class otto.host.command_frame.SessionMarkers(begin, end_prefix, ready, recover)¶
Bases:
objectThe unique per-session sentinel tokens a
CommandFramerenders into commands and keys on when parsing.Built once per session from the session id (see
for_session()), so two concurrent sessions never match each other’s markers. A frame receives this and never generates markers itself, which keeps frames stateless.- begin : --is-rst--:py:class:`str`¶
BEGIN sentinel —
__OTTO_<id>_BEGIN__.
- end_prefix : --is-rst--:py:class:`str`¶
END sentinel prefix —
__OTTO_<id>_END__(bash appends<code>__).
- ready : --is-rst--:py:class:`str`¶
Readiness-probe token —
__OTTO_<id>_READY__.
- recover : --is-rst--:py:class:`str`¶
Post-timeout re-sync token —
__OTTO_<id>_RECOVER__.
- classmethod for_session(session_id)¶
Build the marker set for a session id.
- Return type:¶
- class otto.host.command_frame.CommandFrame¶
Bases:
ABCA shell’s command-framing dialect: how to wrap a command for execution and how to parse the echoed stream back into
(output, retcode).Concrete frames are stateless value objects. The render half (
handshake()/frame()/recover()) and the parse half (end_pattern()/marks_begin()/parse_output()/extract_retcode()) live together because they co-vary through where the retcode lives — splitting them would let mismatched halves combine.- type_name : --is-rst--:py:data:`~typing.ClassVar`\ \[:py:class:`str`]¶
Lab-data string for this dialect (e.g.
'bash'). Looked up against_FRAME_CLASSESby the storage factory; unique across frames.
-
streams_output_live : --is-rst--:py:data:`~typing.ClassVar`\ \[:py:class:`bool`] =
False¶ Whether this dialect’s raw inter-marker byte stream is already clean line-by-line and can be streamed to the log as it arrives. Default False: the session buffers to the END sentinel and logs
parse_output(buffer)— so the log shows exactly the command’s parsed output, with no shell prompts or retcode scaffolding. A dialect sets this True only when its raw stream has no scaffolding to strip (e.g. bash with echo off).
- abstract handshake(m)¶
Readiness-probe payload, echoing
SessionMarkers.ready.- Return type:¶
str
- abstract frame(cmd, m)¶
Full write payload that runs
cmdbracketed by the sentinels.- Return type:¶
str
- abstract recover(m)¶
Re-synchronization payload, echoing
SessionMarkers.recover.- Return type:¶
str
- abstract end_pattern(m)¶
Regex marking the end of a command’s output in the stream.
The session compiles this once per session and uses it both to detect completion in the streaming read loop and to bound parsing.
- Return type:¶
Pattern[str]
- abstract marks_begin(data, m)¶
Return True if
datais the chunk carrying the BEGIN sentinel.- Return type:¶
bool
- class otto.host.command_frame.BashFrame¶
Bases:
CommandFramePOSIX bash dialect:
echobrackets and$?baked into the END marker. The default frame for SSH/telnet/local unix sessions.-
type_name : --is-rst--:py:data:`~typing.ClassVar`\ \[:py:class:`str`] =
'bash'¶ Lab-data string for this dialect (e.g.
'bash'). Looked up against_FRAME_CLASSESby the storage factory; unique across frames.
-
streams_output_live : --is-rst--:py:data:`~typing.ClassVar`\ \[:py:class:`bool`] =
True¶ Whether this dialect’s raw inter-marker byte stream is already clean line-by-line and can be streamed to the log as it arrives. Default False: the session buffers to the END sentinel and logs
parse_output(buffer)— so the log shows exactly the command’s parsed output, with no shell prompts or retcode scaffolding. A dialect sets this True only when its raw stream has no scaffolding to strip (e.g. bash with echo off).
- handshake(m)¶
Readiness-probe payload, echoing
SessionMarkers.ready.- Return type:¶
str
- recover(m)¶
Re-synchronization payload, echoing
SessionMarkers.recover.- Return type:¶
str
- end_pattern(m)¶
Regex marking the end of a command’s output in the stream.
The session compiles this once per session and uses it both to detect completion in the streaming read loop and to bound parsing.
- Return type:¶
Pattern[str]
- marks_begin(data, m)¶
Return True if
datais the chunk carrying the BEGIN sentinel.- Return type:¶
bool
-
type_name : --is-rst--:py:data:`~typing.ClassVar`\ \[:py:class:`str`] =
- class otto.host.command_frame.ZephyrFrame¶
Bases:
CommandFrameStock Zephyr RTOS shell dialect (3.7 / 4.4 LTS).
The Zephyr shell is not bash: no command substitution, no
$?, no genericecho. otto frames each command as four CR-separated lines:__OTTO_<id>_BEGIN__ rejected -> shell emits `<token>: command not found` <the real command> real output appears here retval stock Zephyr builtin: prints <cmd>'s exit code __OTTO_<id>_END__ rejected -> shell emits `<token>: command not found`The four-line order is load-bearing:
retvalmust run immediately after<cmd>(any command, even an unknown one, overwrites the shell’s stored return value). otto never depends on the prompt text and never modifies the firmware —retvalis a default-on builtin and the markers are just rejected input.Parsing is positional. The shell prints its prompt after every executed line and does not echo input, so between the BEGIN error line and
retval’s integer line the slice is exactly[prompt, <output...>, prompt]— dropping the bracketing prompt lines yields the output without ever reading the prompt text. ANSI escapes (the colored prompt) are stripped first.-
type_name : --is-rst--:py:data:`~typing.ClassVar`\ \[:py:class:`str`] =
'zephyr'¶ Lab-data string for this dialect (e.g.
'bash'). Looked up against_FRAME_CLASSESby the storage factory; unique across frames.
- handshake(m)¶
Readiness-probe payload, echoing
SessionMarkers.ready.- Return type:¶
str
- recover(m)¶
Re-synchronization payload, echoing
SessionMarkers.recover.- Return type:¶
str
- end_pattern(m)¶
Regex marking the end of a command’s output in the stream.
The session compiles this once per session and uses it both to detect completion in the streaming read loop and to bound parsing.
- Return type:¶
Pattern[str]
- marks_begin(data, m)¶
Return True if
datais the chunk carrying the BEGIN sentinel.- Return type:¶
bool
- extract_retcode(buffer, m)¶
Recover the exit code from
retval’s output.retvalprints a bare signed integer on its own line just before the END marker. Take the last such line in the region preceding END. Returns-1if no integer is found.- Return type:¶
int
- parse_output(buffer, cmd, m)¶
Extract the command’s output from the framed response, positionally.
Between the BEGIN error line and
retval’s integer line the shell emitted[prompt, <command output...>, prompt](a prompt after each of the two executed lines: the rejected BEGIN, then the command). Drop the bracketing prompt lines.- Return type:¶
str
-
type_name : --is-rst--:py:data:`~typing.ClassVar`\ \[:py:class:`str`] =
- class otto.host.command_frame.ZephyrSerialFrame¶
Bases:
ZephyrFrameZephyr 3.7+ dialect for a serial/UART shell reached over a raw byte bridge (e.g. QEMU
-serial telnet:<ip>:<port>,server).Identical framing and parsing to
ZephyrFrame— only the handshake differs. The in-guestSHELL_BACKEND_TELNEThonours otto’sIAC DONT ECHOand stops echoing input, which is why the stockZephyrFramehandshake assumes a non-echoing shell. A UART shell behind a-serial telnet:bridge never sees that IAC (QEMU consumes it), so it keeps echo on; the echoed END marker would then match otto’s read loop before the command’s real output arrives, desyncing every command by one. Disable echo once, up front —shell echo offis a stock builtin. The readiness marker (rejected as an unknown command) still comes back via the shell’s error handler, which is shell output, not input echo, so the probe is unaffected. Mirrorsrepo1's ZephyrInlineRetcodeFramefor 2.7.-
type_name : --is-rst--:py:data:`~typing.ClassVar`\ \[:py:class:`str`] =
'zephyr-serial'¶ Lab-data string for this dialect (e.g.
'bash'). Looked up against_FRAME_CLASSESby the storage factory; unique across frames.
- handshake(m)¶
Readiness-probe payload, echoing
SessionMarkers.ready.- Return type:¶
str
-
type_name : --is-rst--:py:data:`~typing.ClassVar`\ \[:py:class:`str`] =
- otto.host.command_frame.register_command_frame(type_name, cls)¶
Make a custom
CommandFramesubclass available to lab data.Call from an init module listed in
.otto/settings.toml— the same patternotto.host.embedded_filesystem.register_filesystem()follows. Once registered, lab-data entries can reference the subclass by type_name in thecommand_framefield.- Return type:¶
None
Raises¶
- ValueError
If type_name doesn’t match
cls.type_name(the registry key and the class constant should agree).
- otto.host.command_frame.build_command_frame(type_name)¶
Construct the
CommandFrameregistered under type_name.- Return type:¶
Raises¶
- ValueError
If type_name is not registered. The error lists the registered names so a typo is diagnosable from the message alone.