mirror of
https://github.com/pyMC-dev/pyMC_Repeater.git
synced 2026-08-07 09:23:06 +02:00
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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user