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
31 changes: 31 additions & 0 deletions firmware/esp32-csi-node/main/edge_processing.c
Original file line number Diff line number Diff line change
Expand Up @@ -937,6 +937,37 @@ static void send_vitals_packet(void)
s_latest_pkt = pkt;
s_pkt_valid = true;

/* Per-slot vitals, sent alongside (never instead of) the aggregate packet
* above. update_multi_person_vitals() already computed these; before this
* they were discarded at the wire, leaving the sink unable to tell what the
* slot count was actually counting. See EDGE_VITALS_SLOTS_MAGIC's comment. */
{
edge_vitals_slots_pkt_t spkt;
memset(&spkt, 0, sizeof(spkt));
spkt.magic = EDGE_VITALS_SLOTS_MAGIC;
spkt.node_id = pkt.node_id;
spkt.n_active = n_active;
spkt.max_slots = EDGE_MAX_PERSONS;
spkt.timestamp_ms = pkt.timestamp_ms;
for (uint8_t p = 0; p < EDGE_MAX_PERSONS; p++) {
if (s_persons[p].active) {
spkt.active_mask |= (uint8_t)(1u << p);
}
/* Fixed-point BPM*100 in a uint16 tops out at 655 BPM, far above
* any plausible rate; clamp rather than wrap on a bad estimate. */
float br = s_persons[p].breathing_bpm;
float hr = s_persons[p].heartrate_bpm;
if (br < 0.0f) br = 0.0f;
if (br > 650.0f) br = 650.0f;
if (hr < 0.0f) hr = 0.0f;
if (hr > 650.0f) hr = 650.0f;
spkt.breathing_bpm[p] = (uint16_t)(br * 100.0f);
spkt.heartrate_bpm[p] = (uint16_t)(hr * 100.0f);
spkt.subcarrier_idx[p] = s_persons[p].subcarrier_idx;
}
stream_sender_send((const uint8_t *)&spkt, sizeof(spkt));
}

/* ADR-063: If mmWave is active, send fused 48-byte packet instead. */
mmwave_state_t mw;
if (mmwave_sensor_get_state(&mw) && mw.detected) {
Expand Down
35 changes: 35 additions & 0 deletions firmware/esp32-csi-node/main/edge_processing.h
Original file line number Diff line number Diff line change
Expand Up @@ -343,4 +343,39 @@ void edge_get_phase_history(const float **out_buf, uint16_t *out_len,
*/
void edge_get_variances(float *out_variances, uint16_t n_subcarriers);

/* ---- Per-slot vitals packet (wire format) ----
*
* update_multi_person_vitals() already computes an independent breathing and
* heart rate for every detected slot — its own phase history, filters and
* autocorrelation. The 32-byte edge_vitals_pkt_t above then transmits only the
* aggregate plus a bare n_persons count, so all per-slot detail is computed
* and thrown away.
*
* That discarded detail is the only thing that can distinguish WHAT is being
* counted. The slot count is a subcarrier-energy-cluster heuristic with no
* size or species awareness, so a room containing pets reports them as
* "persons". Per-slot breathing rates are species-plausible evidence: a
* resting human sits near 12-20 BPM, a rabbit near 30-60, a chinchilla higher
* still. Sending them lets the sink label honestly instead of overclaiming.
*
* Sent IN ADDITION to edge_vitals_pkt_t, never instead of it, so an older sink
* keeps working unchanged and simply ignores this magic. */
#define EDGE_VITALS_SLOTS_MAGIC 0xC5110009

typedef struct __attribute__((packed)) {
uint32_t magic; /**< EDGE_VITALS_SLOTS_MAGIC = 0xC5110009. */
uint8_t node_id; /**< ESP32 node identifier. */
uint8_t n_active; /**< Slots currently marked active. */
uint8_t max_slots; /**< EDGE_MAX_PERSONS, so the sink can validate. */
uint8_t active_mask; /**< Bit p set when slot p is active. */
uint32_t timestamp_ms; /**< Milliseconds since boot. */
uint16_t breathing_bpm[EDGE_MAX_PERSONS]; /**< BPM * 100 per slot. */
uint16_t heartrate_bpm[EDGE_MAX_PERSONS]; /**< BPM * 100 per slot. */
uint8_t subcarrier_idx[EDGE_MAX_PERSONS]; /**< Subcarrier group per slot. */
} edge_vitals_slots_pkt_t;

_Static_assert(sizeof(edge_vitals_slots_pkt_t) == 32,
"per-slot vitals packet must be 32 bytes");


#endif /* EDGE_PROCESSING_H */
Loading