mirror of
https://github.com/rightup/pyMC_Repeater.git
synced 2026-03-28 17:43:06 +01:00
feat: add noise floor monitoring with history, stats, and chart data endpoints
This commit is contained in:
@@ -20,6 +20,8 @@ from repeater.storage import StorageCollector
|
||||
|
||||
logger = logging.getLogger("RepeaterHandler")
|
||||
|
||||
NOISE_FLOOR_INTERVAL = 30.0 # seconds
|
||||
|
||||
|
||||
class RepeaterHandler(BaseHandler):
|
||||
|
||||
@@ -80,6 +82,12 @@ class RepeaterHandler(BaseHandler):
|
||||
logger.error(f"Failed to initialize StorageCollector: {e}")
|
||||
self.storage = None
|
||||
|
||||
# Initialize background timer tracking
|
||||
self.last_noise_measurement = time.time()
|
||||
self.noise_floor_interval = NOISE_FLOOR_INTERVAL # 30 seconds
|
||||
self._background_task = None
|
||||
self._start_background_tasks()
|
||||
|
||||
async def __call__(self, packet: Packet, metadata: Optional[dict] = None) -> None:
|
||||
|
||||
if metadata is None:
|
||||
@@ -88,9 +96,6 @@ class RepeaterHandler(BaseHandler):
|
||||
# Track incoming packet
|
||||
self.rx_count += 1
|
||||
|
||||
# Check if it's time to send a periodic advertisement
|
||||
await self._check_and_send_periodic_advert()
|
||||
|
||||
# Check if we're in monitor mode (receive only, no forwarding)
|
||||
mode = self.config.get("repeater", {}).get("mode", "forward")
|
||||
monitor_mode = mode == "monitor"
|
||||
@@ -565,32 +570,6 @@ class RepeaterHandler(BaseHandler):
|
||||
|
||||
asyncio.create_task(delayed_send())
|
||||
|
||||
async def _check_and_send_periodic_advert(self):
|
||||
|
||||
if self.send_advert_interval_hours <= 0 or not self.send_advert_func:
|
||||
return
|
||||
|
||||
current_time = time.time()
|
||||
interval_seconds = self.send_advert_interval_hours * 3600 # Convert hours to seconds
|
||||
time_since_last_advert = current_time - self.last_advert_time
|
||||
|
||||
# Check if interval has elapsed
|
||||
if time_since_last_advert >= interval_seconds:
|
||||
logger.info(
|
||||
f"Periodic advert interval elapsed ({time_since_last_advert:.0f}s >= "
|
||||
f"{interval_seconds:.0f}s). Sending advert..."
|
||||
)
|
||||
try:
|
||||
# Call the send_advert function
|
||||
success = await self.send_advert_func()
|
||||
if success:
|
||||
self.last_advert_time = current_time
|
||||
logger.info("Periodic advert sent successfully")
|
||||
else:
|
||||
logger.warning("Failed to send periodic advert")
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending periodic advert: {e}", exc_info=True)
|
||||
|
||||
def get_noise_floor(self) -> Optional[float]:
|
||||
try:
|
||||
radio = self.dispatcher.radio if self.dispatcher else None
|
||||
@@ -672,7 +651,73 @@ class RepeaterHandler(BaseHandler):
|
||||
stats.update(self.airtime_mgr.get_stats())
|
||||
return stats
|
||||
|
||||
def _start_background_tasks(self):
|
||||
if self._background_task is None:
|
||||
self._background_task = asyncio.create_task(self._background_timer_loop())
|
||||
logger.info("Background timer started for noise floor and adverts")
|
||||
|
||||
async def _background_timer_loop(self):
|
||||
try:
|
||||
while True:
|
||||
current_time = time.time()
|
||||
|
||||
# Check noise floor recording (every 30 seconds)
|
||||
if current_time - self.last_noise_measurement >= self.noise_floor_interval:
|
||||
await self._record_noise_floor_async()
|
||||
self.last_noise_measurement = current_time
|
||||
|
||||
# Check advert sending (every N hours)
|
||||
if self.send_advert_interval_hours > 0 and self.send_advert_func:
|
||||
interval_seconds = self.send_advert_interval_hours * 3600
|
||||
if current_time - self.last_advert_time >= interval_seconds:
|
||||
await self._send_periodic_advert_async()
|
||||
self.last_advert_time = current_time
|
||||
|
||||
# Sleep for 5 seconds before next check
|
||||
await asyncio.sleep(5.0)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.info("Background timer loop cancelled")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error in background timer loop: {e}")
|
||||
# Restart the timer after a delay
|
||||
await asyncio.sleep(30)
|
||||
self._background_task = asyncio.create_task(self._background_timer_loop())
|
||||
|
||||
async def _record_noise_floor_async(self):
|
||||
if not self.storage:
|
||||
return
|
||||
|
||||
try:
|
||||
noise_floor = self.get_noise_floor()
|
||||
if noise_floor is not None:
|
||||
self.storage.record_noise_floor(noise_floor)
|
||||
logger.debug(f"Recorded noise floor: {noise_floor} dBm")
|
||||
else:
|
||||
logger.debug("Unable to read noise floor from radio")
|
||||
except Exception as e:
|
||||
logger.error(f"Error recording noise floor: {e}")
|
||||
|
||||
async def _send_periodic_advert_async(self):
|
||||
logger.info(f"Periodic advert timer triggered (interval: {self.send_advert_interval_hours}h)")
|
||||
try:
|
||||
if self.send_advert_func:
|
||||
success = await self.send_advert_func()
|
||||
if success:
|
||||
logger.info("Periodic advert sent successfully")
|
||||
else:
|
||||
logger.warning("Failed to send periodic advert")
|
||||
else:
|
||||
logger.debug("No send_advert_func configured")
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending periodic advert: {e}")
|
||||
|
||||
def cleanup(self):
|
||||
if self._background_task and not self._background_task.done():
|
||||
self._background_task.cancel()
|
||||
logger.info("Background timer task cancelled")
|
||||
|
||||
if self.storage:
|
||||
try:
|
||||
self.storage.close()
|
||||
|
||||
@@ -90,6 +90,15 @@ class StorageCollector:
|
||||
)
|
||||
""")
|
||||
|
||||
# Noise floor measurements table
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS noise_floor (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
timestamp REAL NOT NULL,
|
||||
noise_floor_dbm REAL NOT NULL
|
||||
)
|
||||
""")
|
||||
|
||||
# Create indexes for performance
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_packets_timestamp ON packets(timestamp)")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_packets_type ON packets(type)")
|
||||
@@ -97,6 +106,7 @@ class StorageCollector:
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_packets_transmitted ON packets(transmitted)")
|
||||
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.commit()
|
||||
logger.info(f"SQLite database initialized: {self.sqlite_path}")
|
||||
@@ -122,6 +132,7 @@ class StorageCollector:
|
||||
"--step", "60", # 1-minute steps
|
||||
"--start", str(int(time.time() - 60)),
|
||||
|
||||
|
||||
# Data sources - Basic metrics
|
||||
"DS:rx_count:COUNTER:120:0:U", # Received packets
|
||||
"DS:tx_count:COUNTER:120:0:U", # Transmitted packets
|
||||
@@ -131,6 +142,7 @@ class StorageCollector:
|
||||
"DS:avg_length:GAUGE:120:0:256", # Average packet length
|
||||
"DS:avg_score:GAUGE:120:0:1", # Average packet score
|
||||
"DS:neighbor_count:GAUGE:120:0:U", # Number of neighbors
|
||||
"DS:noise_floor:GAUGE:120:-150:-50", # Noise floor in dBm
|
||||
|
||||
# Packet type counters (based on pyMC payload types)
|
||||
"DS:type_0:COUNTER:120:0:U", # Request (PAYLOAD_TYPE_REQ)
|
||||
@@ -200,11 +212,20 @@ class StorageCollector:
|
||||
self._store_advert_sqlite(advert_record)
|
||||
self._publish_mqtt(advert_record, "advert")
|
||||
|
||||
def record_noise_floor(self, noise_floor_dbm: float):
|
||||
"""Record noise floor measurement every 30 seconds"""
|
||||
noise_record = {
|
||||
"timestamp": time.time(),
|
||||
"noise_floor_dbm": noise_floor_dbm
|
||||
}
|
||||
self._store_noise_floor_sqlite(noise_record)
|
||||
self._update_rrd_noise_metrics(noise_record)
|
||||
self._publish_mqtt(noise_record, "noise_floor")
|
||||
|
||||
def _store_packet_sqlite(self, record: dict):
|
||||
|
||||
try:
|
||||
with sqlite3.connect(self.sqlite_path) as conn:
|
||||
# Ensure fields that are non-sqlite-bindable are serialized
|
||||
orig_path = record.get("original_path")
|
||||
fwd_path = record.get("forwarded_path")
|
||||
try:
|
||||
@@ -308,6 +329,20 @@ class StorageCollector:
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to store advert in SQLite: {e}")
|
||||
|
||||
def _store_noise_floor_sqlite(self, record: dict):
|
||||
|
||||
try:
|
||||
with sqlite3.connect(self.sqlite_path) as conn:
|
||||
conn.execute("""
|
||||
INSERT INTO noise_floor (timestamp, noise_floor_dbm)
|
||||
VALUES (?, ?)
|
||||
""", (
|
||||
record.get("timestamp", time.time()),
|
||||
record.get("noise_floor_dbm")
|
||||
))
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to store noise floor in SQLite: {e}")
|
||||
|
||||
def _update_rrd_packet_metrics(self, record: dict):
|
||||
if not RRDTOOL_AVAILABLE or not self.rrd_path.exists():
|
||||
return
|
||||
@@ -352,8 +387,35 @@ class StorageCollector:
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update RRD packet metrics: {e}")
|
||||
|
||||
def _update_rrd_noise_metrics(self, record: dict):
|
||||
|
||||
if not RRDTOOL_AVAILABLE or not self.rrd_path.exists():
|
||||
return
|
||||
|
||||
try:
|
||||
timestamp = int(record.get("timestamp", time.time()))
|
||||
noise_floor = record.get("noise_floor_dbm", "U")
|
||||
|
||||
# Skip if trying to update with old data
|
||||
try:
|
||||
info = rrdtool.info(str(self.rrd_path))
|
||||
last_update = int(info.get("last_update", timestamp - 60))
|
||||
if timestamp <= last_update:
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Update RRD with noise floor only, set other metrics to undefined
|
||||
# Format: timestamp:rx:tx:drop:rssi:snr:length:score:neighbors:noise_floor:type_0:type_1:...
|
||||
values = f"{timestamp}:0:0:0:U:U:U:U:U:{noise_floor}" + ":0" * 17 # 17 packet type counters
|
||||
|
||||
rrdtool.update(str(self.rrd_path), values)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update RRD noise metrics: {e}")
|
||||
|
||||
def _publish_mqtt(self, record: dict, record_type: str):
|
||||
"""Publish record to MQTT broker."""
|
||||
|
||||
if not self.mqtt_client:
|
||||
return
|
||||
|
||||
@@ -426,7 +488,7 @@ class StorageCollector:
|
||||
return {}
|
||||
|
||||
def get_recent_packets(self, limit: int = 100) -> list:
|
||||
"""Get recent packets with all fields for debugging/analysis."""
|
||||
|
||||
try:
|
||||
with sqlite3.connect(self.sqlite_path) as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
@@ -454,7 +516,7 @@ class StorageCollector:
|
||||
start_timestamp: Optional[float] = None,
|
||||
end_timestamp: Optional[float] = None,
|
||||
limit: int = 1000) -> list:
|
||||
"""Get packets filtered by type, route, and timestamp range."""
|
||||
|
||||
try:
|
||||
with sqlite3.connect(self.sqlite_path) as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
@@ -506,7 +568,7 @@ class StorageCollector:
|
||||
return []
|
||||
|
||||
def get_packet_by_hash(self, packet_hash: str) -> Optional[dict]:
|
||||
"""Get a specific packet by its hash."""
|
||||
|
||||
try:
|
||||
with sqlite3.connect(self.sqlite_path) as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
@@ -529,7 +591,7 @@ class StorageCollector:
|
||||
|
||||
def get_rrd_data(self, start_time: Optional[int] = None, end_time: Optional[int] = None,
|
||||
resolution: str = "average") -> Optional[dict]:
|
||||
"""Get RRD time series data including packet type statistics."""
|
||||
|
||||
if not RRDTOOL_AVAILABLE or not self.rrd_path.exists():
|
||||
return None
|
||||
|
||||
@@ -597,7 +659,7 @@ class StorageCollector:
|
||||
return None
|
||||
|
||||
def get_packet_type_stats(self, hours: int = 24) -> dict:
|
||||
"""Get packet type statistics for the specified time period."""
|
||||
|
||||
try:
|
||||
# Get RRD data for packet types
|
||||
end_time = int(time.time())
|
||||
@@ -652,7 +714,7 @@ class StorageCollector:
|
||||
return {"error": str(e)}
|
||||
|
||||
def get_neighbors(self) -> dict:
|
||||
"""Get all neighbors from the database formatted like the in-memory neighbors dict."""
|
||||
|
||||
try:
|
||||
with sqlite3.connect(self.sqlite_path) as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
@@ -706,16 +768,146 @@ class StorageCollector:
|
||||
result = conn.execute("DELETE FROM adverts WHERE timestamp < ?", (cutoff,))
|
||||
adverts_deleted = result.rowcount
|
||||
|
||||
# Clean old noise floor measurements
|
||||
result = conn.execute("DELETE FROM noise_floor WHERE timestamp < ?", (cutoff,))
|
||||
noise_deleted = result.rowcount
|
||||
|
||||
conn.commit()
|
||||
|
||||
if packets_deleted > 0 or adverts_deleted > 0:
|
||||
logger.info(f"Cleaned up {packets_deleted} old packets and {adverts_deleted} old adverts")
|
||||
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")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to cleanup old data: {e}")
|
||||
|
||||
def get_noise_floor_history(self, hours: int = 24) -> list:
|
||||
|
||||
try:
|
||||
cutoff = time.time() - (hours * 3600)
|
||||
|
||||
with sqlite3.connect(self.sqlite_path) as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
|
||||
measurements = conn.execute("""
|
||||
SELECT timestamp, noise_floor_dbm
|
||||
FROM noise_floor
|
||||
WHERE timestamp > ?
|
||||
ORDER BY timestamp ASC
|
||||
""", (cutoff,)).fetchall()
|
||||
|
||||
return [{"timestamp": row["timestamp"], "noise_floor_dbm": row["noise_floor_dbm"]}
|
||||
for row in measurements]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get noise floor history: {e}")
|
||||
return []
|
||||
|
||||
def get_noise_floor_stats(self, hours: int = 24) -> dict:
|
||||
|
||||
try:
|
||||
cutoff = time.time() - (hours * 3600)
|
||||
|
||||
with sqlite3.connect(self.sqlite_path) as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
|
||||
stats = conn.execute("""
|
||||
SELECT
|
||||
COUNT(*) as measurement_count,
|
||||
AVG(noise_floor_dbm) as avg_noise_floor,
|
||||
MIN(noise_floor_dbm) as min_noise_floor,
|
||||
MAX(noise_floor_dbm) as max_noise_floor
|
||||
FROM noise_floor
|
||||
WHERE timestamp > ?
|
||||
""", (cutoff,)).fetchone()
|
||||
|
||||
return {
|
||||
"measurement_count": stats["measurement_count"],
|
||||
"avg_noise_floor": round(stats["avg_noise_floor"] or 0, 1),
|
||||
"min_noise_floor": round(stats["min_noise_floor"] or 0, 1),
|
||||
"max_noise_floor": round(stats["max_noise_floor"] or 0, 1),
|
||||
"hours": hours
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get noise floor stats: {e}")
|
||||
return {}
|
||||
|
||||
def get_noise_floor_rrd(self, hours: int = 24, resolution: str = "average") -> dict:
|
||||
|
||||
if not RRDTOOL_AVAILABLE or not self.rrd_path.exists():
|
||||
return {"error": "RRD not available"}
|
||||
|
||||
try:
|
||||
end_time = int(time.time())
|
||||
start_time = end_time - (hours * 3600)
|
||||
|
||||
# Fetch data from RRD
|
||||
fetch_result = rrdtool.fetch(
|
||||
str(self.rrd_path),
|
||||
resolution.upper(),
|
||||
"--start", str(start_time),
|
||||
"--end", str(end_time)
|
||||
)
|
||||
|
||||
if not fetch_result:
|
||||
return {"error": "No data available"}
|
||||
|
||||
(start, end, step), data_sources, data_points = fetch_result
|
||||
|
||||
# Find noise_floor data source index
|
||||
try:
|
||||
noise_floor_index = data_sources.index('noise_floor')
|
||||
except ValueError:
|
||||
return {"error": "Noise floor data not found in RRD"}
|
||||
|
||||
# Extract timestamps and noise floor values
|
||||
timestamps = []
|
||||
noise_values = []
|
||||
|
||||
current_time = start
|
||||
for point in data_points:
|
||||
timestamps.append(current_time * 1000) # Convert to milliseconds for JavaScript
|
||||
noise_floor_value = point[noise_floor_index]
|
||||
noise_values.append(noise_floor_value if noise_floor_value is not None else None)
|
||||
current_time += step
|
||||
|
||||
# Filter out None values and create chart data
|
||||
chart_data = []
|
||||
valid_values = []
|
||||
|
||||
for i, (ts, value) in enumerate(zip(timestamps, noise_values)):
|
||||
if value is not None:
|
||||
chart_data.append([ts, value])
|
||||
valid_values.append(value)
|
||||
|
||||
# Calculate statistics
|
||||
stats = {}
|
||||
if valid_values:
|
||||
stats = {
|
||||
"min": round(min(valid_values), 1),
|
||||
"max": round(max(valid_values), 1),
|
||||
"avg": round(sum(valid_values) / len(valid_values), 1),
|
||||
"count": len(valid_values)
|
||||
}
|
||||
|
||||
return {
|
||||
"start_time": start,
|
||||
"end_time": end,
|
||||
"step": step,
|
||||
"hours": hours,
|
||||
"resolution": resolution,
|
||||
"data": chart_data, # Array of [timestamp_ms, value] pairs for charting
|
||||
"timestamps": timestamps,
|
||||
"values": noise_values,
|
||||
"stats": stats
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get noise floor RRD data: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
def close(self):
|
||||
"""Clean shutdown of storage systems."""
|
||||
|
||||
if self.mqtt_client:
|
||||
self.mqtt_client.loop_stop()
|
||||
self.mqtt_client.disconnect()
|
||||
|
||||
@@ -451,6 +451,55 @@ class APIEndpoints:
|
||||
logger.error(f"Failed to save config to {config_path}: {e}")
|
||||
raise
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
def noise_floor_history(self, hours: int = 24):
|
||||
try:
|
||||
storage = self._get_storage()
|
||||
hours = int(hours)
|
||||
history = storage.get_noise_floor_history(hours=hours)
|
||||
|
||||
return self._success({
|
||||
"history": history,
|
||||
"hours": hours,
|
||||
"count": len(history)
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching noise floor history: {e}")
|
||||
return self._error(e)
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
def noise_floor_stats(self, hours: int = 24):
|
||||
try:
|
||||
storage = self._get_storage()
|
||||
hours = int(hours)
|
||||
stats = storage.get_noise_floor_stats(hours=hours)
|
||||
|
||||
return self._success({
|
||||
"stats": stats,
|
||||
"hours": hours
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching noise floor stats: {e}")
|
||||
return self._error(e)
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
def noise_floor_chart_data(self, hours: int = 24):
|
||||
try:
|
||||
storage = self._get_storage()
|
||||
hours = int(hours)
|
||||
chart_data = storage.get_noise_floor_rrd(hours=hours)
|
||||
|
||||
return self._success({
|
||||
"chart_data": chart_data,
|
||||
"hours": hours
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching noise floor chart data: {e}")
|
||||
return self._error(e)
|
||||
|
||||
@cherrypy.expose
|
||||
def cad_calibration_stream(self):
|
||||
cherrypy.response.headers['Content-Type'] = 'text/event-stream'
|
||||
|
||||
@@ -16,6 +16,427 @@ This document provides examples for using the pyMC_Repeater API endpoints to cre
|
||||
- `/api/packet_type_stats` - Get packet type distribution
|
||||
- `/api/rrd_data` - Get raw RRD time series data
|
||||
|
||||
### Noise Floor Monitoring
|
||||
- `/api/noise_floor_history` - Get noise floor history (SQLite data)
|
||||
- `/api/noise_floor_stats` - Get noise floor statistics
|
||||
- `/api/noise_floor_chart_data` - Get noise floor RRD chart data
|
||||
|
||||
## Noise Floor API Examples
|
||||
|
||||
### Fetch Noise Floor History
|
||||
```javascript
|
||||
// Get last 24 hours of noise floor data from SQLite
|
||||
async function fetchNoiseFloorHistory() {
|
||||
const response = await fetch('/api/noise_floor_history?hours=24');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
const history = result.data.history;
|
||||
console.log(`Found ${history.length} noise floor measurements`);
|
||||
|
||||
// Each record: { timestamp: 1234567890.123, noise_floor_dbm: -95.5 }
|
||||
history.forEach(record => {
|
||||
console.log(`${new Date(record.timestamp * 1000).toISOString()}: ${record.noise_floor_dbm} dBm`);
|
||||
});
|
||||
} else {
|
||||
console.error('Error:', result.error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Fetch Noise Floor Statistics
|
||||
```javascript
|
||||
// Get statistical summary of noise floor data
|
||||
async function fetchNoiseFloorStats() {
|
||||
const response = await fetch('/api/noise_floor_stats?hours=24');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
const stats = result.data.stats;
|
||||
console.log('Noise Floor Statistics:');
|
||||
console.log(`Count: ${stats.count}`);
|
||||
console.log(`Average: ${stats.average?.toFixed(1)} dBm`);
|
||||
console.log(`Min: ${stats.min} dBm`);
|
||||
console.log(`Max: ${stats.max} dBm`);
|
||||
console.log(`Std Dev: ${stats.std_dev?.toFixed(2)}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Fetch Chart-Ready Noise Floor Data
|
||||
```javascript
|
||||
// Get RRD-based noise floor data optimized for charts
|
||||
async function fetchNoiseFloorChartData() {
|
||||
const response = await fetch('/api/noise_floor_chart_data?hours=24');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
const chartData = result.data.chart_data;
|
||||
|
||||
// Data points: [[timestamp_ms, noise_floor_dbm], ...]
|
||||
chartData.data_points.forEach(point => {
|
||||
const [timestamp_ms, value] = point;
|
||||
console.log(`${new Date(timestamp_ms).toISOString()}: ${value} dBm`);
|
||||
});
|
||||
|
||||
console.log('Statistics:', chartData.statistics);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Noise Floor Chart Examples
|
||||
|
||||
### 1. Noise Floor Time Series (Chart.js)
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns"></script>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="noiseFloorChart" width="800" height="400"></canvas>
|
||||
|
||||
<script>
|
||||
async function createNoiseFloorChart() {
|
||||
const response = await fetch('/api/noise_floor_chart_data?hours=24');
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
console.error('API Error:', result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
const chartData = result.data.chart_data;
|
||||
const stats = chartData.statistics;
|
||||
|
||||
const ctx = document.getElementById('noiseFloorChart').getContext('2d');
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [{
|
||||
label: 'Noise Floor',
|
||||
data: chartData.data_points,
|
||||
borderColor: '#FF6384',
|
||||
backgroundColor: '#FF638420',
|
||||
fill: true,
|
||||
tension: 0.1,
|
||||
pointRadius: 1,
|
||||
pointHoverRadius: 5
|
||||
}, {
|
||||
label: `Average (${stats.average.toFixed(1)} dBm)`,
|
||||
data: chartData.data_points.map(point => [point[0], stats.average]),
|
||||
borderColor: '#36A2EB',
|
||||
borderDash: [5, 5],
|
||||
pointRadius: 0,
|
||||
fill: false
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Noise Floor Over Time (Last 24 Hours)'
|
||||
},
|
||||
subtitle: {
|
||||
display: true,
|
||||
text: `Min: ${stats.min} dBm | Max: ${stats.max} dBm | Std Dev: ${stats.std_dev.toFixed(2)}`
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
time: {
|
||||
displayFormats: {
|
||||
hour: 'MMM dd HH:mm'
|
||||
}
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Time'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Noise Floor (dBm)'
|
||||
},
|
||||
min: Math.min(stats.min - 5, -120),
|
||||
max: Math.max(stats.max + 5, -60)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createNoiseFloorChart();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 2. Noise Floor Distribution Histogram
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="noiseDistributionChart" width="600" height="400"></canvas>
|
||||
|
||||
<script>
|
||||
async function createNoiseDistributionChart() {
|
||||
const response = await fetch('/api/noise_floor_history?hours=168'); // 1 week
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
console.error('API Error:', result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
const history = result.data.history;
|
||||
const values = history.map(record => record.noise_floor_dbm);
|
||||
|
||||
// Create histogram bins
|
||||
const min = Math.min(...values);
|
||||
const max = Math.max(...values);
|
||||
const binCount = 20;
|
||||
const binSize = (max - min) / binCount;
|
||||
|
||||
const bins = Array(binCount).fill(0);
|
||||
const binLabels = [];
|
||||
|
||||
for (let i = 0; i < binCount; i++) {
|
||||
const binStart = min + (i * binSize);
|
||||
const binEnd = binStart + binSize;
|
||||
binLabels.push(`${binStart.toFixed(1)} to ${binEnd.toFixed(1)}`);
|
||||
|
||||
values.forEach(value => {
|
||||
if (value >= binStart && value < binEnd) {
|
||||
bins[i]++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const ctx = document.getElementById('noiseDistributionChart').getContext('2d');
|
||||
new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: binLabels,
|
||||
datasets: [{
|
||||
label: 'Frequency',
|
||||
data: bins,
|
||||
backgroundColor: '#4BC0C0',
|
||||
borderColor: '#36A2EB',
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Noise Floor Distribution (Last Week)'
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Noise Floor Range (dBm)'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Count'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createNoiseDistributionChart();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 3. Real-time Noise Floor Monitor
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns"></script>
|
||||
<style>
|
||||
.noise-monitor { padding: 20px; }
|
||||
.current-stats { display: flex; gap: 20px; margin-bottom: 20px; }
|
||||
.stat-card { padding: 15px; background: #f8f9fa; border-radius: 8px; text-align: center; }
|
||||
.stat-value { font-size: 24px; font-weight: bold; }
|
||||
.stat-label { font-size: 14px; color: #666; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="noise-monitor">
|
||||
<h2>Real-time Noise Floor Monitor</h2>
|
||||
|
||||
<div class="current-stats">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="currentNoise">-- dBm</div>
|
||||
<div class="stat-label">Current</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="avgNoise">-- dBm</div>
|
||||
<div class="stat-label">1h Average</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="minNoise">-- dBm</div>
|
||||
<div class="stat-label">1h Min</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="maxNoise">-- dBm</div>
|
||||
<div class="stat-label">1h Max</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<canvas id="realTimeChart" width="800" height="400"></canvas>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let chart = null;
|
||||
let lastUpdateTime = 0;
|
||||
|
||||
async function updateNoiseFloorData() {
|
||||
try {
|
||||
// Get chart data for the last hour
|
||||
const chartResponse = await fetch('/api/noise_floor_chart_data?hours=1');
|
||||
const chartResult = await chartResponse.json();
|
||||
|
||||
if (!chartResult.success) {
|
||||
console.error('Chart API Error:', chartResult.error);
|
||||
return;
|
||||
}
|
||||
|
||||
const chartData = chartResult.data.chart_data;
|
||||
const stats = chartData.statistics;
|
||||
|
||||
// Update current stats display
|
||||
const currentValue = chartData.data_points.length > 0
|
||||
? chartData.data_points[chartData.data_points.length - 1][1]
|
||||
: null;
|
||||
|
||||
document.getElementById('currentNoise').textContent =
|
||||
currentValue ? `${currentValue.toFixed(1)} dBm` : '-- dBm';
|
||||
document.getElementById('avgNoise').textContent = `${stats.average.toFixed(1)} dBm`;
|
||||
document.getElementById('minNoise').textContent = `${stats.min} dBm`;
|
||||
document.getElementById('maxNoise').textContent = `${stats.max} dBm`;
|
||||
|
||||
// Create or update chart
|
||||
if (!chart) {
|
||||
createChart(chartData);
|
||||
} else {
|
||||
// Check if we have new data
|
||||
const latestTimestamp = chartData.data_points.length > 0
|
||||
? chartData.data_points[chartData.data_points.length - 1][0]
|
||||
: 0;
|
||||
|
||||
if (latestTimestamp > lastUpdateTime) {
|
||||
chart.data.datasets[0].data = chartData.data_points;
|
||||
chart.data.datasets[1].data = chartData.data_points.map(point => [point[0], stats.average]);
|
||||
chart.update('none');
|
||||
lastUpdateTime = latestTimestamp;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error updating noise floor data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function createChart(chartData) {
|
||||
const ctx = document.getElementById('realTimeChart').getContext('2d');
|
||||
const stats = chartData.statistics;
|
||||
|
||||
chart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [{
|
||||
label: 'Noise Floor',
|
||||
data: chartData.data_points,
|
||||
borderColor: '#FF6384',
|
||||
backgroundColor: '#FF638420',
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
pointRadius: 2,
|
||||
pointHoverRadius: 5
|
||||
}, {
|
||||
label: 'Average',
|
||||
data: chartData.data_points.map(point => [point[0], stats.average]),
|
||||
borderColor: '#36A2EB',
|
||||
borderDash: [5, 5],
|
||||
pointRadius: 0,
|
||||
fill: false
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
animation: {
|
||||
duration: 750
|
||||
},
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Noise Floor - Last Hour (Real-time)'
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
time: {
|
||||
displayFormats: {
|
||||
minute: 'HH:mm'
|
||||
}
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Time'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Noise Floor (dBm)'
|
||||
},
|
||||
min: Math.min(stats.min - 5, -120),
|
||||
max: Math.max(stats.max + 5, -60)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
lastUpdateTime = chartData.data_points.length > 0
|
||||
? chartData.data_points[chartData.data_points.length - 1][0]
|
||||
: 0;
|
||||
}
|
||||
|
||||
// Initial load
|
||||
updateNoiseFloorData();
|
||||
|
||||
// Update every 30 seconds
|
||||
setInterval(updateNoiseFloorData, 30000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
## Chart.js Examples
|
||||
|
||||
### 1. Packet Type Distribution (Pie Chart)
|
||||
|
||||
Reference in New Issue
Block a user