feat: add config API endpoint and JSON schema draft

This commit is contained in:
2026-04-07 17:26:24 -04:00
parent 7005ae766d
commit a7ab6eaf7c
3 changed files with 438 additions and 113 deletions
+85 -29
View File
@@ -478,58 +478,98 @@ def device_event_waveform(
# ── Write endpoints ───────────────────────────────────────────────────────────
class ProjectInfoBody(BaseModel):
"""Request body for POST /device/config/project."""
project: Optional[str] = None
client_name: Optional[str] = None
operator: Optional[str] = None
seis_loc: Optional[str] = None
notes: Optional[str] = None
class DeviceConfigBody(BaseModel):
"""
Request body for POST /device/config.
All fields are optional — only supplied (non-null) fields are written to
the device. All other config bytes are round-tripped verbatim.
Recording parameters
--------------------
sample_rate : Samples per second. Valid values: 1024, 2048, 4096.
record_time : Record duration in seconds (e.g. 1.0, 2.0, 3.0).
Trigger / alarm thresholds (geo channels, in/s)
------------------------------------------------
trigger_level_geo : Trigger threshold in in/s (e.g. 0.5).
alarm_level_geo : Alarm threshold in in/s (e.g. 1.0).
max_range_geo : Full-scale calibration constant (e.g. 6.206).
Rarely changed — only set if you know what you're doing.
Project / operator strings (max 41 ASCII characters each)
----------------------------
project : Project description.
client_name : Client / company name.
operator : Operator / technician name.
seis_loc : Sensor location description.
notes : Extended notes.
"""
# Recording parameters
sample_rate: Optional[int] = None
record_time: Optional[float] = None
# Threshold parameters
trigger_level_geo: Optional[float] = None
alarm_level_geo: Optional[float] = None
max_range_geo: Optional[float] = None
# Project / operator strings
project: Optional[str] = None
client_name: Optional[str] = None
operator: Optional[str] = None
seis_loc: Optional[str] = None
notes: Optional[str] = None
@app.post("/device/config/project")
def device_config_project(
body: ProjectInfoBody,
@app.post("/device/config")
def device_config(
body: DeviceConfigBody,
port: Optional[str] = Query(None, description="Serial port (e.g. COM5)"),
baud: int = Query(38400, description="Serial baud rate"),
host: Optional[str] = Query(None, description="TCP host — modem IP or ACH relay"),
tcp_port: int = Query(DEFAULT_TCP_PORT, description=f"TCP port (default {DEFAULT_TCP_PORT})"),
) -> dict:
"""
POC: Read the current device config, patch ASCII project/client/operator/
sensor-location/notes fields, and write the modified config back.
Read the current device config, apply any supplied changes to the compliance
block, and write the full config back.
Only fields included in the JSON body (non-null) are modified. All other
bytes are round-tripped verbatim from the device.
Only non-null fields in the JSON body are modified. All other config bytes
are round-tripped verbatim from the device.
Supply either *port* (serial) or *host* (TCP/modem).
Example body:
Example body (all fields optional — include only what you want to change):
{
"project": "Bridge Inspection 2026",
"client_name": "City of Portland",
"operator": "Brian Harrison",
"seis_loc": "South Abutment",
"notes": "Pre-blast baseline"
"sample_rate": 1024,
"record_time": 3.0,
"trigger_level_geo": 0.5,
"alarm_level_geo": 1.0,
"project": "Bridge Inspection 2026",
"client_name": "City of Portland",
"operator": "Brian Harrison",
"seis_loc": "South Abutment",
"notes": "Pre-blast baseline"
}
Returns:
{"status": "ok", "message": "..."} on success.
{"status": "ok", "updated_fields": {...}} on success.
Raises:
502 on protocol errors (timeout, bad ack, etc.).
422 if neither port nor host is provided.
"""
log.info(
"POST /device/config/project port=%s host=%s body=%s",
port, host, body.model_dump(exclude_none=True),
)
changed = body.model_dump(exclude_none=True)
log.info("POST /device/config port=%s host=%s fields=%s", port, host, list(changed.keys()))
try:
def _do():
with _build_client(port, baud, host, tcp_port) as client:
client.connect()
client.set_project_info(
client.apply_config(
sample_rate=body.sample_rate,
record_time=body.record_time,
trigger_level_geo=body.trigger_level_geo,
alarm_level_geo=body.alarm_level_geo,
max_range_geo=body.max_range_geo,
project=body.project,
client_name=body.client_name,
operator=body.operator,
@@ -543,12 +583,28 @@ def device_config_project(
raise HTTPException(status_code=502, detail=f"Protocol error: {exc}") from exc
except OSError as exc:
raise HTTPException(status_code=502, detail=f"Connection error: {exc}") from exc
except ValueError as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc
except Exception as exc:
raise HTTPException(status_code=500, detail=f"Device error: {exc}") from exc
changed = body.model_dump(exclude_none=True)
msg = "Config written. Fields updated: " + (", ".join(changed.keys()) or "none")
return {"status": "ok", "message": msg, "updated_fields": changed}
return {
"status": "ok",
"updated_fields": changed,
}
# Keep the old endpoint alive under its old URL for anything already calling it
@app.post("/device/config/project")
def device_config_project(
body: DeviceConfigBody,
port: Optional[str] = Query(None, description="Serial port (e.g. COM5)"),
baud: int = Query(38400, description="Serial baud rate"),
host: Optional[str] = Query(None, description="TCP host — modem IP or ACH relay"),
tcp_port: int = Query(DEFAULT_TCP_PORT, description=f"TCP port (default {DEFAULT_TCP_PORT})"),
) -> dict:
"""Deprecated alias for POST /device/config — use that instead."""
return device_config(body=body, port=port, baud=baud, host=host, tcp_port=tcp_port)
# ── Entry point ────────────────────────────────────────────────────────────────