Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 54 additions & 2 deletions repeater/data_acquisition/sqlite_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,14 @@ def _init_database(self):
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 TABLE IF NOT EXISTS crc_errors (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp REAL NOT NULL
)
""")
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)")

Expand Down Expand Up @@ -478,6 +486,47 @@ def store_noise_floor(self, record: dict):
except Exception as e:
logger.error(f"Failed to store noise floor in SQLite: {e}")

def store_crc_error(self, timestamp: float):
try:
with sqlite3.connect(self.sqlite_path) as conn:
conn.execute(
"INSERT INTO crc_errors (timestamp) VALUES (?)",
(timestamp,)
)
except Exception as e:
logger.error(f"Failed to store CRC error in SQLite: {e}")

def get_crc_error_count(self, hours: int = 24) -> dict:
try:
cutoff = time.time() - (hours * 3600)

with sqlite3.connect(self.sqlite_path) as conn:
count = conn.execute(
"SELECT COUNT(*) FROM crc_errors WHERE timestamp > ?",
(cutoff,)
).fetchone()[0]

oldest = conn.execute(
"SELECT MIN(timestamp) FROM crc_errors WHERE timestamp > ?",
(cutoff,)
).fetchone()[0]

newest = conn.execute(
"SELECT MAX(timestamp) FROM crc_errors WHERE timestamp > ?",
(cutoff,)
).fetchone()[0]

return {
"crc_error_count": count,
"hours": hours,
"oldest_event": oldest,
"newest_event": newest
}

except Exception as e:
logger.error(f"Failed to get CRC error count: {e}")
return {"crc_error_count": 0, "hours": hours, "oldest_event": None, "newest_event": None}

def get_packet_stats(self, hours: int = 24) -> dict:
try:
cutoff = time.time() - (hours * 3600)
Expand Down Expand Up @@ -841,10 +890,13 @@ def cleanup_old_data(self, days: int = 7):
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 errors")

except Exception as e:
logger.error(f"Failed to cleanup old data: {e}")
Expand Down
6 changes: 6 additions & 0 deletions repeater/data_acquisition/storage_collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,12 @@ def record_noise_floor(self, noise_floor_dbm: float):
self.sqlite_handler.store_noise_floor(noise_record)
self.mqtt_handler.publish(noise_record, "noise_floor")

def record_crc_error(self, timestamp: float = None):
self.sqlite_handler.store_crc_error(timestamp or time.time())

def get_crc_error_count(self, hours: int = 24) -> dict:
return self.sqlite_handler.get_crc_error_count(hours)

def get_packet_stats(self, hours: int = 24) -> dict:
return self.sqlite_handler.get_packet_stats(hours)

Expand Down
4 changes: 3 additions & 1 deletion repeater/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from repeater.config import get_radio_for_board, load_config
from repeater.config_manager import ConfigManager
from repeater.engine import RepeaterHandler
from repeater.web.http_server import HTTPStatsServer, _log_buffer
from repeater.web.http_server import HTTPStatsServer, _log_buffer, _crc_tracker
from repeater.handler_helpers import TraceHelper, DiscoveryHelper, AdvertHelper, LoginHelper, TextHelper, PathHelper, ProtocolRequestHelper
from repeater.packet_router import PacketRouter
from repeater.identity_manager import IdentityManager
Expand Down Expand Up @@ -47,6 +47,8 @@ def __init__(self, config: dict, radio=None):
root_logger = logging.getLogger()
_log_buffer.setLevel(getattr(logging, log_level))
root_logger.addHandler(_log_buffer)
_crc_tracker.setLevel(logging.WARNING)
root_logger.addHandler(_crc_tracker)

async def initialize(self):

Expand Down
8 changes: 5 additions & 3 deletions repeater/web/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from .http_server import HTTPStatsServer, StatsApp, LogBuffer, _log_buffer
from .http_server import HTTPStatsServer, StatsApp, LogBuffer, _log_buffer, CRCErrorTracker, _crc_tracker
from .api_endpoints import APIEndpoints
from .cad_calibration_engine import CADCalibrationEngine

__all__ = [
'HTTPStatsServer',
'StatsApp',
'LogBuffer',
'CRCErrorTracker',
'APIEndpoints',
'CADCalibrationEngine',
'_log_buffer'
]
'_log_buffer',
'_crc_tracker'
]
16 changes: 16 additions & 0 deletions repeater/web/api_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@
# GET /api/noise_floor_stats?hours=24 - Get noise floor statistics
# GET /api/noise_floor_chart_data?hours=24 - Get noise floor chart data

# Radio Health
# GET /api/crc_count?hours=24 - Get CRC error count for time period

# CAD Calibration
# POST /api/cad_calibration_start {"samples": 8, "delay": 100} - Start CAD calibration
# POST /api/cad_calibration_stop - Stop CAD calibration
Expand Down Expand Up @@ -1387,6 +1390,19 @@ def noise_floor_chart_data(self, hours: int = 24):
logger.error(f"Error fetching noise floor chart data: {e}")
return self._error(e)

@cherrypy.expose
@cherrypy.tools.json_out()
def crc_count(self, hours: int = 24):
"""Get CRC error count for the specified time period."""
try:
storage = self._get_storage()
hours = int(hours)
stats = storage.get_crc_error_count(hours=hours)
return self._success(stats)
except Exception as e:
logger.error(f"Error fetching CRC error count: {e}")
return self._error(e)

@cherrypy.expose
def cad_calibration_stream(self):
cherrypy.response.headers['Content-Type'] = 'text/event-stream'
Expand Down
32 changes: 32 additions & 0 deletions repeater/web/http_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,30 @@ def emit(self, record):
_log_buffer = LogBuffer(max_lines=100)


class CRCErrorTracker(logging.Handler):
"""Logging handler that detects CRC errors from SX1262_wrapper and persists them to SQLite."""

def __init__(self):
super().__init__()
self._storage = None

def set_storage(self, storage_collector):
"""Attach storage collector for persistent writes. Called during server init."""
self._storage = storage_collector

def emit(self, record):
try:
if record.name == "SX1262_wrapper" and "CRC error detected" in record.getMessage():
if self._storage:
self._storage.record_crc_error(record.created)
except Exception:
self.handleError(record)


# Global CRC error tracker instance
_crc_tracker = CRCErrorTracker()


class DocEndpoint:
"""Simple wrapper to serve API docs at /doc"""

Expand Down Expand Up @@ -191,6 +215,14 @@ def __init__(
stats_getter, node_name, pub_key, send_advert_func, config, event_loop, daemon_instance, config_path
)

# Connect CRC error tracker to storage for persistent writes
try:
if daemon_instance and hasattr(daemon_instance, 'repeater_handler') and daemon_instance.repeater_handler:
_crc_tracker.set_storage(daemon_instance.repeater_handler.storage)
logger.info("CRC error tracker connected to storage")
except Exception as e:
logger.warning(f"Could not connect CRC error tracker to storage: {e}")

# Create auth endpoints (APIEndpoints has the config_manager)
self.auth_app = AuthEndpoints(self.config, self.jwt_handler, self.token_manager, self.app.api.config_manager)

Expand Down