mirror of
https://github.com/dmduran12/pymc_console-dist.git
synced 2026-07-06 09:51:50 +02:00
96b602d3f9
Automated sync from private repository. Commit: 04b2177304db0b857ae52402b99635bd468ad539
939 lines
33 KiB
JSON
939 lines
33 KiB
JSON
{
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"title": "pyMC Console API Specification",
|
|
"version": "3.0.0",
|
|
"description": "Simplified API specification for pyMC_Repeater backend. WebSocket for radio events, REST for everything else.",
|
|
"meta": {
|
|
"author": "pyMC Console Team",
|
|
"created": "2026-01-16",
|
|
"target_backend": "pyMC_Repeater (CherryPy + SQLite)",
|
|
"philosophy": "KISS - Keep It Simple Stupid"
|
|
},
|
|
|
|
"architecture": {
|
|
"overview": "Two communication channels with clear separation of concerns.",
|
|
"websocket": {
|
|
"purpose": "Real-time radio hardware events only",
|
|
"endpoint": "ws://host:8000/radio",
|
|
"when_to_use": "Streaming RX packets, TX confirmations, noise floor samples, radio errors"
|
|
},
|
|
"rest": {
|
|
"purpose": "Everything else - queries, configuration, historical data, actions",
|
|
"base_url": "http://host:8000/api",
|
|
"when_to_use": "Initial data load, pagination, config changes, time series queries"
|
|
},
|
|
"mental_model": "WebSocket = live radio wire. REST = database/config access."
|
|
},
|
|
|
|
"sx1262_capabilities": {
|
|
"description": "What the SX1262 radio hardware can actually report. These are the source data for WebSocket events.",
|
|
|
|
"on_packet_receive": {
|
|
"rssi_dbm": "Received signal strength (register / -2)",
|
|
"snr_db": "Signal-to-noise ratio",
|
|
"signal_rssi_dbm": "Alternative RSSI measurement",
|
|
"payload": "Raw packet bytes",
|
|
"crc_valid": "Boolean - CRC check passed",
|
|
"header_valid": "Boolean - header decoded successfully"
|
|
},
|
|
|
|
"on_transmit_complete": {
|
|
"airtime_ms": "Actual transmission duration in milliseconds",
|
|
"lbt_attempts": "Number of CAD (Listen Before Talk) retries",
|
|
"lbt_backoff_delays_ms": "Array of backoff durations if channel was busy",
|
|
"channel_busy": "Boolean - whether LBT detected activity"
|
|
},
|
|
|
|
"continuous_sampling": {
|
|
"noise_floor_dbm": "Instantaneous RSSI when radio is idle",
|
|
"sampling_interval": "Background task samples every ~10ms during quiet periods",
|
|
"averaging": "20 samples averaged, reported every 5 seconds"
|
|
},
|
|
|
|
"radio_state": {
|
|
"frequency_hz": "Current operating frequency",
|
|
"tx_power_dbm": "Current TX power",
|
|
"spreading_factor": "Current SF (5-12)",
|
|
"bandwidth_hz": "Current bandwidth",
|
|
"coding_rate": "Current coding rate (5-8)",
|
|
"initialized": "Boolean - radio ready"
|
|
},
|
|
|
|
"irq_events": {
|
|
"description": "Hardware interrupts that can be monitored",
|
|
"IRQ_TX_DONE": "Transmission complete",
|
|
"IRQ_RX_DONE": "Packet received",
|
|
"IRQ_CRC_ERR": "CRC error on receive",
|
|
"IRQ_TIMEOUT": "RX or TX timeout",
|
|
"IRQ_PREAMBLE_DETECTED": "Preamble detected (packet incoming)",
|
|
"IRQ_HEADER_VALID": "Header successfully decoded",
|
|
"IRQ_HEADER_ERR": "Header decode error",
|
|
"IRQ_CAD_DONE": "Channel activity detection complete",
|
|
"IRQ_CAD_DETECTED": "Activity detected during CAD"
|
|
}
|
|
},
|
|
|
|
"websocket": {
|
|
"endpoint": "/radio",
|
|
"description": "Single WebSocket endpoint for all radio events. No subscriptions, no channels - just connect and receive.",
|
|
|
|
"message_format": {
|
|
"type": "string - event type identifier",
|
|
"ts": "integer - Unix timestamp in milliseconds",
|
|
"data": "object - event-specific payload"
|
|
},
|
|
|
|
"server_events": {
|
|
"rx": {
|
|
"description": "Packet received from radio",
|
|
"trigger": "IRQ_RX_DONE with valid CRC",
|
|
"data": {
|
|
"packet_hash": "string - unique packet identifier",
|
|
"rssi": "integer - RSSI in dBm",
|
|
"snr": "number - SNR in dB",
|
|
"signal_rssi": "integer - signal RSSI in dBm",
|
|
"payload_len": "integer - payload size in bytes",
|
|
"crc_ok": "boolean",
|
|
"type": "integer - MeshCore payload type (0-15)",
|
|
"route": "integer - MeshCore route type (0-3)",
|
|
"src_hash": "string - source node hash",
|
|
"path": "array of strings - 2-char hop prefixes"
|
|
},
|
|
"example": {
|
|
"type": "rx",
|
|
"ts": 1736934849000,
|
|
"data": {
|
|
"packet_hash": "a1b2c3d4",
|
|
"rssi": -95,
|
|
"snr": 7.5,
|
|
"signal_rssi": -97,
|
|
"payload_len": 48,
|
|
"crc_ok": true,
|
|
"type": 4,
|
|
"route": 1,
|
|
"src_hash": "0x42AB",
|
|
"path": ["FA", "79", "24"]
|
|
}
|
|
}
|
|
},
|
|
|
|
"tx": {
|
|
"description": "Packet transmitted by this node",
|
|
"trigger": "IRQ_TX_DONE after send_packet()",
|
|
"data": {
|
|
"packet_hash": "string - unique packet identifier",
|
|
"airtime_ms": "number - actual TX duration",
|
|
"lbt_attempts": "integer - CAD retry count (0 = clear on first try)",
|
|
"lbt_backoff_ms": "array of numbers - backoff delays if retried",
|
|
"channel_busy": "boolean - true if any CAD detected activity"
|
|
},
|
|
"example": {
|
|
"type": "tx",
|
|
"ts": 1736934849000,
|
|
"data": {
|
|
"packet_hash": "d4c3b2a1",
|
|
"airtime_ms": 145.2,
|
|
"lbt_attempts": 2,
|
|
"lbt_backoff_ms": [85, 170],
|
|
"channel_busy": true
|
|
}
|
|
}
|
|
},
|
|
|
|
"noise": {
|
|
"description": "Periodic noise floor measurement",
|
|
"trigger": "Background sampling during idle periods",
|
|
"frequency": "Every 5 seconds when radio is idle",
|
|
"data": {
|
|
"noise_floor_dbm": "number - averaged noise floor reading"
|
|
},
|
|
"example": {
|
|
"type": "noise",
|
|
"ts": 1736934849000,
|
|
"data": {
|
|
"noise_floor_dbm": -107.5
|
|
}
|
|
}
|
|
},
|
|
|
|
"cad": {
|
|
"description": "Channel activity detection result",
|
|
"trigger": "CAD operation completed (during LBT or explicit check)",
|
|
"data": {
|
|
"detected": "boolean - activity detected",
|
|
"det_peak": "integer - detection peak threshold used",
|
|
"det_min": "integer - detection minimum threshold used"
|
|
},
|
|
"example": {
|
|
"type": "cad",
|
|
"ts": 1736934849000,
|
|
"data": {
|
|
"detected": true,
|
|
"det_peak": 24,
|
|
"det_min": 12
|
|
}
|
|
}
|
|
},
|
|
|
|
"error": {
|
|
"description": "Radio error event",
|
|
"trigger": "CRC error, header error, or timeout",
|
|
"data": {
|
|
"code": "string - error type: CRC_ERR, HEADER_ERR, TIMEOUT",
|
|
"rssi": "integer - RSSI if available (may help diagnose)",
|
|
"snr": "number - SNR if available"
|
|
},
|
|
"example": {
|
|
"type": "error",
|
|
"ts": 1736934849000,
|
|
"data": {
|
|
"code": "CRC_ERR",
|
|
"rssi": -98,
|
|
"snr": -2.5
|
|
}
|
|
}
|
|
},
|
|
|
|
"hardware": {
|
|
"description": "Periodic system hardware metrics",
|
|
"trigger": "Background sampling every 10 seconds",
|
|
"frequency": "Every 10 seconds",
|
|
"data": {
|
|
"cpu_percent": "number - CPU usage percentage",
|
|
"memory_percent": "number - RAM usage percentage",
|
|
"disk_percent": "number - Disk usage percentage",
|
|
"temperature_c": "number or null - CPU temperature in Celsius (null if unavailable)"
|
|
},
|
|
"example": {
|
|
"type": "hardware",
|
|
"ts": 1736934849000,
|
|
"data": {
|
|
"cpu_percent": 23.5,
|
|
"memory_percent": 45.2,
|
|
"disk_percent": 67.8,
|
|
"temperature_c": 52.3
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
"client_commands": {
|
|
"description": "Commands client can send to control radio. Keep these minimal.",
|
|
|
|
"send_advert": {
|
|
"description": "Trigger ADVERT broadcast",
|
|
"request": {"cmd": "send_advert"},
|
|
"response": {"ok": true}
|
|
},
|
|
|
|
"set_tx_power": {
|
|
"description": "Change TX power (live, no restart needed)",
|
|
"request": {"cmd": "set_tx_power", "dbm": 22},
|
|
"response": {"ok": true, "applied": 22},
|
|
"constraints": "-17 to 22 dBm for SX1262"
|
|
}
|
|
},
|
|
|
|
"connection_lifecycle": {
|
|
"1_connect": "Client opens WebSocket to ws://host:8000/radio",
|
|
"2_receive": "Server immediately starts pushing events",
|
|
"3_command": "Client sends commands as needed (optional)",
|
|
"4_disconnect": "On disconnect, client simply reconnects",
|
|
"notes": [
|
|
"No authentication on WebSocket (same as REST API currently)",
|
|
"No session state - each connection is independent",
|
|
"No replay of missed events - use REST for historical data",
|
|
"No heartbeat needed - WebSocket protocol handles keepalive"
|
|
]
|
|
},
|
|
|
|
"implementation_notes": {
|
|
"library": "Use ws4py or websockets with CherryPy",
|
|
"broadcast": "Maintain list of connected clients, broadcast to all",
|
|
"threading": "Events come from async radio callbacks - use queue or call_soon_threadsafe",
|
|
"estimated_code": "~100 lines for WebSocket handler"
|
|
}
|
|
},
|
|
|
|
"rest_api": {
|
|
"description": "RESTful endpoints organized by domain. Build in phases.",
|
|
|
|
"phase_1_core": {
|
|
"description": "Foundation endpoints - these exist or need minor updates",
|
|
|
|
"GET /api/stats": {
|
|
"description": "Current system state snapshot",
|
|
"response": {
|
|
"node": {
|
|
"name": "string",
|
|
"hash": "string - e.g. 0x42AB1234",
|
|
"uptime_seconds": "integer"
|
|
},
|
|
"radio": {
|
|
"frequency_hz": "integer",
|
|
"tx_power_dbm": "integer",
|
|
"spreading_factor": "integer",
|
|
"bandwidth_hz": "integer",
|
|
"coding_rate": "integer",
|
|
"noise_floor_dbm": "number"
|
|
},
|
|
"counts": {
|
|
"rx_total": "integer",
|
|
"tx_total": "integer",
|
|
"forwarded_total": "integer",
|
|
"dropped_total": "integer"
|
|
},
|
|
"mode": "string - forward|monitor",
|
|
"duty_cycle_enabled": "boolean"
|
|
},
|
|
"status": "EXISTS - may need field additions"
|
|
},
|
|
|
|
"GET /api/config": {
|
|
"description": "Full configuration dump",
|
|
"status": "EXISTS"
|
|
},
|
|
|
|
"POST /api/config": {
|
|
"description": "Update configuration (persists to disk)",
|
|
"status": "EXISTS as update_radio_config - consider consolidating"
|
|
},
|
|
|
|
"GET /api/logs": {
|
|
"description": "Recent system log entries",
|
|
"params": {
|
|
"limit": {"type": "integer", "default": 100, "max": 1000},
|
|
"level": {"type": "string", "enum": ["DEBUG", "INFO", "WARNING", "ERROR"], "description": "Filter by minimum log level"}
|
|
},
|
|
"response": {
|
|
"logs": [{
|
|
"timestamp": "integer - Unix timestamp",
|
|
"level": "string - DEBUG|INFO|WARNING|ERROR",
|
|
"module": "string - source module name",
|
|
"message": "string - log message content"
|
|
}]
|
|
},
|
|
"status": "EXISTS"
|
|
}
|
|
},
|
|
|
|
"phase_2_packets": {
|
|
"description": "Packet storage and retrieval with proper pagination",
|
|
|
|
"GET /api/packets": {
|
|
"description": "Paginated packet list with filtering",
|
|
"params": {
|
|
"limit": {"type": "integer", "default": 100, "max": 1000},
|
|
"offset": {"type": "integer", "default": 0, "description": "Skip N packets for pagination"},
|
|
"before": {"type": "integer", "description": "Unix timestamp - get packets older than this"},
|
|
"after": {"type": "integer", "description": "Unix timestamp - get packets newer than this"},
|
|
"type": {"type": "integer", "description": "Filter by payload type (0-15)"},
|
|
"route": {"type": "integer", "description": "Filter by route type (0-3)"},
|
|
"transmitted": {"type": "boolean", "description": "Filter to TX only"},
|
|
"received": {"type": "boolean", "description": "Filter to RX only"},
|
|
"src_hash": {"type": "string", "description": "Filter by source node hash"},
|
|
"dst_hash": {"type": "string", "description": "Filter by destination node hash"},
|
|
"path_contains": {"type": "string", "description": "Filter packets where path contains this 2-char prefix"}
|
|
},
|
|
"response": {
|
|
"packets": "array of Packet objects",
|
|
"pagination": {
|
|
"has_more": "boolean",
|
|
"next_cursor": "string - timestamp for next page",
|
|
"count": "integer - packets in this response"
|
|
}
|
|
},
|
|
"status": "EXISTS as recent_packets - needs pagination upgrade"
|
|
},
|
|
|
|
"GET /api/packets/{hash}": {
|
|
"description": "Single packet by hash",
|
|
"response": "Packet object or 404",
|
|
"status": "EXISTS as packet_by_hash"
|
|
}
|
|
},
|
|
|
|
"phase_3_neighbors": {
|
|
"description": "Contact/neighbor management with zero-hop detection",
|
|
|
|
"GET /api/neighbors": {
|
|
"description": "List all known neighbors with optional filtering",
|
|
"params": {
|
|
"type": {"type": "string", "enum": ["repeater", "companion", "room_server", "client"], "description": "Filter by contact type"},
|
|
"status": {"type": "string", "enum": ["active", "stale", "expired"]},
|
|
"zero_hop": {"type": "boolean", "description": "Only direct RF contacts"}
|
|
},
|
|
"response": {
|
|
"neighbors": [{
|
|
"hash": "string",
|
|
"name": "string",
|
|
"contact_type": "string",
|
|
"last_seen": "integer - Unix timestamp",
|
|
"advert_count": "integer",
|
|
"is_zero_hop": "boolean",
|
|
"avg_rssi": "integer or null",
|
|
"avg_snr": "number or null",
|
|
"location": {"lat": "number", "lon": "number"}
|
|
}],
|
|
"counts": {
|
|
"total": "integer",
|
|
"repeaters": "integer",
|
|
"companions": "integer",
|
|
"room_servers": "integer",
|
|
"zero_hop": "integer"
|
|
}
|
|
},
|
|
"status": "PARTIAL - need to add zero_hop tracking",
|
|
"zero_hop_logic": "A neighbor is zero-hop if we received ADVERT packets from them with path_len=0 (DIRECT route) or path_len<=1 (FLOOD route)"
|
|
},
|
|
|
|
"DELETE /api/neighbors/{hash}": {
|
|
"description": "Hide neighbor from display",
|
|
"status": "EXISTS as DELETE /api/advert/:id"
|
|
},
|
|
|
|
"POST /api/neighbors/{hash}/ping": {
|
|
"description": "Ping specific neighbor",
|
|
"response": {
|
|
"success": "boolean",
|
|
"rtt_ms": "number",
|
|
"rssi": "integer",
|
|
"snr": "number"
|
|
},
|
|
"status": "EXISTS as ping_neighbor"
|
|
}
|
|
},
|
|
|
|
"phase_4_timeseries": {
|
|
"description": "Historical data for charts - single flexible endpoint",
|
|
|
|
"GET /api/timeseries": {
|
|
"description": "Time-bucketed metrics for charting",
|
|
"params": {
|
|
"metric": {
|
|
"type": "string",
|
|
"required": true,
|
|
"enum": ["rx", "tx", "forwarded", "dropped", "noise_floor"],
|
|
"description": "Which metric to retrieve"
|
|
},
|
|
"start": {"type": "integer", "required": true, "description": "Unix timestamp"},
|
|
"end": {"type": "integer", "required": true, "description": "Unix timestamp"},
|
|
"resolution": {
|
|
"type": "string",
|
|
"default": "auto",
|
|
"enum": ["auto", "1m", "5m", "1h", "1d"],
|
|
"description": "Bucket size"
|
|
}
|
|
},
|
|
"response": {
|
|
"metric": "string",
|
|
"resolution": "string - actual resolution used",
|
|
"buckets": [{"ts": "integer", "value": "number"}],
|
|
"summary": {
|
|
"total": "number",
|
|
"avg": "number",
|
|
"min": "number",
|
|
"max": "number"
|
|
}
|
|
},
|
|
"auto_resolution_logic": {
|
|
"under_1_hour": "1m buckets",
|
|
"under_24_hours": "5m buckets",
|
|
"under_7_days": "1h buckets",
|
|
"over_7_days": "1d buckets"
|
|
},
|
|
"status": "NEW - replaces multiple chart endpoints"
|
|
},
|
|
|
|
"GET /api/stats/bucketed": {
|
|
"description": "Pre-bucketed packet statistics for dashboard charts",
|
|
"params": {
|
|
"hours": {"type": "integer", "default": 24, "max": 168, "description": "Hours of history to return"},
|
|
"bucket_minutes": {"type": "integer", "default": 60, "enum": [5, 15, 60], "description": "Bucket size in minutes"}
|
|
},
|
|
"response": {
|
|
"buckets": [{
|
|
"timestamp": "integer - bucket start Unix timestamp",
|
|
"rx_count": "integer",
|
|
"tx_count": "integer",
|
|
"forwarded_count": "integer",
|
|
"dropped_count": "integer"
|
|
}],
|
|
"totals": {
|
|
"rx": "integer",
|
|
"tx": "integer",
|
|
"forwarded": "integer",
|
|
"dropped": "integer"
|
|
}
|
|
},
|
|
"status": "NEW - for dashboard packet type charts",
|
|
"note": "Alternative to generic /api/timeseries for simpler dashboard use case"
|
|
}
|
|
},
|
|
|
|
"phase_5_actions": {
|
|
"description": "Operational commands",
|
|
|
|
"POST /api/mode": {
|
|
"description": "Set operating mode",
|
|
"request": {"mode": "forward|monitor"},
|
|
"status": "EXISTS as set_mode"
|
|
},
|
|
|
|
"POST /api/duty_cycle": {
|
|
"description": "Enable/disable duty cycle",
|
|
"request": {"enabled": "boolean"},
|
|
"status": "EXISTS as set_duty_cycle"
|
|
},
|
|
|
|
"POST /api/radio": {
|
|
"description": "Update radio configuration",
|
|
"request": {
|
|
"frequency_hz": "integer (optional)",
|
|
"tx_power_dbm": "integer (optional)",
|
|
"spreading_factor": "integer (optional)",
|
|
"bandwidth_hz": "integer (optional)"
|
|
},
|
|
"response": {
|
|
"applied": "array - fields applied immediately",
|
|
"pending_restart": "array - fields requiring restart",
|
|
"warnings": "array of strings"
|
|
},
|
|
"status": "EXISTS as update_radio_config"
|
|
},
|
|
|
|
"POST /api/restart": {
|
|
"description": "Restart the repeater service",
|
|
"status": "EXISTS as restart_service"
|
|
},
|
|
|
|
"GET /api/radio_presets": {
|
|
"description": "Available radio configuration presets",
|
|
"response": {
|
|
"presets": [{
|
|
"name": "string - preset identifier",
|
|
"description": "string - human readable description",
|
|
"frequency_hz": "integer",
|
|
"tx_power_dbm": "integer",
|
|
"spreading_factor": "integer",
|
|
"bandwidth_hz": "integer",
|
|
"coding_rate": "integer"
|
|
}]
|
|
},
|
|
"status": "EXISTS"
|
|
},
|
|
|
|
"POST /api/log_level": {
|
|
"description": "Set system logging level",
|
|
"request": {"level": "string - DEBUG|INFO|WARNING|ERROR"},
|
|
"response": {"success": "boolean", "level": "string - applied level"},
|
|
"status": "EXISTS as set_log_level"
|
|
},
|
|
|
|
"POST /api/flood_policy": {
|
|
"description": "Set global flood routing policy",
|
|
"request": {"allow_all": "boolean - if true, forward all floods; if false, apply filtering rules"},
|
|
"response": {"success": "boolean"},
|
|
"status": "EXISTS as global_flood_policy"
|
|
},
|
|
|
|
"GET /api/duty_cycle": {
|
|
"description": "Get current duty cycle configuration",
|
|
"response": {
|
|
"enabled": "boolean",
|
|
"max_tx_percent": "number - maximum TX duty cycle percentage",
|
|
"window_seconds": "integer - measurement window duration",
|
|
"current_usage_percent": "number - current TX usage in window"
|
|
},
|
|
"status": "NEW - extends existing set_duty_cycle"
|
|
},
|
|
|
|
"POST /api/duty_cycle": {
|
|
"description": "Update duty cycle configuration",
|
|
"request": {
|
|
"enabled": {"type": "boolean", "description": "Enable/disable duty cycle limiting"},
|
|
"max_tx_percent": {"type": "number", "description": "Maximum TX percentage (optional)"},
|
|
"window_seconds": {"type": "integer", "description": "Measurement window (optional)"}
|
|
},
|
|
"response": {"success": "boolean"},
|
|
"status": "EXISTS as set_duty_cycle - expanded request body"
|
|
}
|
|
},
|
|
|
|
"phase_6_identity": {
|
|
"description": "Multi-identity and ACL management",
|
|
"status": "EXISTS - no changes needed",
|
|
"endpoints": [
|
|
"GET /api/identities",
|
|
"POST /api/identities",
|
|
"GET /api/identities/{name}",
|
|
"PUT /api/identities/{name}",
|
|
"DELETE /api/identities/{name}",
|
|
"GET /api/acl",
|
|
"GET /api/acl/clients",
|
|
"GET /api/acl/stats",
|
|
"DELETE /api/acl/clients/{public_key}"
|
|
],
|
|
|
|
"POST /api/identities/{name}/advert": {
|
|
"description": "Send ADVERT for a specific identity (e.g., room server)",
|
|
"response": {"success": "boolean"},
|
|
"status": "EXISTS as send_room_server_advert - generalized"
|
|
}
|
|
},
|
|
|
|
"phase_6b_transport_keys": {
|
|
"description": "Transport key management for encrypted mesh communication",
|
|
"status": "EXISTS - no changes needed",
|
|
|
|
"GET /api/transport_keys": {
|
|
"description": "List all transport keys",
|
|
"response": {
|
|
"keys": [{
|
|
"id": "string - unique key identifier",
|
|
"name": "string - human readable name",
|
|
"created": "integer - Unix timestamp",
|
|
"last_used": "integer or null - Unix timestamp"
|
|
}]
|
|
}
|
|
},
|
|
|
|
"POST /api/transport_keys": {
|
|
"description": "Create a new transport key",
|
|
"request": {
|
|
"name": "string - human readable name"
|
|
},
|
|
"response": {
|
|
"id": "string - generated key ID",
|
|
"key": "string - the actual key (only returned on creation)"
|
|
}
|
|
},
|
|
|
|
"GET /api/transport_keys/{id}": {
|
|
"description": "Get transport key details (not the key itself)",
|
|
"response": {
|
|
"id": "string",
|
|
"name": "string",
|
|
"created": "integer",
|
|
"last_used": "integer or null"
|
|
}
|
|
},
|
|
|
|
"PUT /api/transport_keys/{id}": {
|
|
"description": "Update transport key metadata",
|
|
"request": {"name": "string"},
|
|
"response": {"success": "boolean"}
|
|
},
|
|
|
|
"DELETE /api/transport_keys/{id}": {
|
|
"description": "Delete a transport key",
|
|
"response": {"success": "boolean"}
|
|
}
|
|
},
|
|
|
|
"phase_6c_api_tokens": {
|
|
"description": "API token management for authentication",
|
|
"status": "EXISTS - no changes needed",
|
|
|
|
"GET /api/tokens": {
|
|
"description": "List all API tokens",
|
|
"response": {
|
|
"tokens": [{
|
|
"id": "string - token identifier",
|
|
"name": "string - human readable name",
|
|
"created": "integer - Unix timestamp",
|
|
"last_used": "integer or null",
|
|
"expires": "integer or null - Unix timestamp if expiring"
|
|
}]
|
|
}
|
|
},
|
|
|
|
"POST /api/tokens": {
|
|
"description": "Create a new API token",
|
|
"request": {
|
|
"name": "string - human readable name",
|
|
"expires_days": "integer or null - days until expiration (null = never)"
|
|
},
|
|
"response": {
|
|
"id": "string",
|
|
"token": "string - the actual token (only returned on creation)"
|
|
}
|
|
},
|
|
|
|
"DELETE /api/tokens/{id}": {
|
|
"description": "Revoke an API token",
|
|
"response": {"success": "boolean"}
|
|
}
|
|
},
|
|
|
|
"phase_7_rooms": {
|
|
"description": "Room server message management",
|
|
"status": "EXISTS - no changes needed",
|
|
"endpoints": [
|
|
"GET /api/rooms",
|
|
"GET /api/rooms/{name}/messages",
|
|
"POST /api/rooms/{name}/messages",
|
|
"DELETE /api/rooms/{name}/messages/{id}",
|
|
"DELETE /api/rooms/{name}/messages"
|
|
],
|
|
|
|
"GET /api/rooms/{name}/stats": {
|
|
"description": "Room statistics",
|
|
"response": {
|
|
"message_count": "integer",
|
|
"client_count": "integer",
|
|
"last_activity": "integer - Unix timestamp"
|
|
},
|
|
"status": "EXISTS as room_stats"
|
|
},
|
|
|
|
"GET /api/rooms/{name}/clients": {
|
|
"description": "List clients synced to a room",
|
|
"response": {
|
|
"clients": [{
|
|
"hash": "string",
|
|
"name": "string",
|
|
"last_sync": "integer - Unix timestamp"
|
|
}]
|
|
},
|
|
"status": "EXISTS as room_clients"
|
|
}
|
|
},
|
|
|
|
"phase_8_frontend": {
|
|
"description": "Frontend/web UI configuration - pymc_console specific",
|
|
"status": "EXISTS - pymc_console overlay features",
|
|
|
|
"GET /api/web_config": {
|
|
"description": "Get current web UI configuration",
|
|
"response": {
|
|
"active_frontend": "string - 'pymc_console' or 'default'",
|
|
"pymc_console_installed": "boolean",
|
|
"default_frontend_installed": "boolean",
|
|
"web_path": "string - current static file path"
|
|
}
|
|
},
|
|
|
|
"POST /api/web_config": {
|
|
"description": "Switch active frontend",
|
|
"request": {
|
|
"frontend": "string - 'pymc_console' or 'default'"
|
|
},
|
|
"response": {
|
|
"success": "boolean",
|
|
"restart_required": "boolean"
|
|
},
|
|
"status": "EXISTS as update_web_config"
|
|
},
|
|
|
|
"GET /api/web_config/check": {
|
|
"description": "Check frontend installation status",
|
|
"response": {
|
|
"pymc_console": {"installed": "boolean", "version": "string or null"},
|
|
"default": {"installed": "boolean"}
|
|
},
|
|
"status": "EXISTS as check_pymc_console + check_default_frontend"
|
|
}
|
|
}
|
|
},
|
|
|
|
"database_schema": {
|
|
"description": "Minimal schema additions. Most tables already exist.",
|
|
|
|
"existing_tables": {
|
|
"packets": "Already exists - stores all packet data",
|
|
"adverts": "Already exists - stores neighbor/contact info"
|
|
},
|
|
|
|
"new_tables": {
|
|
"zero_hop_contacts": {
|
|
"description": "Track direct RF contacts for zero-hop detection",
|
|
"columns": {
|
|
"node_hash": "TEXT PRIMARY KEY",
|
|
"packet_count": "INTEGER - number of zero-hop packets received",
|
|
"rssi_sum": "INTEGER - for calculating average",
|
|
"snr_sum": "REAL - for calculating average",
|
|
"last_seen": "INTEGER - Unix timestamp"
|
|
},
|
|
"sql": "CREATE TABLE IF NOT EXISTS zero_hop_contacts (node_hash TEXT PRIMARY KEY, packet_count INTEGER DEFAULT 0, rssi_sum INTEGER DEFAULT 0, snr_sum REAL DEFAULT 0, last_seen INTEGER);"
|
|
},
|
|
|
|
"stats_buckets": {
|
|
"description": "Pre-aggregated time buckets for efficient chart queries",
|
|
"columns": {
|
|
"bucket_ts": "INTEGER - bucket start timestamp",
|
|
"resolution": "TEXT - '1m', '5m', '1h', '1d'",
|
|
"rx_count": "INTEGER",
|
|
"tx_count": "INTEGER",
|
|
"forwarded_count": "INTEGER",
|
|
"dropped_count": "INTEGER",
|
|
"noise_floor_avg": "REAL",
|
|
"noise_floor_min": "REAL",
|
|
"noise_floor_max": "REAL"
|
|
},
|
|
"sql": "CREATE TABLE IF NOT EXISTS stats_buckets (bucket_ts INTEGER, resolution TEXT, rx_count INTEGER DEFAULT 0, tx_count INTEGER DEFAULT 0, forwarded_count INTEGER DEFAULT 0, dropped_count INTEGER DEFAULT 0, noise_floor_avg REAL, noise_floor_min REAL, noise_floor_max REAL, PRIMARY KEY (bucket_ts, resolution));",
|
|
"index": "CREATE INDEX IF NOT EXISTS idx_stats_resolution ON stats_buckets(resolution, bucket_ts DESC);"
|
|
},
|
|
|
|
"noise_floor_samples": {
|
|
"description": "Raw noise floor readings for detailed analysis",
|
|
"columns": {
|
|
"id": "INTEGER PRIMARY KEY AUTOINCREMENT",
|
|
"timestamp": "INTEGER",
|
|
"value_dbm": "REAL"
|
|
},
|
|
"sql": "CREATE TABLE IF NOT EXISTS noise_floor_samples (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER, value_dbm REAL);",
|
|
"index": "CREATE INDEX IF NOT EXISTS idx_noise_ts ON noise_floor_samples(timestamp DESC);",
|
|
"retention": "Keep 24 hours, prune older"
|
|
}
|
|
}
|
|
},
|
|
|
|
"background_tasks": {
|
|
"description": "Periodic tasks needed for the system",
|
|
|
|
"stats_aggregation": {
|
|
"description": "Aggregate packet counts into time buckets",
|
|
"frequency": "Every 60 seconds",
|
|
"logic": [
|
|
"1. Count packets in last minute, upsert into stats_buckets with resolution='1m'",
|
|
"2. Every 5 minutes: roll up 1m buckets into 5m bucket",
|
|
"3. Every hour: roll up 5m buckets into 1h bucket",
|
|
"4. Every day at midnight: roll up 1h buckets into 1d bucket",
|
|
"5. Prune: delete 1m buckets older than 24h, 5m older than 7d"
|
|
],
|
|
"estimated_code": "~50 lines"
|
|
},
|
|
|
|
"zero_hop_detection": {
|
|
"description": "Update zero-hop contact tracking on packet receive",
|
|
"trigger": "On each ADVERT packet received",
|
|
"logic": [
|
|
"1. Check if packet is zero-hop (DIRECT route: path_len=0, FLOOD route: path_len<=1)",
|
|
"2. If zero-hop, upsert into zero_hop_contacts table",
|
|
"3. Increment packet_count, add to rssi_sum/snr_sum, update last_seen"
|
|
],
|
|
"estimated_code": "~20 lines"
|
|
},
|
|
|
|
"noise_floor_sampling": {
|
|
"description": "Already implemented in SX1262 wrapper",
|
|
"location": "sx1262_wrapper.py _sample_noise_floor()",
|
|
"change_needed": "Add insert into noise_floor_samples table, broadcast to WebSocket"
|
|
}
|
|
},
|
|
|
|
"implementation_priority": {
|
|
"sprint_1": {
|
|
"name": "WebSocket Radio Events + Hardware",
|
|
"effort": "1-2 days",
|
|
"tasks": [
|
|
"Add /radio WebSocket endpoint to CherryPy",
|
|
"Create broadcast function for connected clients",
|
|
"Hook SX1262 RX callback to push 'rx' events",
|
|
"Hook TX completion to push 'tx' events",
|
|
"Hook noise floor sampling to push 'noise' events",
|
|
"Add 'hardware' event with CPU/memory/disk/temp (every 10s)",
|
|
"Add 'send_advert' command handler"
|
|
]
|
|
},
|
|
"sprint_2": {
|
|
"name": "REST Core Cleanup",
|
|
"effort": "1-2 days",
|
|
"tasks": [
|
|
"Ensure /api/stats returns all current state fields",
|
|
"Add /api/logs endpoint with level filter",
|
|
"Add proper before/after/offset pagination to /api/packets",
|
|
"Add src_hash, dst_hash, path_contains filters to /api/packets",
|
|
"Add /api/neighbors endpoint with type and zero_hop filters"
|
|
]
|
|
},
|
|
"sprint_3": {
|
|
"name": "Time Series + Actions",
|
|
"effort": "2-3 days",
|
|
"tasks": [
|
|
"Create stats_buckets and noise_floor_samples tables",
|
|
"Implement aggregation background task",
|
|
"Implement /api/timeseries endpoint",
|
|
"Implement /api/stats/bucketed endpoint",
|
|
"Add /api/radio_presets endpoint",
|
|
"Add /api/log_level POST endpoint",
|
|
"Add /api/flood_policy POST endpoint",
|
|
"Expand /api/duty_cycle with GET and full config POST"
|
|
]
|
|
},
|
|
"sprint_4": {
|
|
"name": "Identity + Keys + Tokens",
|
|
"effort": "1-2 days",
|
|
"tasks": [
|
|
"Add POST /api/identities/{name}/advert for room server adverts",
|
|
"Verify transport key CRUD endpoints",
|
|
"Verify API token CRUD endpoints"
|
|
],
|
|
"note": "Most of these already exist - just verify and document"
|
|
},
|
|
"sprint_5": {
|
|
"name": "Rooms + Frontend Config",
|
|
"effort": "1 day",
|
|
"tasks": [
|
|
"Verify room stats and clients endpoints",
|
|
"Add /api/web_config GET/POST endpoints",
|
|
"Add /api/web_config/check endpoint"
|
|
],
|
|
"note": "Frontend config is pymc_console overlay specific"
|
|
}
|
|
},
|
|
|
|
"what_we_are_not_doing": {
|
|
"description": "Complexity removed from previous design to keep it simple",
|
|
"removed": [
|
|
"Subscription channels - WebSocket just streams everything",
|
|
"Message sequencing/replay - miss events? use REST for history",
|
|
"Server-side topology computation - keep client-side, it works",
|
|
"Server-side prefix disambiguation - same, keep client-side",
|
|
"Sparkline pre-computation - client computes from time series",
|
|
"Encoded pagination cursors - simple timestamps work fine",
|
|
"Prometheus metrics export - add later if needed",
|
|
"Federation/multi-node support - premature",
|
|
"Machine learning hooks - premature"
|
|
]
|
|
},
|
|
|
|
"packet_schema": {
|
|
"description": "Reference: MeshCore packet structure for context",
|
|
"fields": {
|
|
"packet_hash": "string - unique identifier",
|
|
"timestamp": "integer - Unix timestamp",
|
|
"type": "integer - payload type (0-15)",
|
|
"route": "integer - route type (0-3)",
|
|
"transmitted": "boolean - true if TX, false if RX",
|
|
"src_hash": "string - source node hash",
|
|
"dst_hash": "string - destination node hash (if direct)",
|
|
"rssi": "integer - RSSI in dBm (RX only)",
|
|
"snr": "number - SNR in dB (RX only)",
|
|
"path": "array of strings - hop prefixes",
|
|
"payload": "bytes - raw payload"
|
|
},
|
|
"payload_types": {
|
|
"0": "REQ",
|
|
"1": "RESPONSE",
|
|
"2": "TXT_MSG",
|
|
"3": "ACK",
|
|
"4": "ADVERT",
|
|
"5": "GRP_TXT",
|
|
"6": "GRP_DATA",
|
|
"7": "ANON_REQ",
|
|
"8": "PATH",
|
|
"9": "TRACE",
|
|
"10": "MULTIPART",
|
|
"15": "RAW_CUSTOM"
|
|
},
|
|
"route_types": {
|
|
"0": "T_FLOOD (transport flood)",
|
|
"1": "FLOOD",
|
|
"2": "DIRECT",
|
|
"3": "T_DIRECT (transport direct)"
|
|
}
|
|
}
|
|
}
|