/api/v1/chainStatus#
The complete conditioning verdict, in one call.
Query parameters
targetstring · nullable
air, device-loopback or pl-loopback. Defaults to what the board is
already wired for.
Responses
200OKapplication/json
targetrequiredstring
The target these conditions were computed for.
current_target string · nullable
What the board is wired for right now; null when the two loopback
switches could not be read.
reaches_the_connector requiredboolean
Does anything leave the board in this target? False for both loopbacks — the AD9361's internal one returns TX to RX BEFORE the mixers, so no LO, gain or attenuation setting can explain an empty analyser.
stagerequiredstring
readyrequiredboolean
nextstring · nullable
The first unmet condition and the command to run. null when ready.
conditionsrequiredarray of ConditionDRO
Every condition, in the order the bring-up needs them.
ConditionDRO · 8 fields
keyrequiredstring
Stable key — dac-source, tx-egress, …
stagerequiredstring
The stage this must be satisfied to leave.
sourcerequiredstring
board when the value was re-read from a register or an IIO attribute,
session when it is this daemon's memory and a restart forgets it.
requiredrequiredstring
observedrequiredstring
What it is, or could not be read.
verdictrequiredstring
met / unmet / unknown. An unknown BLOCKS: a register we failed to
read is not a register holding the right value.
breaksrequiredstring
What goes wrong when it is unmet, as the symptom the operator will see.
fixrequiredstring
The command that clears it.
Request
curl "http://192.168.2.1:8080/api/v1/chain"import requests
BASE_URL = "http://192.168.2.1:8080"
response = requests.get(f"{BASE_URL}/api/v1/chain")
response.raise_for_status()
print(response.json())let base_url = "http://192.168.2.1:8080";
let response = reqwest::Client::new()
.get(format!("{base_url}/api/v1/chain"))
.send()
.await?
.error_for_status()?;
let body: serde_json::Value = response.json().await?;
println!("{body:#}");Response
{
"target": "string",
"current_target": "string",
"reaches_the_connector": true,
"stage": "string",
"ready": true,
"next": "string",
"conditions": [
{
"key": "string",
"stage": "string",
"source": "string",
"required": "string",
"observed": "string",
"verdict": "string",
"breaks": "string",
"fix": "string"
}
]
}/api/v1/chain/conditionCondition#
Run the missing conditioning steps, in the one order that works.
WHY AN ENDPOINT AND NOT A RUNBOOK. The order is not a convention, it is a
constraint: rf tune resets the sample rate and drives dac_data_sel to PN
for its sweep, a mode switch rewrites the ingress gain, and a profile apply
rewrites the datapath registers and the LOs. Each step therefore undoes part
of the ones before it if taken out of turn — which is exactly the mistake
that produced five empty RF passes on 2026-09-09, from a note that stated the
rule correctly. A sequence encoded in a function cannot be read in the wrong
order.
IT CALLS THE EXISTING HANDLERS, deliberately, rather than repeating what they
do. Duplicating rf_mode_air's body here would let the two drift, and the
part that would drift is the ordering inside it — the destination is
configured fully before the path is switched to it, because mux-first
measured 31.28 % against 99.98 %.
Request body application/jsonrequired
targetstring · nullable
Where the samples should go. Defaults to air — the only target of the
three that puts anything on a connector, and the one an operator at a
spectrum analyser is asking for.
profilestring · nullable
Which profile to apply. Defaults to the one currently applied; with none applied the request is refused rather than guessing, because the profile decides the frame length, the coding and the frequency.
rate_sps integer · int64
AD9361 rate for the tuning step, when one is needed.
Responses
200OKapplication/json
stepsrequiredarray of string
The steps actually run, in order.
detailrequiredarray of string
What each step reported, keyed in the same order as steps.
staterequiredChainStateDRO · 7 fields
targetrequiredstring
The target these conditions were computed for.
current_target string · nullable
What the board is wired for right now; null when the two loopback
switches could not be read.
reaches_the_connector requiredboolean
Does anything leave the board in this target? False for both loopbacks — the AD9361's internal one returns TX to RX BEFORE the mixers, so no LO, gain or attenuation setting can explain an empty analyser.
stagerequiredstring
readyrequiredboolean
nextstring · nullable
The first unmet condition and the command to run. null when ready.
conditionsrequiredarray of ConditionDRO
Every condition, in the order the bring-up needs them.
ConditionDRO · 8 fields
keyrequiredstring
Stable key — dac-source, tx-egress, …
stagerequiredstring
The stage this must be satisfied to leave.
sourcerequiredstring
board when the value was re-read from a register or an IIO attribute,
session when it is this daemon's memory and a restart forgets it.
requiredrequiredstring
observedrequiredstring
What it is, or could not be read.
verdictrequiredstring
met / unmet / unknown. An unknown BLOCKS: a register we failed to
read is not a register holding the right value.
breaksrequiredstring
What goes wrong when it is unmet, as the symptom the operator will see.
fixrequiredstring
The command that clears it.
Request
curl -X POST "http://192.168.2.1:8080/api/v1/chain/condition" \
-H "Content-Type: application/json" \
-d '{
"target": "string",
"profile": "string",
"rate_sps": 0
}'import requests
BASE_URL = "http://192.168.2.1:8080"
response = requests.post(
f"{BASE_URL}/api/v1/chain/condition",
json={
"target": "string",
"profile": "string",
"rate_sps": 0,
},
)
response.raise_for_status()
print(response.json())let base_url = "http://192.168.2.1:8080";
let response = reqwest::Client::new()
.post(format!("{base_url}/api/v1/chain/condition"))
.json(&serde_json::json!({
"target": "string",
"profile": "string",
"rate_sps": 0
}))
.send()
.await?
.error_for_status()?;
let body: serde_json::Value = response.json().await?;
println!("{body:#}");Response
{
"steps": [
"string"
],
"detail": [
"string"
],
"state": {
"target": "string",
"current_target": "string",
"reaches_the_connector": true,
"stage": "string",
"ready": true,
"next": "string",
"conditions": [
{
"key": "string",
"stage": "string",
"source": "string",
"required": "string",
"observed": "string",
"verdict": "string",
"breaks": "string",
"fix": "string"
}
]
}
}/api/v1/chain/recoverRecover#
Pulse the receiver's CLEAR bits — the recovery that is not a reboot.
WHY THIS IS A ROUTE AND NOT A NOTE. The carrier loop on this bench gets
stuck: symbols flow, lock reads true, and the deframer never finds its ASM.
The recorded remedy is a CLEAR pulse, after which the RF link frames at
~100 %. Three blocks decode CTRL[1] = clear, the RTL consumes it, and the
PS wrote none of them — so the remedy existed only as a reg set typed by
hand, and the ordinary answer to a stuck loop was a reboot.
IT REPORTS WHAT IT PULSED, NOT THAT IT WORKED. The reply carries an explicit caveat, because a frame counter that starts moving is not evidence: the ASM is inserted downstream of the payload in the PL, so it counts clean frames over entirely wrong bytes (STE-402). The verdict is a byte-exact loopback.
Responses
503No PL driverapplication/json
Request
curl -X POST "http://192.168.2.1:8080/api/v1/chain/recover" \
-o response.binimport requests
BASE_URL = "http://192.168.2.1:8080"
response = requests.post(f"{BASE_URL}/api/v1/chain/recover")
response.raise_for_status()
data = response.contentlet base_url = "http://192.168.2.1:8080";
let response = reqwest::Client::new()
.post(format!("{base_url}/api/v1/chain/recover"))
.send()
.await?
.error_for_status()?;
let bytes = response.bytes().await?;Response
{}