feat(dashboard): presence panel with per-link motion indicators and deltaRMS time series
Add 500ms periodic presence_update broadcast from the dashboard hub with per-link motion state (is_motion, delta_rms, confidence). Surface this in a new Presence panel on the dashboard with coloured dot indicators (green=clear, amber=motion, red=high-confidence) and deltaRMS values. Includes a rolling 10s Canvas 2D line chart of deltaRMS with a threshold line at 0.02 for the selected link. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
bcd19ad756
commit
75edd8339a
4 changed files with 429 additions and 9 deletions
|
|
@ -269,6 +269,125 @@
|
|||
transition: background 0.2s;
|
||||
}
|
||||
#floorplan-btn:hover { background: rgba(255,255,255,0.12); color: #ccc; }
|
||||
|
||||
/* Presence panel */
|
||||
#presence-panel {
|
||||
position: fixed;
|
||||
top: 60px;
|
||||
right: 20px;
|
||||
width: 300px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
#presence-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#presence-header h3 {
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#presence-status {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.3s, color 0.3s;
|
||||
}
|
||||
|
||||
#presence-status.motion {
|
||||
background: rgba(244, 67, 54, 0.3);
|
||||
color: #ef5350;
|
||||
}
|
||||
|
||||
#presence-status.clear {
|
||||
background: rgba(76, 175, 80, 0.15);
|
||||
color: #66bb6a;
|
||||
}
|
||||
|
||||
#presence-list {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.presence-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 5px 8px;
|
||||
margin-bottom: 3px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.presence-row:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.presence-row.selected {
|
||||
background: rgba(79, 195, 247, 0.2);
|
||||
}
|
||||
|
||||
.presence-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.presence-dot.clear {
|
||||
background: #66bb6a;
|
||||
box-shadow: 0 0 6px rgba(102, 187, 106, 0.5);
|
||||
}
|
||||
|
||||
.presence-dot.motion {
|
||||
background: #ffa726;
|
||||
box-shadow: 0 0 6px rgba(255, 167, 38, 0.5);
|
||||
}
|
||||
|
||||
.presence-dot.high-confidence {
|
||||
background: #ef5350;
|
||||
box-shadow: 0 0 6px rgba(239, 83, 80, 0.5);
|
||||
}
|
||||
|
||||
.presence-link-id {
|
||||
font-family: monospace;
|
||||
color: #aaa;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.presence-rms {
|
||||
font-family: monospace;
|
||||
color: #888;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
#deltarms-label {
|
||||
font-size: 10px;
|
||||
color: #555;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
#deltarms-chart {
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -326,6 +445,19 @@
|
|||
<canvas id="timeseries-chart"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- Presence panel -->
|
||||
<div id="presence-panel">
|
||||
<div id="presence-header">
|
||||
<h3>Presence</h3>
|
||||
<span id="presence-status" class="clear">CLEAR</span>
|
||||
</div>
|
||||
<div id="presence-list">
|
||||
<div class="empty-state">No links active</div>
|
||||
</div>
|
||||
<div id="deltarms-label">Delta RMS (10 s)</div>
|
||||
<canvas id="deltarms-chart"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- Three.js from CDN -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
||||
<!-- OrbitControls from CDN -->
|
||||
|
|
|
|||
|
|
@ -23,7 +23,10 @@
|
|||
chartBars: 64, // number of subcarriers
|
||||
chartUpdateMs: 100, // update chart at 10 Hz max
|
||||
tsMaxPoints: 360, // max time series samples per link (~60s at 6Hz)
|
||||
tsMinIntervalMs: 100 // min ms between time series samples
|
||||
tsMinIntervalMs: 100, // min ms between time series samples
|
||||
drTsWindowMs: 10000, // deltaRMS time series window: 10 seconds
|
||||
drTsMaxPoints: 100, // max deltaRMS samples per link
|
||||
drThreshold: 0.02 // DefaultDeltaRMSThreshold
|
||||
};
|
||||
|
||||
// ============================================
|
||||
|
|
@ -35,6 +38,8 @@
|
|||
nodes: new Map(), // MAC -> { mac, firmware, chip, lastSeen }
|
||||
links: new Map(), // linkID -> { nodeMAC, peerMAC, lastFrame, lastCSI, motionDetected, deltaRMS, ampHistory, lastAmpSample }
|
||||
selectedLinkID: null,
|
||||
presenceSelectedLinkID: null,
|
||||
drHistory: new Map(), // linkID -> [{ t: number, rms: number }]
|
||||
lastChartUpdate: 0,
|
||||
frameCount: 0,
|
||||
lastFpsTime: performance.now()
|
||||
|
|
@ -299,6 +304,10 @@
|
|||
}
|
||||
break;
|
||||
|
||||
case 'presence_update':
|
||||
handlePresenceUpdate(msg);
|
||||
break;
|
||||
|
||||
case 'registry_state':
|
||||
Viz3D.handleRegistryState(msg);
|
||||
break;
|
||||
|
|
@ -484,6 +493,116 @@
|
|||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Presence Panel
|
||||
// ============================================
|
||||
function handlePresenceUpdate(msg) {
|
||||
if (!msg.links) return;
|
||||
|
||||
const now = performance.now();
|
||||
let anyMotion = false;
|
||||
|
||||
for (const [linkID, info] of Object.entries(msg.links)) {
|
||||
// Update link state if link exists
|
||||
const link = state.links.get(linkID);
|
||||
if (link) {
|
||||
link.motionDetected = info.is_motion || info.motion_detected || false;
|
||||
link.deltaRMS = info.delta_rms || 0;
|
||||
}
|
||||
|
||||
if (info.is_motion || info.motion_detected) anyMotion = true;
|
||||
|
||||
// Append to deltaRMS history
|
||||
let history = state.drHistory.get(linkID);
|
||||
if (!history) {
|
||||
history = [];
|
||||
state.drHistory.set(linkID, history);
|
||||
}
|
||||
history.push({ t: now, rms: info.delta_rms || 0 });
|
||||
|
||||
// Trim to window
|
||||
const cutoff = now - CONFIG.drTsWindowMs;
|
||||
while (history.length > 0 && history[0].t < cutoff) {
|
||||
history.shift();
|
||||
}
|
||||
if (history.length > CONFIG.drTsMaxPoints) {
|
||||
history.splice(0, history.length - CONFIG.drTsMaxPoints);
|
||||
}
|
||||
}
|
||||
|
||||
updatePresencePanel(msg.links, anyMotion);
|
||||
updateLinkList();
|
||||
drawDeltaRMSTimeSeries();
|
||||
}
|
||||
|
||||
function updatePresencePanel(links, anyMotion) {
|
||||
const container = document.getElementById('presence-list');
|
||||
const statusEl = document.getElementById('presence-status');
|
||||
|
||||
if (anyMotion) {
|
||||
statusEl.className = 'motion';
|
||||
statusEl.textContent = 'MOTION';
|
||||
} else {
|
||||
statusEl.className = 'clear';
|
||||
statusEl.textContent = 'CLEAR';
|
||||
}
|
||||
|
||||
const entries = Object.entries(links);
|
||||
if (entries.length === 0) {
|
||||
container.innerHTML = '<div class="empty-state">No links active</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
for (const [linkID, info] of entries) {
|
||||
const isMotion = info.is_motion || info.motion_detected || false;
|
||||
const confidence = info.confidence || 0;
|
||||
const rms = info.delta_rms || 0;
|
||||
const shortID = abbreviateLinkID(linkID);
|
||||
const selected = state.presenceSelectedLinkID === linkID ? 'selected' : '';
|
||||
|
||||
let dotClass = 'clear';
|
||||
if (isMotion && confidence > 0.7) {
|
||||
dotClass = 'high-confidence';
|
||||
} else if (isMotion) {
|
||||
dotClass = 'motion';
|
||||
}
|
||||
|
||||
html += `
|
||||
<div class="presence-row ${selected}" data-link-id="${linkID}">
|
||||
<span class="presence-dot ${dotClass}"></span>
|
||||
<span class="presence-link-id">${shortID}</span>
|
||||
<span class="presence-rms">${rms.toFixed(4)}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
container.innerHTML = html;
|
||||
|
||||
container.querySelectorAll('.presence-row').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
state.presenceSelectedLinkID = el.dataset.linkId;
|
||||
updatePresencePanel(links, anyMotion);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function abbreviateLinkID(linkID) {
|
||||
const parts = linkID.split(':');
|
||||
if (parts.length >= 12) {
|
||||
// Full MAC:MAC format: AA:BB:CC:DD:EE:FF:AA:BB:CC:DD:EE:FF
|
||||
const nodeShort = parts.slice(3, 6).join(':');
|
||||
const peerShort = parts.slice(9, 12).join(':');
|
||||
return nodeShort + '\u2192' + peerShort;
|
||||
}
|
||||
// Fallback: last segment of each side
|
||||
const halves = linkID.split(':').reduce((acc, p, i, arr) => {
|
||||
if (i < 6) acc[0].push(p);
|
||||
else acc[1].push(p);
|
||||
return acc;
|
||||
}, [[], []]);
|
||||
return halves[0].slice(-2).join(':') + '\u2192' + halves[1].slice(-2).join(':');
|
||||
}
|
||||
|
||||
function updateLinkList() {
|
||||
const container = document.getElementById('link-list');
|
||||
document.getElementById('link-count').textContent = state.links.size;
|
||||
|
|
@ -536,6 +655,7 @@
|
|||
// ============================================
|
||||
let chartCanvas, chartCtx;
|
||||
let tsCanvas, tsCtx;
|
||||
let drCanvas, drCtx;
|
||||
|
||||
function initChart() {
|
||||
chartCanvas = document.getElementById('amplitude-chart');
|
||||
|
|
@ -557,6 +677,16 @@
|
|||
tsCtx.scale(window.devicePixelRatio, window.devicePixelRatio);
|
||||
|
||||
drawTimeSeries([]);
|
||||
|
||||
drCanvas = document.getElementById('deltarms-chart');
|
||||
drCtx = drCanvas.getContext('2d');
|
||||
|
||||
const drRect = drCanvas.getBoundingClientRect();
|
||||
drCanvas.width = drRect.width * window.devicePixelRatio;
|
||||
drCanvas.height = drRect.height * window.devicePixelRatio;
|
||||
drCtx.scale(window.devicePixelRatio, window.devicePixelRatio);
|
||||
|
||||
drawDeltaRMSTimeSeries();
|
||||
}
|
||||
|
||||
function drawEmptyChart() {
|
||||
|
|
@ -677,11 +807,101 @@
|
|||
tsCtx.fillStyle = '#555';
|
||||
tsCtx.font = '9px monospace';
|
||||
tsCtx.textAlign = 'left';
|
||||
tsCtx.fillText(`−${spanS}s`, 2, height - 2);
|
||||
tsCtx.fillText(`-${spanS}s`, 2, height - 2);
|
||||
tsCtx.textAlign = 'right';
|
||||
tsCtx.fillText('now', width - 2, height - 2);
|
||||
}
|
||||
|
||||
function drawDeltaRMSTimeSeries() {
|
||||
if (!drCanvas) return;
|
||||
const width = drCanvas.width / window.devicePixelRatio;
|
||||
const height = drCanvas.height / window.devicePixelRatio;
|
||||
|
||||
drCtx.fillStyle = '#1a1a2e';
|
||||
drCtx.fillRect(0, 0, width, height);
|
||||
|
||||
const linkID = state.presenceSelectedLinkID;
|
||||
const history = linkID ? state.drHistory.get(linkID) : null;
|
||||
|
||||
if (!history || history.length < 2) {
|
||||
drCtx.fillStyle = '#444';
|
||||
drCtx.font = '10px sans-serif';
|
||||
drCtx.textAlign = 'center';
|
||||
drCtx.fillText('Select a link', width / 2, height / 2 + 4);
|
||||
return;
|
||||
}
|
||||
|
||||
const padTop = 4;
|
||||
const padBottom = 14;
|
||||
const plotH = height - padTop - padBottom;
|
||||
|
||||
// Y-axis range: 0 to 0.1 (typical deltaRMS range)
|
||||
const yMax = 0.1;
|
||||
|
||||
// Draw threshold line at 0.02
|
||||
const threshY = padTop + plotH - (CONFIG.drThreshold / yMax) * plotH;
|
||||
drCtx.strokeStyle = 'rgba(255, 167, 38, 0.5)';
|
||||
drCtx.lineWidth = 1;
|
||||
drCtx.setLineDash([4, 3]);
|
||||
drCtx.beginPath();
|
||||
drCtx.moveTo(0, threshY);
|
||||
drCtx.lineTo(width, threshY);
|
||||
drCtx.stroke();
|
||||
drCtx.setLineDash([]);
|
||||
|
||||
// Threshold label
|
||||
drCtx.fillStyle = 'rgba(255, 167, 38, 0.7)';
|
||||
drCtx.font = '9px monospace';
|
||||
drCtx.textAlign = 'left';
|
||||
drCtx.fillText('0.02', 2, threshY - 2);
|
||||
|
||||
// Time range
|
||||
const tEnd = history[history.length - 1].t;
|
||||
const tStart = tEnd - CONFIG.drTsWindowMs;
|
||||
|
||||
// Draw deltaRMS line
|
||||
drCtx.lineWidth = 1.5;
|
||||
drCtx.strokeStyle = 'rgba(79, 195, 247, 0.8)';
|
||||
drCtx.beginPath();
|
||||
let started = false;
|
||||
|
||||
for (let i = 0; i < history.length; i++) {
|
||||
const x = ((history[i].t - tStart) / CONFIG.drTsWindowMs) * width;
|
||||
const y = padTop + plotH - (Math.min(history[i].rms, yMax) / yMax) * plotH;
|
||||
|
||||
if (!started) {
|
||||
drCtx.moveTo(x, y);
|
||||
started = true;
|
||||
} else {
|
||||
drCtx.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
drCtx.stroke();
|
||||
|
||||
// Fill area under curve
|
||||
if (history.length >= 2) {
|
||||
const lastX = ((history[history.length - 1].t - tStart) / CONFIG.drTsWindowMs) * width;
|
||||
const firstX = ((history[0].t - tStart) / CONFIG.drTsWindowMs) * width;
|
||||
drCtx.lineTo(lastX, padTop + plotH);
|
||||
drCtx.lineTo(firstX, padTop + plotH);
|
||||
drCtx.closePath();
|
||||
drCtx.fillStyle = 'rgba(79, 195, 247, 0.08)';
|
||||
drCtx.fill();
|
||||
}
|
||||
|
||||
// Time labels
|
||||
drCtx.fillStyle = '#555';
|
||||
drCtx.font = '9px monospace';
|
||||
drCtx.textAlign = 'left';
|
||||
drCtx.fillText('-10s', 2, height - 2);
|
||||
drCtx.textAlign = 'right';
|
||||
drCtx.fillText('now', width - 2, height - 2);
|
||||
|
||||
// Y-axis labels
|
||||
drCtx.textAlign = 'right';
|
||||
drCtx.fillText('0.1', width - 2, padTop + 10);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Initialization
|
||||
// ============================================
|
||||
|
|
|
|||
|
|
@ -57,8 +57,11 @@ func (h *Hub) SetIngestionState(state IngestionState) {
|
|||
|
||||
// Run starts the hub's main loop
|
||||
func (h *Hub) Run() {
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
stateTicker := time.NewTicker(5 * time.Second)
|
||||
defer stateTicker.Stop()
|
||||
|
||||
presenceTicker := time.NewTicker(500 * time.Millisecond)
|
||||
defer presenceTicker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
|
|
@ -89,8 +92,11 @@ func (h *Hub) Run() {
|
|||
}
|
||||
h.mu.RUnlock()
|
||||
|
||||
case <-ticker.C:
|
||||
case <-stateTicker.C:
|
||||
h.broadcastState()
|
||||
|
||||
case <-presenceTicker.C:
|
||||
h.broadcastPresence()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -174,6 +180,36 @@ func (h *Hub) BroadcastMotionState(states []ingestion.MotionStateItem) {
|
|||
h.Broadcast(data)
|
||||
}
|
||||
|
||||
// BroadcastPresenceUpdate sends periodic presence state for all links.
|
||||
// Broadcasts every 500ms with {type: "presence_update", links: {linkID: {...}}}.
|
||||
func (h *Hub) broadcastPresence() {
|
||||
h.mu.RLock()
|
||||
state := h.ingestionState
|
||||
clientCount := len(h.clients)
|
||||
h.mu.RUnlock()
|
||||
|
||||
if state == nil || clientCount == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
items := state.GetAllMotionStates()
|
||||
if len(items) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
links := make(map[string]ingestion.MotionStateItem, len(items))
|
||||
for _, item := range items {
|
||||
links[item.LinkID] = item
|
||||
}
|
||||
|
||||
msg := map[string]interface{}{
|
||||
"type": "presence_update",
|
||||
"links": links,
|
||||
}
|
||||
data, _ := json.Marshal(msg)
|
||||
h.Broadcast(data)
|
||||
}
|
||||
|
||||
// ─── Phase 3 Broadcasts ─────────────────────────────────────────────────────
|
||||
|
||||
// nodeJSON is the wire format for a fleet node sent to the dashboard.
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ type MotionStateItem struct {
|
|||
LinkID string `json:"link_id"`
|
||||
MotionDetected bool `json:"motion_detected"`
|
||||
DeltaRMS float64 `json:"delta_rms"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
}
|
||||
|
||||
// ReplayAppender appends raw CSI frames to a persistent store.
|
||||
|
|
@ -155,6 +156,18 @@ func (s *Server) SetRateController(rc *RateController) {
|
|||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// SetFleetNotifier sets the fleet manager for node lifecycle callbacks.
|
||||
func (s *Server) SetFleetNotifier(fn FleetNotifier) {
|
||||
s.mu.Lock()
|
||||
s.fleetNotifier = fn
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// GetConnectedMACs returns the MACs of currently-connected nodes.
|
||||
func (s *Server) GetConnectedMACs() []string {
|
||||
return s.GetConnectedNodes()
|
||||
}
|
||||
|
||||
// SendConfigToMAC sends a rate config command to a connected node by MAC.
|
||||
// varianceThreshold > 0 enables on-device amplitude variance monitoring.
|
||||
func (s *Server) SendConfigToMAC(mac string, rateHz int, varianceThreshold float64) {
|
||||
|
|
@ -242,6 +255,7 @@ func (s *Server) HandleNodeWS(w http.ResponseWriter, r *http.Request) {
|
|||
s.connections[hello.MAC] = nc
|
||||
s.malformedCounts[hello.MAC] = &malformedCounter{}
|
||||
broadcaster := s.dashboardBroadcaster
|
||||
fleetFn := s.fleetNotifier
|
||||
s.mu.Unlock()
|
||||
|
||||
log.Printf("[INFO] Node connected: MAC=%s firmware=%s chip=%s",
|
||||
|
|
@ -251,8 +265,12 @@ func (s *Server) HandleNodeWS(w http.ResponseWriter, r *http.Request) {
|
|||
broadcaster.BroadcastNodeConnected(hello.MAC, hello.FirmwareVersion, hello.Chip)
|
||||
}
|
||||
|
||||
s.sendRole(nc, "rx", "")
|
||||
s.sendConfig(nc, RateIdle, 0, DefaultVarianceThreshold)
|
||||
if fleetFn != nil {
|
||||
fleetFn.OnNodeConnected(hello.MAC, hello.FirmwareVersion, hello.Chip)
|
||||
} else {
|
||||
s.sendRole(nc, "rx", "")
|
||||
s.sendConfig(nc, RateIdle, 0, DefaultVarianceThreshold)
|
||||
}
|
||||
|
||||
go s.pingLoop(nc)
|
||||
s.handleMessages(nc)
|
||||
|
|
@ -267,6 +285,7 @@ func (s *Server) handleMessages(nc *NodeConnection) {
|
|||
delete(s.malformedCounts, nc.MAC)
|
||||
broadcaster := s.dashboardBroadcaster
|
||||
rateCtrl := s.rateCtrl
|
||||
fleetFn := s.fleetNotifier
|
||||
s.mu.Unlock()
|
||||
|
||||
log.Printf("[INFO] Node disconnected: MAC=%s", nc.MAC)
|
||||
|
|
@ -277,6 +296,9 @@ func (s *Server) handleMessages(nc *NodeConnection) {
|
|||
if rateCtrl != nil {
|
||||
rateCtrl.OnNodeDisconnected(nc.MAC)
|
||||
}
|
||||
if fleetFn != nil {
|
||||
fleetFn.OnNodeDisconnected(nc.MAC)
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
|
|
@ -580,16 +602,26 @@ func (s *Server) GetAllLinksInfo() []LinkInfo {
|
|||
|
||||
// GetAllMotionStates returns current motion state for all known links.
|
||||
func (s *Server) GetAllMotionStates() []MotionStateItem {
|
||||
s.mu.RLock()
|
||||
pm := s.processorMgr
|
||||
s.mu.RUnlock()
|
||||
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
states := make([]MotionStateItem, 0, len(s.linkMotionState))
|
||||
for linkID, detected := range s.linkMotionState {
|
||||
states = append(states, MotionStateItem{
|
||||
item := MotionStateItem{
|
||||
LinkID: linkID,
|
||||
MotionDetected: detected,
|
||||
DeltaRMS: s.linkDeltaRMS[linkID],
|
||||
})
|
||||
}
|
||||
if pm != nil {
|
||||
if proc := pm.GetProcessor(linkID); proc != nil {
|
||||
item.Confidence = proc.GetBaseline().GetConfidence()
|
||||
}
|
||||
}
|
||||
states = append(states, item)
|
||||
}
|
||||
return states
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue