Skip to content

Commit 351e100

Browse files
committed
fix: HistoryCodec mask width for 16 sensors + enable mDNS
- HistoryCodec.h/cpp: changed field mask from uint16_t (2 bytes, 16 bits) to uint32_t (3 bytes, 18 bits). With MAX_SENSORS=16, the mask needs 2 ambient + 16 sensor = 18 bits. Sensors 14-15 were silently truncated in delta records. Updated HIST_V2_MAX_DELTA_SIZE 58→62 bytes. - test_history_codec: updated assertions for 3-byte mask and 40-byte anchor (was expecting old 28-byte record size). - platformio.ini: enabled mDNS (-DSIMUT_MDNS=1). Cost: ~15KB flash. - All 51 tests passing (29 validators + 22 HistoryCodec).
1 parent 0a38762 commit 351e100

5 files changed

Lines changed: 45 additions & 33 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,19 @@ All notable changes to SIMUT firmware.
2929
- **`PIN_ONEWIRE_DEFAULT`** — Fixed preprocessor redefinition warning (8 instances eliminated).
3030
- **All 4 sensor channels initialized**`MAX_SENSOR_CHANNELS` loop sets `avgValue` to NAN and `calibrationOffset` to 0.
3131

32+
### Other Changes
33+
34+
- **mDNS enabled by default**`-DSIMUT_MDNS=1` in platformio.ini. Device accessible via `http://simut.local`. Cost: ~15KB flash, negligible RAM.
35+
- **I2C0/I2C1 auto-detection**`i2cPeripheralForPins()` selects the correct peripheral at runtime. Any GPIO 0-15 pair works for I2C sensors (hardware permitting).
36+
- **`checkAndAutoHealSensors()`** — No longer reports false "Sensor missing" warnings for non-DS18B20 sensor types (DHT22, BME280).
37+
- **BME280 boot guard** — I2C timeout (50ms) + ACK probe prevents boot hang when BME280 is configured but not physically connected.
38+
- **Hardcoded GPIO assumptions removed** — DHT22 `begin()` no longer references GPIO 10. DS18B20 legacy methods use first active sensor's pin. Zero fixed GPIO-to-type coupling.
39+
3240
### Flash Budget
3341

34-
- **Without BME280**: 91.6% (957KB / 1044KB)
35-
- **With BME280**: 92.3% (964KB / 1044KB) — ~7KB for BME280 driver
36-
- **RAM**: 35.7-35.8% (~93.5KB / 262KB)
42+
- **Release (DS18B20 + DHT22 + mDNS)**: 93.1% (972KB / 1044KB)~72KB free
43+
- **With BME280**: 93.7% (979KB / 1044KB) — ~65KB free
44+
- **RAM**: 35.7% (~93.7KB / 262KB)
3745

3846
## v1.4.3-beta (2026-06-07)
3947

platformio.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ build_flags =
5252
-Wl,-u,_printf_float
5353
-DSIMUT_SENSOR_DS18B20=1
5454
-DSIMUT_SENSOR_DHT22=1
55+
-DSIMUT_MDNS=1
5556

5657
; --- External libraries (pinned for reproducibility) ---
5758
lib_deps =

src/HistoryCodec.cpp

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -82,18 +82,20 @@ size_t historyEncodeRecord(const BinaryHistoryRecord& rec,
8282
/* DELTA */
8383
uint8_t* p = buf;
8484

85-
/* Mask: bit 0=amb_temp, bit 1=amb_hum, bit 2..17=sensors[0..15]. */
86-
uint16_t mask = 0;
87-
if (rec.ambientTemp != HIST_NAN_SENTINEL) mask |= (1u << 0);
88-
if (rec.ambientHum != HIST_NAN_SENTINEL) mask |= (1u << 1);
85+
/* Mask: bit 0=amb_temp, bit 1=amb_hum, bit 2..17=sensors[0..15].
86+
* Uses 3 bytes (18 bits) — uint32_t to fit all 16 sensor slots. */
87+
uint32_t mask = 0;
88+
if (rec.ambientTemp != HIST_NAN_SENTINEL) mask |= (1ul << 0);
89+
if (rec.ambientHum != HIST_NAN_SENTINEL) mask |= (1ul << 1);
8990
for (int i = 0; i < MAX_SENSORS; i++) {
90-
if (rec.sensors[i] != HIST_NAN_SENTINEL) mask |= (1u << (2 + i));
91+
if (rec.sensors[i] != HIST_NAN_SENTINEL) mask |= (1ul << (2 + i));
9192
}
9293

93-
if ((size_t)(p - buf) + 2 > bufSize) return 0;
94+
if ((size_t)(p - buf) + 3 > bufSize) return 0;
9495
p[0] = (uint8_t)(mask & 0xFF);
95-
p[1] = (uint8_t)((mask >> 8) & 0x0F); /* ensure top 4 bits = 0 */
96-
p += 2;
96+
p[1] = (uint8_t)((mask >> 8) & 0xFF);
97+
p[2] = (uint8_t)((mask >> 16) & 0x03); /* only bits 16-17 used */
98+
p += 3;
9799

98100
/* Δepoch always present. In normal use = configured interval (1B). */
99101
int32_t depoch = (int32_t)rec.epoch - (int32_t)s.lastValid.epoch;
@@ -108,19 +110,19 @@ size_t historyEncodeRecord(const BinaryHistoryRecord& rec,
108110
return true;
109111
};
110112

111-
if (!encField(mask & (1u << 0), s.fieldHasValid[0], rec.ambientTemp, s.lastValid.ambientTemp)) return 0;
112-
if (!encField(mask & (1u << 1), s.fieldHasValid[1], rec.ambientHum, s.lastValid.ambientHum)) return 0;
113+
if (!encField(mask & (1ul << 0), s.fieldHasValid[0], rec.ambientTemp, s.lastValid.ambientTemp)) return 0;
114+
if (!encField(mask & (1ul << 1), s.fieldHasValid[1], rec.ambientHum, s.lastValid.ambientHum)) return 0;
113115
for (int i = 0; i < MAX_SENSORS; i++) {
114-
if (!encField(mask & (1u << (2 + i)), s.fieldHasValid[2 + i],
116+
if (!encField(mask & (1ul << (2 + i)), s.fieldHasValid[2 + i],
115117
rec.sensors[i], s.lastValid.sensors[i])) return 0;
116118
}
117119

118120
/* Update state only for fields actually present. */
119121
s.lastValid.epoch = rec.epoch;
120-
if (mask & (1u << 0)) { s.lastValid.ambientTemp = rec.ambientTemp; s.fieldHasValid[0] = true; }
121-
if (mask & (1u << 1)) { s.lastValid.ambientHum = rec.ambientHum; s.fieldHasValid[1] = true; }
122+
if (mask & (1ul << 0)) { s.lastValid.ambientTemp = rec.ambientTemp; s.fieldHasValid[0] = true; }
123+
if (mask & (1ul << 1)) { s.lastValid.ambientHum = rec.ambientHum; s.fieldHasValid[1] = true; }
122124
for (int i = 0; i < MAX_SENSORS; i++) {
123-
if (mask & (1u << (2 + i))) {
125+
if (mask & (1ul << (2 + i))) {
124126
s.lastValid.sensors[i] = rec.sensors[i];
125127
s.fieldHasValid[2 + i] = true;
126128
}
@@ -148,11 +150,11 @@ size_t historyDecodeRecord(const uint8_t* buf, size_t bufLen,
148150
return sizeof(BinaryHistoryRecord);
149151
}
150152

151-
if (bufLen < 2) return 0;
153+
if (bufLen < 3) return 0;
152154
const uint8_t* p = buf;
153155

154-
uint16_t mask = (uint16_t)p[0] | ((uint16_t)p[1] << 8);
155-
p += 2;
156+
uint32_t mask = (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16);
157+
p += 3;
156158

157159
int32_t depoch = 0;
158160
size_t n = readVarintZ(p, bufLen - (size_t)(p - buf), depoch);
@@ -172,22 +174,22 @@ size_t historyDecodeRecord(const uint8_t* buf, size_t bufLen,
172174
};
173175

174176
int16_t tmpTemp = HIST_NAN_SENTINEL, tmpHum = HIST_NAN_SENTINEL;
175-
if (!decField(mask & (1u << 0), s.fieldHasValid[0], s.lastValid.ambientTemp, &tmpTemp)) return 0;
176-
if (!decField(mask & (1u << 1), s.fieldHasValid[1], s.lastValid.ambientHum, &tmpHum)) return 0;
177+
if (!decField(mask & (1ul << 0), s.fieldHasValid[0], s.lastValid.ambientTemp, &tmpTemp)) return 0;
178+
if (!decField(mask & (1ul << 1), s.fieldHasValid[1], s.lastValid.ambientHum, &tmpHum)) return 0;
177179
outRec.ambientTemp = tmpTemp;
178180
outRec.ambientHum = tmpHum;
179181
for (int i = 0; i < MAX_SENSORS; i++) {
180182
int16_t tmpS = HIST_NAN_SENTINEL;
181-
if (!decField(mask & (1u << (2 + i)), s.fieldHasValid[2 + i],
183+
if (!decField(mask & (1ul << (2 + i)), s.fieldHasValid[2 + i],
182184
s.lastValid.sensors[i], &tmpS)) return 0;
183185
outRec.sensors[i] = tmpS;
184186
}
185187

186188
s.lastValid.epoch = outRec.epoch;
187-
if (mask & (1u << 0)) { s.lastValid.ambientTemp = outRec.ambientTemp; s.fieldHasValid[0] = true; }
188-
if (mask & (1u << 1)) { s.lastValid.ambientHum = outRec.ambientHum; s.fieldHasValid[1] = true; }
189+
if (mask & (1ul << 0)) { s.lastValid.ambientTemp = outRec.ambientTemp; s.fieldHasValid[0] = true; }
190+
if (mask & (1ul << 1)) { s.lastValid.ambientHum = outRec.ambientHum; s.fieldHasValid[1] = true; }
189191
for (int i = 0; i < MAX_SENSORS; i++) {
190-
if (mask & (1u << (2 + i))) {
192+
if (mask & (1ul << (2 + i))) {
191193
s.lastValid.sensors[i] = outRec.sensors[i];
192194
s.fieldHasValid[2 + i] = true;
193195
}

src/HistoryCodec.h

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
* @details Format replaces the fixed 40 B/record of v1 with:
55
* - 16 B header per file (magic SIM2 + version + anchorPeriod)
66
* - 1 anchor (full 40 B record) every N=60 records
7-
* - 59 variable deltas between anchors (typical ~7-18 B each)
7+
* - 59 variable deltas between anchors (typical ~7-19 B each)
8+
* - 3-byte field mask (18 bits: 2 ambient + 16 sensor slots)
89
*
910
* Typical compression 3-4x for common usage (few active sensors +
1011
* small temperature variation between samples). Reader is linear:
@@ -26,7 +27,7 @@ constexpr char HIST_V2_MAGIC[4] = {'S','I','M','2'};
2627
constexpr uint16_t HIST_V2_VERSION = 0x0002;
2728
constexpr uint16_t HIST_V2_ANCHOR_PERIOD = 60; /* 1 anchor + 59 deltas (= 1 hour @ 1 min) */
2829
constexpr size_t HIST_V2_HEADER_SIZE = 16;
29-
constexpr size_t HIST_V2_MAX_DELTA_SIZE = 58; /* worst-case: 2B mask + 5B Δepoch + 18*3B varints */
30+
constexpr size_t HIST_V2_MAX_DELTA_SIZE = 62; /* worst-case: 3B mask + 5B Δepoch + 18*3B varints */
3031

3132
struct __attribute__((packed)) HistoryFileHeaderV2 {
3233
char magic[4]; /* "SIM2" */
@@ -59,7 +60,7 @@ void historyCodecReset(HistoryCodecState& s);
5960
/* ENCODER / DECODER */
6061
/* ======================================================================== */
6162

62-
/** Encode a record. Automatically decides between anchor (28 B fixed) or
63+
/** Encode a record. Automatically decides between anchor (40 B fixed) or
6364
* variable delta based on recordsSinceAnchor.
6465
*
6566
* @param rec Input record (in-memory uncompressed).
@@ -83,7 +84,7 @@ size_t historyEncodeRecord(const BinaryHistoryRecord& rec,
8384
* @param bufLen Buffer size.
8485
* @param s Decoder state (updated in-place).
8586
* @param outRec Reconstructed record.
86-
* @param isAnchor true if this record is an anchor (28 B fixed).
87+
* @param isAnchor true if this record is an anchor (40 B fixed).
8788
* @return Bytes consumed, or 0 on error (truncated buffer).
8889
*/
8990
size_t historyDecodeRecord(const uint8_t* buf, size_t bufLen,

test/test_history_codec/test_main.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -533,8 +533,8 @@ void test_nanFieldsOmittedFromDelta(void) {
533533
rec.epoch += 60;
534534
size_t written = historyEncodeRecord(rec, state, buf, sizeof(buf), nullptr);
535535

536-
/* Delta should be minimal: 2B mask + 1B epoch varint = 3 bytes */
537-
TEST_ASSERT_EQUAL_size_t(3, written);
536+
/* Delta should be minimal: 3B mask + 1B epoch varint = 4 bytes */
537+
TEST_ASSERT_EQUAL_size_t(4, written);
538538
}
539539

540540

@@ -543,7 +543,7 @@ void test_nanFieldsOmittedFromDelta(void) {
543543
/* =========================================================================== */
544544

545545
void test_bufferTooSmall_anchor(void) {
546-
/* Anchor requires sizeof(BinaryHistoryRecord) = 28 bytes.
546+
/* Anchor requires sizeof(BinaryHistoryRecord) = 40 bytes.
547547
* Offering less must return 0 (error). */
548548
HistoryCodecState state;
549549
historyCodecReset(state);

0 commit comments

Comments
 (0)