Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

FieldUsed inPurpose
tcl_scriptHardware driver modeDefines how the hardware responds to an incoming call
software_driver_scriptSoftware driver modeBuilds 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:

  1. Spec lookup — the interaction is found by ID or alias from the loaded VHB
  2. 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
    
  3. Script execution — the spec’s software_driver_script is appended and run
  4. 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

TypeWhat happens
ioctlOpens resource_path, calls ioctl with the TCL-built buffer in-place, reads response, closes
writeOpens resource_path (or uses fd override), writes the payload, closes if auto-opened
readOpens resource_path (or uses fd override), reads into a buffer sized by max_data_length (default 4096 bytes)
sendSends the payload on the provided fd
recvReceives data from the provided fd into a buffer
sendtoSends a UDP datagram — requires fd, host, and port overrides
recvfromReceives a UDP datagram — requires fd override

TCL-only — no OS dispatch, user manages fds

TypeNotes
openScript sets up state, returns a value
closeScript handles teardown
socketScript creates the socket and returns the fd
connectScript handles connection setup
bindScript handles binding
shutdownScript 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.

KeyTypeUsed by
fdi32 (string)write, read, send, recv, sendto, recvfrom — uses existing fd instead of auto-opening
hostdotted-decimal IPv4sendto — destination address
portu16 (string)sendto — destination port
flagsi32 (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]}