Add CRC error tracking and API endpoints for error count and history

- Create a new table for storing CRC errors in SQLite.
- Implement methods to store and retrieve CRC error counts and history.
- Update StorageCollector to record CRC errors and expose relevant methods.
- Enhance RepeaterHandler to track and record CRC error deltas from the radio hardware.
- Add API endpoints to fetch CRC error count and history.
This commit is contained in:
Lloyd
2026-03-02 12:36:08 +00:00
parent c2f57c3d0f
commit 4a05e20172
4 changed files with 131 additions and 2 deletions
+62 -2
View File
@@ -76,6 +76,14 @@ class SQLiteHandler:
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS crc_errors (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp REAL NOT NULL,
count INTEGER NOT NULL DEFAULT 1
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS transport_keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -107,6 +115,7 @@ class SQLiteHandler:
conn.execute("CREATE INDEX IF NOT EXISTS idx_adverts_timestamp ON adverts(timestamp)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_adverts_pubkey ON adverts(pubkey)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_noise_timestamp ON noise_floor(timestamp)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_crc_errors_timestamp ON crc_errors(timestamp)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_transport_keys_name ON transport_keys(name)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_transport_keys_parent ON transport_keys(parent_id)")
@@ -478,6 +487,54 @@ class SQLiteHandler:
except Exception as e:
logger.error(f"Failed to store noise floor in SQLite: {e}")
def store_crc_errors(self, record: dict):
"""Store a CRC error batch (delta count since last poll)."""
try:
with sqlite3.connect(self.sqlite_path) as conn:
conn.execute("""
INSERT INTO crc_errors (timestamp, count)
VALUES (?, ?)
""", (
record.get("timestamp", time.time()),
record.get("count", 1)
))
except Exception as e:
logger.error(f"Failed to store CRC errors in SQLite: {e}")
def get_crc_error_count(self, hours: int = 24) -> int:
"""Return total CRC errors within the given time window."""
try:
cutoff = time.time() - (hours * 3600)
with sqlite3.connect(self.sqlite_path) as conn:
row = conn.execute(
"SELECT COALESCE(SUM(count), 0) FROM crc_errors WHERE timestamp > ?",
(cutoff,)
).fetchone()
return row[0] if row else 0
except Exception as e:
logger.error(f"Failed to get CRC error count: {e}")
return 0
def get_crc_error_history(self, hours: int = 24, limit: int = None) -> list:
"""Return CRC error records within the given time window (chronological)."""
try:
cutoff = time.time() - (hours * 3600)
with sqlite3.connect(self.sqlite_path) as conn:
conn.row_factory = sqlite3.Row
query = """
SELECT timestamp, count
FROM crc_errors
WHERE timestamp > ?
ORDER BY timestamp DESC
"""
if limit:
query += f" LIMIT {int(limit)}"
rows = conn.execute(query, (cutoff,)).fetchall()
return [{"timestamp": r["timestamp"], "count": r["count"]} for r in reversed(rows)]
except Exception as e:
logger.error(f"Failed to get CRC error history: {e}")
return []
def get_packet_stats(self, hours: int = 24) -> dict:
try:
cutoff = time.time() - (hours * 3600)
@@ -841,10 +898,13 @@ class SQLiteHandler:
result = conn.execute("DELETE FROM noise_floor WHERE timestamp < ?", (cutoff,))
noise_deleted = result.rowcount
result = conn.execute("DELETE FROM crc_errors WHERE timestamp < ?", (cutoff,))
crc_deleted = result.rowcount
conn.commit()
if packets_deleted > 0 or adverts_deleted > 0 or noise_deleted > 0:
logger.info(f"Cleaned up {packets_deleted} old packets, {adverts_deleted} old adverts, {noise_deleted} old noise measurements")
if packets_deleted > 0 or adverts_deleted > 0 or noise_deleted > 0 or crc_deleted > 0:
logger.info(f"Cleaned up {packets_deleted} old packets, {adverts_deleted} old adverts, {noise_deleted} old noise measurements, {crc_deleted} old CRC error records")
except Exception as e:
logger.error(f"Failed to cleanup old data: {e}")
@@ -209,6 +209,18 @@ class StorageCollector:
self.sqlite_handler.store_noise_floor(noise_record)
self.mqtt_handler.publish(noise_record, "noise_floor")
def record_crc_errors(self, count: int):
"""Record a batch of CRC errors detected since last poll."""
crc_record = {"timestamp": time.time(), "count": count}
self.sqlite_handler.store_crc_errors(crc_record)
self.mqtt_handler.publish(crc_record, "crc_errors")
def get_crc_error_count(self, hours: int = 24) -> int:
return self.sqlite_handler.get_crc_error_count(hours)
def get_crc_error_history(self, hours: int = 24, limit: int = None) -> list:
return self.sqlite_handler.get_crc_error_history(hours, limit)
def get_packet_stats(self, hours: int = 24) -> dict:
return self.sqlite_handler.get_packet_stats(hours)
+23
View File
@@ -95,6 +95,7 @@ class RepeaterHandler(BaseHandler):
self.last_noise_measurement = time.time()
self.noise_floor_interval = NOISE_FLOOR_INTERVAL # 30 seconds
self._background_task = None
self._last_crc_error_count = 0 # Track radio counter for delta persistence
# Cache transport keys for efficient lookup
self._transport_keys_cache = None
@@ -708,6 +709,10 @@ class RepeaterHandler(BaseHandler):
# Get current noise floor from radio
noise_floor_dbm = self.get_noise_floor()
# Get CRC error count from radio hardware
radio = self.dispatcher.radio if self.dispatcher else None
crc_error_count = getattr(radio, "crc_error_count", 0) if radio else 0
# Get neighbors from database
neighbors = self.storage.get_neighbors() if self.storage else {}
@@ -724,6 +729,7 @@ class RepeaterHandler(BaseHandler):
"neighbors": neighbors,
"uptime_seconds": uptime_seconds,
"noise_floor_dbm": noise_floor_dbm,
"crc_error_count": crc_error_count,
# Add configuration data
"config": {
"node_name": repeater_config.get("node_name", "Unknown"),
@@ -768,6 +774,7 @@ class RepeaterHandler(BaseHandler):
# Check noise floor recording (every 30 seconds)
if current_time - self.last_noise_measurement >= self.noise_floor_interval:
await self._record_noise_floor_async()
await self._record_crc_errors_async()
self.last_noise_measurement = current_time
# Check advert sending (every N hours)
@@ -803,6 +810,22 @@ class RepeaterHandler(BaseHandler):
except Exception as e:
logger.error(f"Error recording noise floor: {e}")
async def _record_crc_errors_async(self):
"""Persist CRC error delta from the radio hardware counter."""
if not self.storage:
return
try:
radio = self.dispatcher.radio if self.dispatcher else None
current = getattr(radio, "crc_error_count", 0) if radio else 0
delta = current - self._last_crc_error_count
if delta > 0:
self.storage.record_crc_errors(delta)
logger.debug(f"Recorded {delta} CRC errors (total: {current})")
self._last_crc_error_count = current
except Exception as e:
logger.error(f"Error recording CRC errors: {e}")
async def _send_periodic_advert_async(self):
logger.info(
f"Periodic advert timer triggered (interval: {self.send_advert_interval_hours}h)"
+34
View File
@@ -1420,6 +1420,40 @@ class APIEndpoints:
logger.error(f"Error fetching noise floor chart data: {e}")
return self._error(e)
@cherrypy.expose
@cherrypy.tools.json_out()
def crc_error_count(self, hours: int = 24):
"""Return total CRC errors within the given time window."""
try:
storage = self._get_storage()
hours = int(hours)
count = storage.get_crc_error_count(hours=hours)
return self._success({
"crc_error_count": count,
"hours": hours
})
except Exception as e:
logger.error(f"Error fetching CRC error count: {e}")
return self._error(e)
@cherrypy.expose
@cherrypy.tools.json_out()
def crc_error_history(self, hours: int = 24, limit: int = None):
"""Return CRC error records within the given time window."""
try:
storage = self._get_storage()
hours = int(hours)
limit = int(limit) if limit else None
history = storage.get_crc_error_history(hours=hours, limit=limit)
return self._success({
"history": history,
"hours": hours,
"count": len(history)
})
except Exception as e:
logger.error(f"Error fetching CRC error history: {e}")
return self._error(e)
@cherrypy.expose
def cad_calibration_stream(self):
cherrypy.response.headers['Content-Type'] = 'text/event-stream'