Writing a client for the obSCEne protocol

docs/PROTOCOL.md is the specification: what the wire carries and what each reply means. This document is what a client author does about it, in the order they meet it.

It is self-contained so it can be copied out and used by a consumer that does not have obSCEne checked out. The contract is this document, docs/PROTOCOL.md, docs/OUTPUT.md, and the captured transcripts in docs/examples/protocol/.

Shape

obSCEne is the server. It runs on the target (the hardware, or an emulator standing in for one), binds a TCP port, and waits. The client connects, issues commands one at a time, and reads back what the target did. The hardware has no shell, no DNS and no config file; it has an address the operator reads off a screen, and the client connects to it.

Connecting

obSCEne listens only in a serving build. The operator builds and runs one (make module GEN=<gen> SERVE=1, with the crash-exclusion list). It serves after running its self-report, so nothing is listening until that finishes.

The wire

Sequence numbers

<seq> is a decimal integer, strictly increasing within a session, starting at 1. The client owns it; the server never supplies it. It matches a reply to its request and records issue order. A repeated or non-increasing sequence is refused as bad-argument without an acknowledgement (docs/PROTOCOL.md, bad sequence rule).

Handshake

Every session opens with hello, before any other command is accepted:

→ CMD|1|hello|1|<secret>
← OBS|ack|1|hello
← OBS|hello|1|<session-id>|<capabilities>
← OBS|part|<session-id>|probe|<build-id>
← OBS|part|<session-id>|binary|<module|host>
← OBS|part|<session-id>|transport|<scenet|posix>
← OBS|done|1|ok||

Close cleanly with CMD|<seq>|bye, answered OBS|done|<seq>|ok||.

Capabilities and verbs

capability verbs
call call
read read
report report
blob blob, run
reset reset
resolve resolve
write write
gpu gpu

hello and bye need no capability. A default serving build announces call,read,report; a build with OBS_NET_ESCAPE adds blob,reset. write is off in every build: a read or a call costs a crash at worst, a write costs a crash and whatever state was being built.

A verb whose capability was not announced is refused not-negotiated when the build knows the verb, or unknown-verb when it does not. A client handles both and never sends an un-announced verb expecting an answer.

call - invoke an address

→ CMD|3|call|<addr>|<arg0>|...|<arg5>          (address and up to six integer args, all hex)
← OBS|ack|3|call
← OBS|done|3|returned|<value-in-hex>|

read - dump guest memory

→ CMD|2|read|<addr>|<len>                    (both hex)
← OBS|bytes|read/0x<addr>|(memory)|contents|<offset>|<hex>
← OBS|bytes|read/0x<addr>|(memory)|contents|<offset>|<hex>          (16 bytes per record)
← OBS|done|2|returned|<len-in-hex>|

report - the suite, streamed

→ CMD|2|report
← OBS|ack|2|report
← OBS|meta|1|26|499               (the full report streams here, between ack and done)
← OBS|sink|obscene-report.txt
← OBS|sysinfo|memory|known|441M   (the sysinfo header - see the next section)
← ...section / try / res / sym / sectiontally records...
← OBS|tally|...
← OBS|end|...
← OBS|done|2|returned|<fail-count-in-hex>|

report runs the compiled-in suite and streams every record down the socket as it runs, between the ack and the done, in the shapes docs/OUTPUT.md defines. The stream is additive: the probe also writes the same records to its stdout and file sink (obscene-report.txt on the target), so a dropped connection loses nothing. The done value is the fail count, a summary after the stream.

sysinfo records

The report stream carries a block of sysinfo records after sink: the status readout obSCEne draws on screen, one record per field. A serving build also emits the block when it starts listening, so it reaches stdout and the file before any client asks for a run.

OBS|sysinfo|listening|known|0.0.0.0:9803
OBS|sysinfo|memory|known|441M
OBS|sysinfo|vram|known|4608M
OBS|sysinfo|ip|unconfirmed|unknown
OBS|sysinfo|firmware|unconfirmed|unknown
OBS|sysinfo|generation|known|both
OBS|sysinfo|temp|absent|unknown
OBS|sysinfo|storage|absent|unknown

Fields are field|state|value. Key on the state, not only the value; all three states can show unknown:

state value means
known the reading (441M) read through a confirmed signature
unconfirmed unknown the query resolves, but obSCEne has no confirmed signature to call it through
absent unknown no such query on this platform

A display that collapses the three loses the distinction between an emulator's missing feature, obSCEne's own unwired query, and a real value.

The state set is open: treat an unrecognised state as unrecognised and degrade. obSCEne may append a value to any report enum (a state, a res status or provenance, a call outcome) without bumping the format version (docs/OUTPUT.md). The protocol grammar is closed - verbs, refusal reasons and capability tokens are fixed lists, and a new one is a specification change.

These are observations, never machine identity (D108). Inside an emulator each answers as the emulator chooses: memory|known|441M is the emulator's number. Display them as the target's self-report and never promote them to graded provenance. generation is 4 (gnm) or 5 (agc) when exactly one graphics driver resolves, both when both do, and unknown when neither does. known|both and absent|unknown are different findings. both is not a claim that the machine is both generations; presence is not implementation. Treat sysinfo|generation as a display hint; the graded target generation is operator-asserted.

Outcomes

A command ends in exactly one terminal reply:

The ack is written and flushed before the command runs, so a death is legible from the client's end:

An ack with no matching done, followed by the connection closing, means the command ended the process. Record it as died, never as returned 0.

The client writes the non-answers; the probe cannot, because it is gone.

outcome client observes means
died ack, then the connection closed with no done the command ended the process
timeout ack, budget elapsed, connection still open not returned yet; not resolved into died
lost ack, connection closed, and it is unclear whether the probe returned ambiguous, recorded as ambiguous

The value field is empty for every non-answer. A 0x0 there makes a call that died indistinguishable from one that returned zero.

Machine origin

obSCEne's records carry no trustworthy machine identity, by design. A probe cannot certify its own machine: inside an emulator, sceKernelGetSystemSwVersion returns the emulator's chosen version, and stamping that as firmware= would present an emulator's answer as a hardware measurement.

The machine identity - target, gpu, firmware, and whether this is real hardware - is asserted by the operator. The client collects it from the person running the session (what device, what firmware, real hardware or which emulator) and stamps it onto the records it keeps. obSCEne's reference driver does this with --part key=value.

Grade hardware -> measured only when the operator asserted the target was real hardware. The same result observed in an emulator is assumed.

The corpus

A session is transient; the corpus is the product. obSCEne's reference driver writes each session as OBSCORPUS| records (docs/OUTPUT.md). A client's own corpus preserves these properties:

Testing without hardware

A session

From obSCEne serving inside shadPS4, with the report stream abbreviated. In a graded run the machine fields are operator-supplied:

→ CMD|1|hello|1|d98130d191ccee2143658132dd1d2499
← OBS|ack|1|hello
← OBS|hello|1|t0x13bd05e|call,read,report
← OBS|part|t0x13bd05e|probe|dev
← OBS|part|t0x13bd05e|binary|module
← OBS|part|t0x13bd05e|transport|scenet
← OBS|done|1|ok||
→ CMD|2|report
← OBS|ack|2|report
← OBS|meta|1|26|499                   (the run streams between ack and done...)
← OBS|sink|obscene-report.txt
← OBS|sysinfo|memory|known|441M       (...the sysinfo header, then the checks...)
← OBS|sysinfo|vram|known|4608M
← OBS|sysinfo|generation|known|both
← ...section / try / res / sym / tally records...
← OBS|end|1
← OBS|done|2|returned|0x28|           (0x28 = 40 checks failed; the detail is above and in the file)
→ CMD|3|bye
← OBS|ack|3|bye
← OBS|done|3|ok||

Client checklist

  1. TCP client, operator-supplied host:port (default 9803), one connection at a time.
  2. Line reader and writer: \n-terminated, 4096-byte cap, CMD| out and OBS| in.
  3. Client-owned sequence numbers, strictly increasing from 1.
  4. hello first, with the secret; parse <session-id> and capabilities; never send an un-announced verb.
  5. A changed session id is a probe restart.
  6. Ack-before-done: a lone ack and a closed socket is died; an elapsed budget is timeout; ambiguity is lost; none carries a value.
  7. An operator form for machine identity, stamped onto every kept record.
  8. The report stream between report's ack and done, parsed incrementally, with a mid-stream death handled as died. Unknown record kinds are ignored.
  9. The sysinfo block keyed on state, shown as self-report, never as graded machine identity.
  10. Replay-from-transcript mode for CI.
  11. call: returned, bad-argument or died. read: bytes records then returned, or died on a bad address. Every other verb only when its capability is announced, with refusal handled.