Software Driver Mode
What Is It?
EMU normally operates as a hardware simulator: it attaches to a running process, intercepts its system calls, and returns scripted responses — effectively impersonating the hardware layer.
Software driver mode inverts this. The engine now acts as the software layer and proactively issues real system calls down to actual hardware. You script what your software would send, and EMU dispatches it as a genuine OS call, returning whatever the hardware responds with.
This is useful when you want to:
- Drive real hardware from a controlled, reproducible script without a full software stack running
- Test firmware or kernel drivers by acting as their software counterpart
- Prototype or replay a driver’s communication sequence interactively
Enabling Software Driver Mode
In the Desktop UI
Open the Driver Mode toggle in the toolbar. When enabled, the TCL editor label switches from Hardware Behavior Script to Software Driver Script so it’s always clear which script you’re editing.
In the CLI Shell
Pass --software-driver to the shell subcommand:
certo emu shell --software-driver --vhb ./behaviors/my_device.vhb
Programmatically
#![allow(unused)]
fn main() {
session.set_software_driver_mode(true);
}
The Two Script Fields
Each interaction in a VHB has two separate TCL fields:
| Field | Used in | Purpose |
|---|---|---|
tcl_script | Hardware driver mode | Defines how the hardware responds to an incoming call |
software_driver_script | Software driver mode | Builds the payload the software sends to hardware |
They are kept separate because the meaning of “inbound” and “outbound” reverses completely between modes. A single shared field would be ambiguous.
In the UI, the Driver Mode tab shows the appropriate field with an amber accent when in software driver mode so you always know which script you’re editing.
How Execution Works
When you run an interaction in software driver mode:
- Spec lookup — the interaction is found by ID or alias from the loaded VHB
- TCL preamble — the engine builds and prepends:
namespace eval certo {} set certo::direction "Inbound" ;# data back from hardware is inbound set <override_key> <value> ;# one line per caller override - Script execution — the spec’s
software_driver_scriptis appended and run - OS dispatch — the script’s return value becomes the payload for the system call
certo::direction is always preset to "Inbound" because from the engine-as-software perspective, data that comes back from hardware is inbound.
Call Types and What They Do
Not all call types behave the same way.
Auto-dispatched — engine opens, calls, and closes
| Type | What happens |
|---|---|
ioctl | Opens resource_path, calls ioctl with the TCL-built buffer in-place, reads response, closes |
write | Opens resource_path (or uses fd override), writes the payload, closes if auto-opened |
read | Opens resource_path (or uses fd override), reads into a buffer sized by max_data_length (default 4096 bytes) |
send | Sends the payload on the provided fd |
recv | Receives data from the provided fd into a buffer |
sendto | Sends a UDP datagram — requires fd, host, and port overrides |
recvfrom | Receives a UDP datagram — requires fd override |
TCL-only — no OS dispatch, user manages fds
| Type | Notes |
|---|---|
open | Script sets up state, returns a value |
close | Script handles teardown |
socket | Script creates the socket and returns the fd |
connect | Script handles connection setup |
bind | Script handles binding |
shutdown | Script handles shutdown |
These types are TCL-only because they manage file descriptor lifetime. If the engine auto-closed the socket inside a socket interaction, the fd would be gone before your subsequent send calls could use it. The TCL-only pattern keeps you in control of when fds are created and destroyed.
The fd Override Pattern
For multi-step socket interactions, the workflow is:
1. select socket-open 2. set fd <result from step 1> 3. select send-data
send select send-data send
→ fields["result"] = "7" set host 192.168.1.10
set port 9000
The socket interaction’s TCL script creates the socket and returns the fd number. You capture it from fields["result"] and pass it as the fd override to the data-transfer interactions that follow.
Override Keys
Overrides serve two purposes: they are injected into TCL as variables (so your script can read them), and certain keys are additionally consumed by the dispatch logic.
| Key | Type | Used by |
|---|---|---|
fd | i32 (string) | write, read, send, recv, sendto, recvfrom — uses existing fd instead of auto-opening |
host | dotted-decimal IPv4 | sendto — destination address |
port | u16 (string) | sendto — destination port |
flags | i32 (string) | send, recv, sendto, recvfrom — OS-level flags |
All other keys you set are available in your TCL script as $varname but have no effect on the dispatch mechanism.
Example: Reading a Sensor via ioctl
In the shell:
emu> select sensor-read
Selected: sensor-read
emu(sensor-read)> defaults
# Script output:
binary format H* "0102030405060708"
emu(sensor-read)> send
{"fields":{},"raw_bytes":[12,0,128,0,0,0,0,0]}
The engine opened /dev/my_sensor, called ioctl with the 8-byte request buffer your script produced, read the hardware’s in-place response, and returned it as raw_bytes.
Example: UDP Round-Trip
emu> select udp-socket
emu(udp-socket)> send
{"fields":{"result":"7"},"raw_bytes":[]}
↑ fd = 7 returned by socket script
emu> select udp-send
emu(udp-send)> set fd 7
emu(udp-send)> set host 192.168.1.10
emu(udp-send)> set port 9000
emu(udp-send)> send
{"fields":{},"raw_bytes":[80,73,78,71]}
emu> select udp-recv
emu(udp-recv)> set fd 7
emu(udp-recv)> send
{"fields":{},"raw_bytes":[80,79,78,71]}