Raw CI-V Transactions¶
Raw CI-V transactions are the response-capable counterpart to the
fire-and-forget send_civ command path. They are intentionally scoped to one
frame and one explicit expectation.
Release Status¶
The response-capable transaction surface is public/dev-facing for v2.5-style release readers:
- Python:
CoreRadio.send_civ_transaction(); - HTTP:
POST /api/v1/civ/transaction; - ordered batches:
type: "raw_civ_transaction"steps inPOST /api/v1/commands/batch.
The transparent raw CI-V pipe used by the local Hamlib A1 bridge runner
(send_civ_raw_fire_and_forget() plus add_raw_civ_listener()) remains
internal experimental infrastructure. It exists to validate external CAT
session ownership and bridge behavior without committing the pipe itself as a
stable application API. The open-core boundary remains the generic provider
contract, external rigctld client behavior, discovery candidates, and public
CLI/docs; managed setup UX, support-evidence workflows, packaging decisions,
and hosted/customer-specific flows belong outside Core.
Ownership¶
CoreRadio.send_civ_transaction() claims the existing external CAT-session
guard before sending the frame. While the guard is active, cooperating pollers
pause through external_cat_session_active, preventing background CI-V reads
or writes from consuming the caller's response. The guard is released in a
finally block on success, timeout, cancellation, and errors.
begin_external_cat_session() remains idempotent when the external CAT owner
already holds the guard, and end_external_cat_session() remains idempotent.
A transaction rejects overlapping ownership instead of interrupting an existing
external CAT session; external CAT begin likewise fails cleanly while a raw
transaction owns the guard.
CoreRadio.connect() resets any leaked external-CAT ownership at the LAN
runtime full-connect boundary. This covers managed-runtime restarts, partial
bridge startup failures, and LAN soft_reconnect() paths that fall back to
that full CoreRadio.connect() after begin_external_cat_session() was called
but the matching end_external_cat_session() never ran. A normal soft
reconnect is not the reset boundary, and the shared serial Icom backend
connect() override does not currently perform this reset. Rigctld and web
pollers resume ownership only after the new LAN full radio session is
established.
Response Matching¶
Transactions run on CivRuntime, not RadioPoller. They reuse the existing
CI-V RX pump and CivRequestTracker:
expect="none"sends once and returnsstatus: "sent".expect="ack"drops orphan ACK/NAK backlog, then registers an ACK waiter and resolves only a fresh ACK or NAK from the active transaction.expect="data"registers a keyed response waiter and a NAK-only waiter, so a radio NAK returnsstatus: "nak"without allowing unrelated ACK frames to satisfy the data expectation.
The runtime does not infer response behavior from _civ_expects_response() for
transactions. Callers must choose the expectation mode so vendor-specific
commands remain explicit.
HTTP Surface¶
POST /api/v1/civ/transaction and explicit
type: "raw_civ_transaction" steps in /api/v1/commands/batch are the HTTP
surfaces that wait for raw CI-V responses. /api/v1/commands and legacy
send_civ steps in /api/v1/commands/batch remain queued fire-and-forget
surfaces, and wait_response=true stays rejected there.
Ordered Batch Transaction Contract¶
Issue #1633 defines the contract for response-capable raw CI-V transaction
steps in POST /api/v1/commands/batch; #1634 and #1635 implement and test the
server behavior under parent #1624. This is a Core API/protocol feature: it
defines generic ordered-batch behavior, backend-neutral radio ownership, and
deterministic per-step results. Core does not store named radio profiles,
managed setup data, hosted-account state, or customer-specific workflows.
Callers may still send caller-owned profile batches as ordinary request bodies.
The batch endpoint must keep the existing legacy command step shape unchanged:
Raw CI-V transactions use a new explicit step type, not name/params:
{
"type": "raw_civ_transaction",
"id": "display-type-query",
"command": 26,
"sub": 5,
"data": "0153",
"expect": "data",
"timeout_ms": 1000
}
Required transaction step fields:
type: exactly"raw_civ_transaction".command: integer byte from0through255.expect: one of"none","ack", or"data".
Optional transaction step fields:
id: caller-owned JSON value echoed in that step result when present.sub: integer byte from0through255.data: compact even-length hexadecimal string. Input is case-insensitive; output hex is uppercase.timeout_ms: positive finite number of milliseconds for this transaction step. If omitted, the transaction step uses the batch step timeout default:10000milliseconds.
continue_on_error remains a batch-level option only. A
raw_civ_transaction step-level continue_on_error field is invalid, because
mixing batch-level and step-level continuation rules would make ordering hard
to reason about. Legacy command steps keep their existing tolerance for extra
fields; the new transaction step contract should be strict.
Batch Ordering¶
The batch executor processes steps in array order. It must not enqueue or
start step N + 1 until step N has either succeeded, failed with
continue_on_error: true, or caused later steps to be skipped.
Legacy command steps continue to use the ordered command queue. The HTTP
handler waits for the poller/backend future for that queued step before
advancing. Transaction steps bypass RadioPoller, acquire the scoped CI-V
transaction owner, send exactly one frame, wait according to expect, and
release ownership before the next batch step is considered.
When legacy command steps and transaction steps are mixed, ordering is therefore:
- queued command step is enqueued on the exact-order lane;
- handler waits for that queued command's completion or timeout;
- transaction step acquires CI-V ownership and completes or fails;
- later queued command steps are enqueued only after the transaction releases ownership.
The ordering guarantee is for steps inside this batch. Existing cross-request
queue behavior is otherwise unchanged. If another external CAT session or raw
transaction already owns the backend when the transaction step tries to start,
the step returns civ_owner_conflict.
Per-Step Timeout¶
Queued command steps keep the existing 10 second command batch step timeout.
Transaction steps use timeout_ms when present, otherwise 10000
milliseconds. Units are always milliseconds in JSON and seconds only inside
Python internals. Timeout covers ownership acquisition plus the send/wait path
for the transaction step; it does not include earlier or later batch steps.
On timeout, the transaction primitive must unregister pending CI-V waiters, release ownership, and return a per-step timeout result. It must not leave the poller paused or the request tracker polluted for later steps.
Transaction Step Results¶
The batch response remains HTTP 200 after the request body is accepted. The
top-level ok is true only when every reported step has ok: true.
Transport/auth/root JSON failures keep the existing endpoint-level HTTP error
behavior.
Successful transaction result shapes:
{
"index": 0,
"type": "raw_civ_transaction",
"id": "display-type-query",
"ok": true,
"status": "response",
"result": {
"frame": "FEFEE0981A050153FD",
"command": 26,
"sub": 5,
"data": "0153"
}
}
expect: "none" returns status: "sent" and does not wait for a radio frame:
{
"index": 0,
"type": "raw_civ_transaction",
"ok": true,
"status": "sent",
"result": {
"frame": null,
"command": null,
"sub": null,
"data": null
}
}
ACK and NAK results use the same result object shape as the single
transaction endpoint:
{
"index": 0,
"type": "raw_civ_transaction",
"ok": true,
"status": "ack",
"result": {
"frame": "FEFEE0A2FBFD",
"command": 251,
"sub": null,
"data": ""
}
}
{
"index": 0,
"type": "raw_civ_transaction",
"ok": false,
"status": "nak",
"error": "radio_nak",
"message": "radio returned CI-V NAK",
"result": {
"frame": "FEFEE0A2FAFD",
"command": 250,
"sub": null,
"data": ""
}
}
Failure results:
| Case | status |
error |
Notes |
|---|---|---|---|
| timeout | timed_out |
transaction_timeout |
Timeout before expected ACK/data response or before ownership could complete |
| owner conflict | owner_conflict |
civ_owner_conflict |
Another transaction or external CAT owner already owns the CI-V stream |
| unsupported backend | unsupported |
unsupported_command |
Active backend does not implement CivTransactionCapable |
| read-only server | read_only |
read_only |
Raw CI-V transaction steps are disabled in read-only mode |
| no radio during execution | no_radio |
no_radio |
No active radio is available for this step |
| validation failure | failed_validation |
invalid_request or invalid_step |
Malformed transaction shape, byte range, hex data, expectation, or timeout |
| runtime failure | failed_execution |
transaction_failed |
Non-owner-conflict RuntimeError raised by send_civ_transaction() |
| skipped after failure | skipped |
skipped_after_failure |
Step was not validated or executed because an earlier step stopped the batch |
Unsupported typed batch steps are handled separately from malformed
raw_civ_transaction steps. If a step has type present, the type is not
supported, and no legacy name is present, that step returns
status: "failed_validation" with error: "unknown_step_type".
Examples:
{
"index": 0,
"type": "raw_civ_transaction",
"ok": false,
"status": "timed_out",
"error": "transaction_timeout",
"message": "raw CI-V transaction timed out"
}
{
"index": 0,
"type": "raw_civ_transaction",
"ok": false,
"status": "owner_conflict",
"error": "civ_owner_conflict",
"message": "CI-V stream is already owned by another transaction"
}
{
"index": 0,
"type": "raw_civ_transaction",
"ok": false,
"status": "failed_validation",
"error": "invalid_request",
"message": "timeout_ms must be a positive finite number"
}
Skipped legacy command steps keep the existing skipped result shape with
name. Skipped transaction steps use type and echo id when those fields
are readable without full validation:
{
"index": 2,
"type": "raw_civ_transaction",
"id": "later-query",
"ok": false,
"status": "skipped",
"error": "skipped_after_failure",
"message": "skipped after earlier batch failure"
}
continue_on_error¶
continue_on_error defaults to false and remains a JSON boolean at the
batch root. It applies uniformly to legacy queued command failures and raw
CI-V transaction failures.
When continue_on_error is false, these failures stop the batch and mark
all later steps as skipped:
- transaction
nak; - transaction
timed_out; - transaction
owner_conflict; - transaction
unsupported; - transaction
read_only; - transaction
no_radio; - transaction
failed_validation; - transaction
failed_execution; - queued command
failed_validation; - queued command
timed_out; - queued command
failed_execution.
When continue_on_error is true, the failing step is reported with
ok: false and the executor proceeds to the next step after cleanup. For
transaction failures this means ownership has been released and pending CI-V
waiters have been unregistered before the next step starts. For queued command
timeouts this keeps the current behavior: the unconsumed timed-out queued step
is cancelled before the next batch step is prepared.
Batch root validation failures are not per-step failures and do not honor
continue_on_error. Examples include malformed JSON, missing or non-list
steps, empty steps, too many steps, and a non-boolean batch-level
continue_on_error.
Compatibility¶
Existing fire-and-forget batch behavior is unchanged:
- legacy
{ "name": ..., "params": ... }steps retain their request and result shape; send_civin/api/v1/commandsand/api/v1/commands/batchremains queued and fire-and-forget;send_civstill rejectswait_response=true;- response-capable waiting is used only when the new
"type": "raw_civ_transaction"step is explicitly requested; - the single
POST /api/v1/civ/transactionendpoint keeps its current contract.
Implementation Test Matrix¶
Implementation issues for #1633/#1624 should cover:
| Case | Expected coverage |
|---|---|
| legacy-only batch regression | Existing { "name": ..., "params": ... } batches keep their request shape, result shape, ordering, and success behavior |
mixed command -> transaction -> command batch |
The first command completes before the transaction starts, and the later command is enqueued only after transaction ownership is released |
| transaction timeout | A transaction step returns status: "timed_out" and error: "transaction_timeout", unregisters waiters, releases ownership, and skips later steps when continue_on_error is false |
| transaction NAK | A fresh radio NAK returns status: "nak" and error: "radio_nak" with the NAK result frame, then applies the configured continuation rule |
| owner conflict | Existing external CAT or transaction ownership returns status: "owner_conflict" and error: "civ_owner_conflict" without interrupting the current owner |
| read-only server | A transaction step in read-only mode returns status: "read_only" and error: "read_only" before sending any CI-V frame |
| no radio during execution | A transaction step with no active radio returns status: "no_radio" and error: "no_radio" as a per-step failure |
| unsupported backend | A backend without CivTransactionCapable returns status: "unsupported" and error: "unsupported_command" as a per-step failure |
continue_on_error: false |
Transaction failures stop the batch and report later steps as status: "skipped" with error: "skipped_after_failure" |
continue_on_error: true |
Transaction failures are reported with ok: false, cleanup completes, and the executor proceeds to the next step in order |
fire-and-forget send_civ regression |
send_civ in /api/v1/commands and legacy batch steps remains queued and fire-and-forget, and wait_response=true remains rejected |