Skip to content
Merged
27 changes: 14 additions & 13 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ Runtime deadline semantics are intentionally separate from protocol/device timin

Still intentionally incomplete:

- [ ] richer device/runtime error taxonomy for vendor-specific failures without leaking vendor SDK types into Core;
- [ ] persistent evidence/artifact storage beyond the current bounded in-memory Runtime stores;
- [ ] remote/team leases — local mutation locks are **not** a substitute for authenticated remote ownership.
- [x] richer device/runtime error taxonomy for vendor-specific failures without leaking vendor SDK types into Core;
- [x] persistent evidence/artifact storage beyond the current bounded in-memory Runtime stores;
- [x] remote/team leases — target leases with TTL and token-hash ownership, optional per profile (`safety.leasesRequired`), plus a persistent audit trail.

## Real bench vertical slice — in progress

Expand Down Expand Up @@ -149,8 +149,8 @@ The Runtime makes common bench failures explainable to an Agent without dumping
- [x] strict size/count limits so evidence remains LLM-context friendly;
- [x] correlate recent power-on/off and current measurement/assertion context with flash/reset and boot-wait failures;
- [x] per-target context ring is bounded, newest-first, age-limited, and performs no extra hardware I/O;
- [ ] define artifact references for larger evidence that must stay out of LLM context;
- [ ] persist selected evidence/artifacts across Runtime restarts when team/CI workflows require it.
- [x] define artifact references for larger evidence that must stay out of LLM context;
- [x] persist selected evidence/artifacts across Runtime restarts when team/CI workflows require it.

Real-bench exit criterion:

Expand Down Expand Up @@ -185,10 +185,10 @@ Protocol/semantic layer:
- [x] UDS client with P2/P2*, NRC taxonomy and pending handling (ISO 14229);
- [x] UDS flash workflow engine (session, security access, erase, download,
verify, reset) with per-step audit and declarative plans;
- [ ] CAN / CAN FD transmit and capture;
- [ ] bounded capture artifacts;
- [ ] DBC decoding;
- [ ] `wait_signal` / `assert_signal` / `measure_signal` observations.
- [x] CAN transmit and capture (bounded newest-first ring per session, JSONL artifact rows);
- [x] DBC decoding (BO_/SG_ subset: Intel + Motorola layouts, factors, offsets, signedness) and signal encode;
- [x] signal observation surface: `can frames` / `can decode` over a capture;
- [ ] CAN FD (FDF) frame variants.

Exit criterion: Agent validates ECU behavior from decoded signals without consuming an unbounded CAN log.

Expand All @@ -203,9 +203,9 @@ UDS core:
- [x] DID read/write and RoutineControl primitives;
- [x] Security Access with pluggable named key derivers;
- [x] ISO-TP and DoIP transports behind one client;
- [ ] DTC primitives;
- [ ] Security Provider abstraction for vendor seed-key algorithms beyond the
registered derivers.
- [x] DTC primitives (0x19/0x14) with simulated-ECU coverage and CLI surface;
- [x] Security Provider abstraction for vendor seed-key algorithms beyond the
registered derivers (`command:<id>` external providers).

Flash Engine:

Expand All @@ -214,7 +214,8 @@ Flash Engine:
- [x] erase / RequestDownload / TransferData / TransferExit / verify / reset;
- [x] block-level retries;
- [x] audit trace and machine-readable result;
- [ ] BIN / Intel HEX / S-record image model;
- [x] BIN / Intel HEX / S-record image model (HEX/S-record become one
segment per contiguous region automatically);
- [ ] preflight target fingerprint;
- [ ] voltage/current monitoring during programming;
- [ ] explicit recovery strategies;
Expand Down
168 changes: 168 additions & 0 deletions racket/benchpilot/client/cli.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
;; stable exit-code contract, one-retry autostart and the doctor check.

(require json
net/base64
racket/format
racket/list
racket/port
Expand Down Expand Up @@ -556,6 +557,11 @@
(define (require-positional index label)
(or (args-positional args index) (raise-validation (format "Missing required ~a." label))))

(define (hex-string->bytes hex)
(list->bytes (hex-parse hex)))
(define (bytes->hex-string bs)
(bytes->hex (bytes->list bs)))

(define (maybe-did text)
(define n (parse-hex-or-dec text))
(unless (and n (>= n 0) (<= n #xFFFF))
Expand Down Expand Up @@ -791,6 +797,48 @@
(args-get args 'confirm-target)))
compact)
0]
[(dtc)
(define action
(string->symbol (string-downcase
(or (args-positional args 2) ""))))
(case action
[(read)
(define mask (args-hex-opt args 'mask))
(define request (uds-read-dtcs (or mask #xFF)))
(define result
(call "POST" "/uds/request"
(target-query)
(hasheq 'requestHex (bytes->hex-string (list->bytes request)))))
(unless (hash-ref result 'positive #f)
(print-result result compact)
1)
;; responseHex already excludes the SID.
(define payload (hex-string->bytes (hash-ref result 'responseHex "")))
(define parsed (parse-dtc-response (bytes->list payload)))
(define out
(if (eq? parsed 'unsupported)
(hasheq 'ok #t 'positive #f 'error "ECU does not support DTC read (NRC or odd response).")
(hasheq 'ok #t
'positive #t
'availableMask (format "0x~a" (~r (hash-ref parsed 'availableMask) #:base 16 #:min-width 2 #:pad-string "0"))
'dtcs (hash-ref parsed 'dtcs '()))))
(print-result out compact)
0]
[(clear)
(define group (args-hex-opt args 'group))
(define request (uds-clear-dtcs (or group #xFFFFFF)))
(define result
(call "POST" "/uds/request"
(target-query)
(hasheq 'requestHex (bytes->hex-string (list->bytes request)))))
(if (not (hash-ref result 'positive #f))
(begin (print-result result compact) 1)
(let ()
;; ClearDiagnosticInformation answers with the bare 0x54
;; positive SID; responseHex is empty by design.
(print-result (hasheq 'ok #t 'positive #t 'cleared #t) compact)
0))]
[else (raise-validation (format "Unknown command: uds dtc ~a" action))])]
[else (raise-validation (format "Unknown command: uds ~a" subcommand))])]
[(doip)
(unless (eq? subcommand (quote discover))
Expand All @@ -799,6 +847,114 @@
(call "POST" "/doip/discover" (hasheq) (hasheq 'windowMs (args-int-opt args 'window-ms)))
compact)
(if (> (length (hash-ref (unbox last-printed-box) 'vehicles '())) 0) 0 1)]
[(can)
(case subcommand
[(send)
(print-result
(call "POST" "/can/send" (target-query)
(hasheq 'resource (args-get args 'resource)
'frameId (or (args-hex-opt args 'id)
(raise-validation "Missing --id (frame id, e.g. 0x123)."))
'extended (if (args-flag args 'extended) #t 'null)
'dataHex (require-positional 2 "data hex")))
compact)
0]
[(capture)
(case (string->symbol (string-downcase (or (args-positional args 2) "")))
[(start)
(print-result
(call "POST" "/can/capture/start"
(target-query)
(hasheq 'resource (args-get args 'resource)
'capacity (args-int-opt args 'capacity)))
compact)
0]
[(stop)
(print-result
(call "POST" "/can/capture/stop" (hasheq)
(hasheq 'captureId (require-positional 3 "capture id")))
compact)
0]
[else (raise-validation (format "Unknown command: can capture ~a" (args-positional args 2)))])]
[(frames)
(print-result
(call "GET" "/can/frames"
(hasheq 'captureId (require-positional 2 "capture id")
'limit (or (args-int-opt args 'limit) 256)))
compact)
0]
[(decode)
(print-result
(call "POST" "/can/decode" (hasheq)
(hasheq 'captureId (require-positional 2 "capture id")
'dbcPath (args-get args 'dbc)
'frameId (or (args-hex-opt args 'id)
(raise-validation "Missing --id (frame id)."))
'limit (or (args-int-opt args 'limit) 64)))
compact)
0]
[else (raise-validation (format "Unknown command: can ~a" subcommand))])]
[(lease)
(case subcommand
[(acquire)
(print-result
(call "POST" "/lease/acquire" (hasheq)
(hasheq 'target (require-positional 2 "target id")
'ttlSeconds (or (args-int-opt args 'ttl) 300)))
compact)
0]
[(renew)
(print-result
(call "POST" "/lease/renew" (hasheq)
(hasheq 'leaseId (require-positional 2 "lease id")
'ttlSeconds (or (args-int-opt args 'ttl) 300)))
compact)
0]
[(release)
(print-result
(call "POST" "/lease/release" (hasheq)
(hasheq 'leaseId (require-positional 2 "lease id")))
compact)
0]
[(list)
(print-result (call "GET" "/lease/list" (hasheq)) compact)
0]
[else (raise-validation (format "Unknown command: lease ~a" subcommand))])]
[(store)
(case subcommand
[(evidence)
(print-result (call "GET" "/store/operations"
(hasheq 'limit (or (args-int-opt args 'limit) 50)))
compact)
0]
[(observations)
(print-result (call "GET" "/store/observations"
(hasheq 'limit (or (args-int-opt args 'limit) 50)))
compact)
0]
[(artifacts)
(print-result (call "GET" "/store/artifacts" (hasheq)) compact)
0]
[(artifact)
(define id (require-positional 2 "artifact id"))
(define result (call "GET" "/store/artifact" (hasheq 'artifactId id)))
(define content (base64-decode (string->bytes/latin-1
(hash-ref result 'contentBase64))))
(define out (args-get args 'out))
(if out
(begin
(display-to-file content out #:mode 'binary #:exists 'replace)
(print-result (hasheq 'ok #t
'artifactId (hash-ref result 'artifactId)
'bytes (hash-ref result 'bytes)
'sha256 (hash-ref result 'sha256)
'output out)
compact))
(begin
(write-bytes content)
(newline)))
0]
[else (raise-validation (format "Unknown command: store ~a" subcommand))])]
[(report)
;; --format html: one static evidence report for the bench session.
(define results (make-hasheq))
Expand Down Expand Up @@ -878,6 +1034,18 @@ Usage:
benchpilot observe cancel <observation-id> [--json] [--endpoint URL]
benchpilot report [--format html] [--out PATH]
[--target ID] [--json] [--endpoint URL]
benchpilot uds dtc read [--mask XX] [--target ID] [--json]
benchpilot uds dtc clear [--group XXXXXX] [--target ID] [--json]
benchpilot can send <data-hex> [--id 0x123] [--extended] [--resource ID]
benchpilot can capture start|stop <capture-id> [--resource ID] [--capacity N]
benchpilot can frames <capture-id> [--limit N] [--json]
benchpilot can decode <capture-id> --dbc FILE --id 0x123 [--limit N] [--json]
benchpilot lease acquire|renew|release <target-or-id> [--ttl SEC] [--json]
benchpilot lease list [--json]
benchpilot store evidence [--limit N] [--json]
benchpilot store observations [--limit N] [--json]
benchpilot store artifacts [--json]
benchpilot store artifact <id> [--out PATH] [--json]

benchpilot preflight [--target ID] [--json]
benchpilot bench validate [--target ID] [--json]
Expand Down
28 changes: 26 additions & 2 deletions racket/benchpilot/core/bench-runtime.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@
"readiness.rkt"
"runtime-state.rkt")

(require (only-in benchpilot/diagnostics/flash/image
image-format-for-path
image-segment-address
image-segment-data
merge-image-segments
parse-intel-hex
parse-srecord))

;; driver generics (Core abstractions)
(provide gen:resource-health-check
gen:power-supply
Expand Down Expand Up @@ -58,6 +66,7 @@
driver-create-runtime
;; runtime object: state + registry
(struct-out bench-runtime)
(struct-out target-ref)
make-bench-runtime
runtime-target
runtime-preflight
Expand Down Expand Up @@ -726,11 +735,26 @@
seg
[data
(read-segment-file (uds-flash-segment-file seg) base-directory)])))]))
(begin
(let ()
(unless address
(raise-validation
"Provide --address (flash start address, for example 0x08000000) or a flash plan file."))
(uds-flash-plan (list (uds-flash-segment address (file->bytes firmware-path) #f))
;; BIN keeps one explicit-address segment; HEX/S-record images
;; carry their own addresses and become one segment per region.
(define image-format (image-format-for-path firmware-path))
(define segments
(if image-format
(let* ([text (file->string firmware-path)]
[raw (if (string=? image-format "hex")
(parse-intel-hex text)
(parse-srecord text))]
[merged (merge-image-segments raw)])
(for/list ([seg (in-list merged)])
(uds-flash-segment (image-segment-address seg)
(bytes->list (image-segment-data seg))
#f)))
(list (uds-flash-segment address (file->bytes firmware-path) #f))))
(uds-flash-plan segments
1024
#x02
#f
Expand Down
66 changes: 66 additions & 0 deletions racket/benchpilot/core/device-errors.rkt
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#lang racket/base

;; Device/runtime error taxonomy: vendor SDK messages are classified into a
;; small, stable set of classes without ever leaking vendor types or vendor
;; SDK details into Core. Faulted operations carry a `device.error` evidence
;; item whose metadata names the class and the pattern that matched, so
;; agents can branch on behavior instead of parsing vendor strings.

(require racket/string)

(require benchpilot/core/contracts)

(provide device-error-classes
classify-device-error
device-error-evidence-item)

;; Each class lists the case-insensitive substrings that indicate it. The
;; first matching class wins; unmatched failures classify as `unknown`.
(define device-error-classes
(list
(cons "timeout"
'("timed out" "timeout" "deadline exceeded" "no response"))
(cons "transport"
'("connection refused" "connection closed" "no route" "unreachable"
"broken pipe" "reset by peer" "socket"))
(cons "device-state"
'("busy" "not ready" "device is off" "power is off" "no power"
"already open" "not open"))
(cons "permission"
'("access denied" "permission" "unauthorized" "in use by another"
"locked"))
(cons "not-found"
'("not found" "no such file" "no such device" "was not found"
"not visible" "not installed" "could not resolve"))
(cons "protocol"
'("checksum" "crc" "invalid response" "unexpected response"
"malformed" "nrc" "flow control" "iso-tp"))))

(define (contains-ci? haystack needle)
(string-contains? (string-foldcase haystack) (string-foldcase needle)))

;; -> (values class matched-pattern)
(define (classify-device-error message)
(let loop ([classes device-error-classes])
(cond
[(null? classes) (values "unknown" "")]
[else
(define match
(findf (lambda (pat) (contains-ci? message pat))
(cdr (car classes))))
(if match
(values (car (car classes)) match)
(loop (cdr classes)))])))

;; The evidence item attached to faulted operations.
(define (device-error-evidence-item message)
(define-values (class pattern) (classify-device-error message))
(bench-evidence-item
"device.error"
(format "Failure classified as ~a." class)
(let ([bounded (if (<= (string-length message) 2000)
message
(string-append (substring message 0 2000) "…"))])
bounded)
(hasheq "class" class
"matchedPattern" (if (string=? pattern "") "" pattern))))
6 changes: 4 additions & 2 deletions racket/benchpilot/core/evidence.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,10 @@
(with-handlers ([exn:fail? (lambda (e)
(semaphore-post sema)
(raise e))])
(begin0 (proc)
(semaphore-post sema))))
(dynamic-wind
(lambda () (void))
proc
(lambda () (semaphore-post sema)))))

;; Items of one bundle: bounded like the C# BoundItem.
(define (evidence-bound-items items)
Expand Down
Loading
Loading