host.embedded_host¶
Embedded (bare-metal / RTOS) host class.
An EmbeddedHost is a network-reached target whose “OS” is a real-time
kernel or bare-metal firmware rather than a POSIX system — Zephyr is the first
concrete example. It is exposed through the same Host
API as UnixHost (run/oneshot/send/
expect/put/get) so test code does not care whether a target is a
Linux box or a microcontroller.
What makes an embedded target different from a Unix host:
One console. A Zephyr device exposes a single shell over telnet. There is no second channel and no stateless exec primitive, so
oneshotshares the one persistent session withrunand is therefore not concurrency-safe (it is onUnixHost).No bash. No
$?, no command substitution, noscp/ftp/nc. Command framing and file transfer cannot reuse the Unix machinery.Telnet only. The shell is reached over telnet (optionally through an SSH hop), never SSH directly.
Command execution requires a command frame: a CommandFrame instance
that frames each command for the target’s RTOS shell over the plain telnet
transport and parses the output/return-code back. There is no default frame
— a bare EmbeddedHost raises ValueError at construction if none
is supplied (fail loud). The frame is provided by:
a registered
OsProfiledata bundle (e.g. acommand_framekey in an[os_profiles.<name>]settings table), ora concrete subclass that re-declares the default, or
an explicit constructor argument.
ZephyrHost is the in-tree concrete class: it subclasses
EmbeddedHost and declares ZephyrFrame
as the default command_frame (along with os_type='zephyr' and
os_name='Zephyr'). Zephyr-specific framing and OS naming live on
ZephyrHost, not on the base class.
File transfer (get/put) is delegated to
EmbeddedFileTransfer, which speaks the
device shell only (the console backend uses Zephyr’s fs commands).
The interactive bridge (_interact) currently raises
NotImplementedError.
- class otto.host.embedded_host.EmbeddedHost(ip: str, element: str, os_type: OsType = 'embedded', os_name: str | None = None, os_version: str | None = None, name: str = None, creds: dict[str, str] = <factory>, user: str | None = None, element_id: int | None = None, board: str | None = None, slot: int | None = None, is_virtual: bool = False, term: str = 'telnet', transfer: str = 'console', valid_terms: list[str] = <factory>, valid_transfers: list[str] = <factory>, filesystem: EmbeddedFileSystem = <factory>, command_frame: CommandFrame | None = None, loader: BinaryLoader | None = None, default_dest_dir: Path = <factory>, max_filename_len: int = 255, telnet_options: TelnetOptions = <factory>, snmp: SnmpOptions | None = None, toolchain: Toolchain = <factory>, hop: str | None = None, resources: set[str] = <factory>, interfaces: dict[str, str] = <factory>, products: list['Product'] = <factory>, power_control: PowerController | None = None, log: bool = True, log_stdout: bool = True, _connection_factory: type[ConnectionManager] | None = None, *, _lab: Lab | None = None)¶
Bases:
RemoteHostOS-agnostic bare-metal / RTOS host reached over telnet.
EmbeddedHostcarries no OS-specific defaults. Acommand_framemust be supplied — either via a profile, a subclass (e.g.ZephyrHost), or an explicit constructor argument — or construction raisesValueError(fail loud).ZephyrHostis the in-tree concrete subclass and worked example.- os_type : OsType¶
Default profile selector for a bare
EmbeddedHost. Subclasses (e.g.ZephyrHost) override this to their registered name.
- os_name : str | None¶
Kernel/OS name, or None. A bare
embeddedhost carries no OS name; a concrete subclass (e.g.ZephyrHost) sets it.
- creds : dict[str, str]¶
Users and their respective passwords. Optional — the Zephyr telnet shell backend has no login step, so this is empty for a stock Zephyr target.
- term : str¶
Active session transport. Embedded hosts speak telnet today; the command frame is transport-independent, so this is not a hard coupling.
- transfer : str¶
File-transfer backend.
console(default) drives the device shell’sfscommands;tftpis reserved and not yet implemented.
- valid_transfers : list[str]¶
Closed menu of transfer backends this host supports (active is
transfer).
- filesystem : EmbeddedFileSystem¶
On-device filesystem variant — e.g.
FatRamFileSystem,LittleFsFileSystem, orNoFileSystem(the default). Carries the mount path, the optionalfs mountcommand, and the command-formation hooks the transfer code and the embedded monitor’s disk parser drive. Seeotto.host.embedded_filesystem.Lab data declares the variant by string in the
filesystemfield; the storage factory resolves the string to a class. Projects can register custom variants viaotto.host.embedded_filesystem.register_filesystem().
- command_frame : CommandFrame | None¶
Shell-framing dialect for this target’s console — how a command is wrapped in sentinels and how output/retcode are parsed back. There is NO default: a bare
embeddedhost carries no dialect, so a frame is required — supplied either by a profile/subclass (e.g.ZephyrHost) or as an explicit value. A frame-lessEmbeddedHostfails loud at construction.Lab data declares the dialect by string in the
command_framefield (e.g. a Zephyr 2.7 build that reports its retcode inline would name a project-registered frame); the storage factory resolves the string to an instance. Projects can register custom dialects viaotto.host.command_frame.register_command_frame(). The dialect is independent of the transport, so it is handed straight to theSessionManager.
- loader : BinaryLoader | None¶
Binary-load strategy for this target’s runtime (e.g. Zephyr LLEXT). Unlike
command_frameit is optional — many embedded hosts never load binaries. Lab data declares it by string in theloaderfield (e.g."llext-hex");__post_init__resolves the string to an instance.load()/unload()fail loud (ValueError) when it is None. Projects register custom loaders viaotto.host.binary_loader.register_binary_loader().
- default_dest_dir : Path¶
Default landing directory for
put/getwhen the caller supplies an empty or relativedest_dir. When left at the default (an emptyPath()),__post_init__resolves it tofilesystem.mountso generic fan-out callers likedo_for_all_hostsdon’t have to branch on host type. Override in lab data to land transfers somewhere other than the FS root. Seedefault_dest_dir.
- max_filename_len : int¶
Upper bound on the basename length (including extension) accepted by the target’s filesystem. Defaults to
255— the LinuxNAME_MAX, also the typical LittleFS ceiling. Override per-host when the firmware enforces a tighter limit (e.g.32for a Zephyr build that setsCONFIG_FS_FATFS_MAX_LFN=32/CONFIG_FS_LITTLEFS_NAME_MAX=32, or12for a stock FAT 8.3 build without LFN support). Seemax_filename_len.
- telnet_options : TelnetOptions¶
Connection options for the telnet shell (port, cols/rows, etc.).
- snmp : SnmpOptions | None¶
Optional SNMP polling config (lab
snmpblock). When set, otto’s monitor collects this host’s metrics over SNMP — a separate channel from the single telnet console — instead of running shell commands. SeeSnmpOptions.
- toolchain : Toolchain¶
Cross-toolchain for this bed’s products. Used by the coverage pipeline to select the correct
gcov/lcov. The host is the test bed, so it owns the toolchain matching its target ABI — a Zephyr 3.7 bed and a 4.4 bed declare different SDKs. Defaults to system-installed tools.
- interfaces : dict[str, str]¶
Named secondary interface addresses (see
interfaces). Resolve withaddress_for().
- power_control : PowerController | None¶
Pluggable power backend. Lab data declares it by string (a config-free controller type) or a
[power]table ({type, on_cmd, off_cmd, ...});__post_init__coerces it to an instance. None → power()/reboot(hard=True) fail loud. Seepower_control.
- async verify_connection() CommandStatus¶
Attempt to open the telnet shell without running commands (dry-run).
-
async oneshot(cmd: str, timeout: float | None =
None, log: bool =True) CommandStatus¶ Run a single command on the embedded host.
Unlike
oneshot(), this is not concurrency-safe: an embedded target exposes a single console with no stateless exec primitive, sooneshotruns on the same persistent session asrun. It exists for API parity; userunfor stateful workflows.
- async open_session(name: str) HostSession¶
Open a named persistent shell session.
Note: an embedded target has a single console. Opening a second named session opens a second telnet connection to the device, which most RTOS shell backends do not accept concurrently. Prefer the default session via
run.
-
async expect(pattern: str | Pattern[str], timeout: float =
10.0) str¶ Wait for a pattern in the host’s session output stream.
- async get(src_files: ~types.Annotated[list[~pathlib.Path] | ~pathlib.Path, ~otto.utils.Arg(variadic=True, elem_type=~pathlib.Path, name=None, help=Remote file(s) to download.)], dest_dir: ~pathlib.Path, show_progress: ~typing.Annotated[bool, <otto.utils._Exclude object at 0x7fcc41e8fea0>] = True) tuple[Status, str]¶
Transfer files from the embedded host to the local machine.
Delegates to
EmbeddedFileTransfer, which speaks the device shell (theconsolebackend uses Zephyr’sfscommands). Transfers are sequential — an embedded target has a single console.
- async put(src_files: ~types.Annotated[list[~pathlib.Path] | ~pathlib.Path, ~otto.utils.Arg(variadic=True, elem_type=~pathlib.Path, name=None, help=Local file(s) to upload.)], dest_dir: ~pathlib.Path, show_progress: ~typing.Annotated[bool, <otto.utils._Exclude object at 0x7fcc41e8fea0>] = True) tuple[Status, str]¶
Transfer files from the local machine to the embedded host.
Delegates to
EmbeddedFileTransfer(theconsolebackend writes via Zephyr’s chunkedfs write). Transfers are sequential — an embedded target has a single console.dest_diris resolved againstdefault_dest_dirso a genericPath()from a fan-out caller lands on the host’s mounted filesystem (e.g./RAM:on a FAT target) rather than on Zephyr’s bare/, which has no FS and rejects opens with-ENOENT.
-
async ls(path: Annotated[str | Path, Arg(variadic=False, elem_type=None, name=None, help=None)] =
'.', all: bool =False) list[str]¶ List entry names in path via the device
fs lsformer.
-
async rm(path: str | Path, recursive: bool =
False, force: bool =False) tuple[Status, str]¶ Remove path via the device
fs rmformer (flags ignored).
-
async load(file: Path, name: str, show_progress: bool =
False, timeout: float | None =120.0) tuple[Status, str]¶ Load a binary into the device runtime via the host’s binary loader.
Distinct from
put()(a file transfer to a mounted filesystem):loadpushes a binary into the target’s loader (e.g. Zephyr LLEXT’sllext load_hex), with no destination file. The payload is read from file, formatted into the device command by the loader, and sent withlog=Falseso the (large) encoded payload never reaches the console or log. Returns(Status, str)likeput()/get(); thestrcarries the device’s failure text on error.show_progressis off by default (the bar only renders in interactive /otto run; underotto testoutput is captured). When enabled it drives a transfer-style Rich bar from the paced telnet write of the payload — the only measurable progress (the device’s relocation emits no incremental signal). Fails loud (ValueError) if the host declares no loader.
-
async unload(name: str, timeout: float | None =
20.0) tuple[Status, str]¶ Unload name from the device runtime, draining to full eviction.
Some loaders (LLEXT) refcount a resident binary, so one unload may only decrement it.
unloadloops the loader’s unload command untilis_fully_unloaded()reports the binary gone (bounded byloader.max_unload_rounds). Idempotent: unloading something not loaded succeeds on the first round. Returns(Status, str); fails loud (ValueError) if no loader is declared.
- class otto.host.embedded_host.ZephyrHost(ip: str, element: str, os_type: OsType = 'zephyr', os_name: str | None = 'Zephyr', os_version: str | None = None, name: str = None, creds: dict[str, str] = <factory>, user: str | None = None, element_id: int | None = None, board: str | None = None, slot: int | None = None, is_virtual: bool = False, term: str = 'telnet', transfer: str = 'console', valid_terms: list[str] = <factory>, valid_transfers: list[str] = <factory>, filesystem: EmbeddedFileSystem = <factory>, command_frame: CommandFrame = <factory>, loader: BinaryLoader | None = None, default_dest_dir: Path = <factory>, max_filename_len: int = 255, telnet_options: TelnetOptions = <factory>, snmp: SnmpOptions | None = None, toolchain: Toolchain = <factory>, hop: str | None = None, resources: set[str] = <factory>, interfaces: dict[str, str] = <factory>, products: list['Product'] = <factory>, power_control: PowerController | None = None, log: bool = True, log_stdout: bool = True, _connection_factory: type[ConnectionManager] | None = None, *, _lab: Lab | None = None)¶
Bases:
EmbeddedHostA Zephyr RTOS host — the concrete, registered embedded host.
This is the worked example for shipping a host subclass: it re-declares the Zephyr-specific field defaults that
EmbeddedHostno longer assumes, and is registered underos_type: "zephyr"viaotto.host.os_profile.register_host_class(). External repositories register their ownEmbeddedHost/UnixHostsubclasses the same way (from an init module listed in.otto/settings.toml), and may layer per-buildOsProfiledata bundles over them.- creds : dict[str, str]¶
Users and their respective passwords. Optional — the Zephyr telnet shell backend has no login step, so this is empty for a stock Zephyr target.
- term : str¶
Active session transport. Embedded hosts speak telnet today; the command frame is transport-independent, so this is not a hard coupling.
- transfer : str¶
File-transfer backend.
console(default) drives the device shell’sfscommands;tftpis reserved and not yet implemented.
- valid_transfers : list[str]¶
Closed menu of transfer backends this host supports (active is
transfer).
- filesystem : EmbeddedFileSystem¶
On-device filesystem variant — e.g.
FatRamFileSystem,LittleFsFileSystem, orNoFileSystem(the default). Carries the mount path, the optionalfs mountcommand, and the command-formation hooks the transfer code and the embedded monitor’s disk parser drive. Seeotto.host.embedded_filesystem.Lab data declares the variant by string in the
filesystemfield; the storage factory resolves the string to a class. Projects can register custom variants viaotto.host.embedded_filesystem.register_filesystem().
- loader : BinaryLoader | None¶
Binary-load strategy for this target’s runtime (e.g. Zephyr LLEXT). Unlike
command_frameit is optional — many embedded hosts never load binaries. Lab data declares it by string in theloaderfield (e.g."llext-hex");__post_init__resolves the string to an instance.load()/unload()fail loud (ValueError) when it is None. Projects register custom loaders viaotto.host.binary_loader.register_binary_loader().
- default_dest_dir : Path¶
Default landing directory for
put/getwhen the caller supplies an empty or relativedest_dir. When left at the default (an emptyPath()),__post_init__resolves it tofilesystem.mountso generic fan-out callers likedo_for_all_hostsdon’t have to branch on host type. Override in lab data to land transfers somewhere other than the FS root. Seedefault_dest_dir.
- max_filename_len : int¶
Upper bound on the basename length (including extension) accepted by the target’s filesystem. Defaults to
255— the LinuxNAME_MAX, also the typical LittleFS ceiling. Override per-host when the firmware enforces a tighter limit (e.g.32for a Zephyr build that setsCONFIG_FS_FATFS_MAX_LFN=32/CONFIG_FS_LITTLEFS_NAME_MAX=32, or12for a stock FAT 8.3 build without LFN support). Seemax_filename_len.
- telnet_options : TelnetOptions¶
Connection options for the telnet shell (port, cols/rows, etc.).
- snmp : SnmpOptions | None¶
Optional SNMP polling config (lab
snmpblock). When set, otto’s monitor collects this host’s metrics over SNMP — a separate channel from the single telnet console — instead of running shell commands. SeeSnmpOptions.
- toolchain : Toolchain¶
Cross-toolchain for this bed’s products. Used by the coverage pipeline to select the correct
gcov/lcov. The host is the test bed, so it owns the toolchain matching its target ABI — a Zephyr 3.7 bed and a 4.4 bed declare different SDKs. Defaults to system-installed tools.
- interfaces : dict[str, str]¶
Named secondary interface addresses (see
interfaces). Resolve withaddress_for().
- power_control : PowerController | None¶
Pluggable power backend. Lab data declares it by string (a config-free controller type) or a
[power]table ({type, on_cmd, off_cmd, ...});__post_init__coerces it to an instance. None → power()/reboot(hard=True) fail loud. Seepower_control.
- command_frame : CommandFrame¶
Stock Zephyr
retvalshell framing (3.7 / 4.4 LTS).