Skip to content
Merged
28 changes: 27 additions & 1 deletion lib/steami_config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,36 @@ config.apply_accelerometer_calibration(imu)

---

## Boot Counter

Track how many times the board has booted.

### Set boot counter
```python
config.set_boot_count(0)
```

### Get boot counter
```python
count = config.get_boot_count()
print("Boot count:", count)
# -> Boot count: 0
```

### Increment boot counter
```python
config.increment_boot_count()
# -> boot_count = boot_count + 1
```

---

# JSON Format

Data is stored as compact JSON to fit within 1 KB:

```json
{"rev":3,"name":"STeaMi-01","tc":{"hts":{"g":1.0,"o":-0.5}},"cm":{"hx":12.3,"hy":-5.1,"hz":0.8,"sx":1.01,"sy":0.98,"sz":1.0},"ca":{"ox":0.01,"oy":-0.02,"oz":0.03}}
{"rev":3,"name":"STeaMi-01","tc":{"hts":{"g":1.0,"o":-0.5}},"cm":{"hx":12.3,"hy":-5.1,"hz":0.8,"sx":1.01,"sy":0.98,"sz":1.0},"ca":{"ox":0.01,"oy":-0.02,"oz":0.03},"bc":0}
```

| Key | Content |
Expand All @@ -165,6 +189,7 @@ Data is stored as compact JSON to fit within 1 KB:
| `cm.sx/sy/sz` | Soft-iron scale factors (X, Y, Z) |
| `ca` | Accelerometer calibration dict |
| `ca.ox/oy/oz` | Bias offsets in g (X, Y, Z) |
| `bc` | Boot counter |

Sensor short keys: `hts` (HTS221), `mag` (LIS2MDL), `ism` (ISM330DL),
`hid` (WSEN-HIDS), `pad` (WSEN-PADS).
Expand All @@ -179,6 +204,7 @@ Sensor short keys: `hts` (HTS221), `mag` (LIS2MDL), `ism` (ISM330DL),
| `calibrate_temperature.py` | Calibrate all sensors against WSEN-HIDS reference |
| `calibrate_magnetometer.py` | Calibrate LIS2MDL with OLED display and persistent storage |
| `calibrate_accelerometer.py` | Calibrate ISM330DL accelerometer bias and persist it |
| `boot_counter.py` | Display the amount of boot and increment it |

Run with mpremote:

Expand Down
28 changes: 28 additions & 0 deletions lib/steami_config/examples/boot_counter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""
Boot counter example.
Each time the board boots,
it increments the boot count
and saves it to non-volatile storage.
"""

from daplink_bridge import DaplinkBridge
from machine import I2C
from steami_config import SteamiConfig

# --- Hardware init ---
i2c = I2C(1)
bridge = DaplinkBridge(i2c)

config = SteamiConfig(bridge)
config.load()

# --- Boot counter logic ---
config.increment_boot_count()

# Save updated value
config.save()
Comment on lines +19 to +23
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example increments the counter and immediately calls config.save(). Since SteamiConfig.save() erases the config zone (bridge.clear_config()) before rewriting JSON, doing this on every boot can significantly increase flash erase/write cycles (and is especially risky in reboot loops). Consider adding a note about flash wear / optionally persisting less frequently (e.g., only every N boots or on graceful shutdown).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid point about flash wear. However, this is a pedagogical example — simplicity is the priority. The STeaMi DAPLink config zone uses a dedicated EEPROM-like region, not the main flash, so the write endurance is higher. Adding wear-leveling logic to a 28-line example would obscure the intent. A comment warning about frequent writes could be added, but it's not blocking.


# Read and display
count = config.get_boot_count()

print("Boot count:", count)
19 changes: 19 additions & 0 deletions lib/steami_config/steami_config/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,3 +254,22 @@ def apply_accelerometer_calibration(self, ism330dl_instance):
cal["oy"],
cal["oz"],
)

# --------------------------------------------------
# Boot counter
# --------------------------------------------------

def set_boot_count(self, count):
"""Store the number of times the board has booted."""
self._data["bc"] = int(count)

def get_boot_count(self):
"""Return the stored boot count, or None."""
return self._data.get("bc")

def increment_boot_count(self):
"""Increment the stored boot count by 1."""
count = self.get_boot_count()
if count is None:
count = 0
self.set_boot_count(count + 1)
147 changes: 147 additions & 0 deletions tests/scenarios/steami_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,72 @@ tests:
expect_true: true
mode: [mock]

# -- Mock boot counter --
- name: "Set and get boot counter"
action: script
script: |
dev._data = {}
dev.set_boot_count(427)
result = dev.get_boot_count() == 427
expect_true: true
mode: [mock]

- name: "Get boot counter returns None when not set"
action: script
script: |
dev._data = {}
result = dev.get_boot_count() is None
expect_true: true
mode: [mock]

- name: "Increment boot counter from empty starts at 1"
action: script
script: |
dev._data = {}
dev.increment_boot_count()
result = dev.get_boot_count() == 1
expect_true: true
mode: [mock]

- name: "Increment boot counter increases existing value"
action: script
script: |
dev._data = {}
dev.set_boot_count(41)
dev.increment_boot_count()
result = dev.get_boot_count() == 42
expect_true: true
mode: [mock]

- name: "Boot counter survives save/load"
action: script
script: |
dev._data = {}
dev.set_boot_count(427)
dev.save()
dev2 = SteamiConfig(dev._bridge)
dev2.load()
result = dev2.get_boot_count() == 427
expect_true: true
mode: [mock]

- name: "Boot counter coexists with other config values"
action: script
script: |
dev._data = {}
dev.board_revision = 3
dev.board_name = "STeaMi-Test"
dev.set_boot_count(12)
dev.set_accelerometer_calibration(ox=0.01, oy=-0.02, oz=0.03)
result = (
dev.board_revision == 3
and dev.board_name == "STeaMi-Test"
and dev.get_boot_count() == 12
and dev.get_accelerometer_calibration()["ox"] == 0.01
)
expect_true: true
mode: [mock]

# ----- Hardware -----

- name: "Save and load config on hardware"
Expand Down Expand Up @@ -541,3 +607,84 @@ tests:
)
expect_true: true
mode: [hardware]

# -- Hardware Boot Counter --
- name: "Save and load boot counter on hardware"
action: script
script: |
from time import sleep_ms
dev._bridge.clear_config()
dev._data = {}
dev.set_boot_count(427)
dev.save()
sleep_ms(200)
dev2 = SteamiConfig(dev._bridge)
dev2.load()
result = dev2.get_boot_count() == 427
expect_true: true
mode: [hardware]

- name: "Increment boot counter on hardware"
action: script
script: |
from time import sleep_ms
dev._bridge.clear_config()
dev._data = {}
dev.set_boot_count(5)
dev.save()
sleep_ms(200)

dev2 = SteamiConfig(dev._bridge)
dev2.load()
dev2.increment_boot_count()
dev2.save()
sleep_ms(200)

dev3 = SteamiConfig(dev._bridge)
dev3.load()
result = dev3.get_boot_count() == 6
expect_true: true
mode: [hardware]

- name: "Boot counter survives clear_flash"
action: script
script: |
from time import sleep_ms
from daplink_flash import DaplinkFlash
dev._bridge.clear_config()
dev._data = {}
dev.set_boot_count(99)
dev.save()
sleep_ms(200)

flash = DaplinkFlash(dev._bridge)
flash.clear_flash()
sleep_ms(500)

dev2 = SteamiConfig(dev._bridge)
dev2.load()
result = dev2.get_boot_count() == 99
expect_true: true
mode: [hardware]

- name: "Boot counter coexists with temperature calibration on hardware"
action: script
script: |
from time import sleep_ms
dev._bridge.clear_config()
dev._data = {}
dev.set_boot_count(12)
dev.set_temperature_calibration("hts221", gain=1.05, offset=-0.3)
dev.save()
sleep_ms(200)

dev2 = SteamiConfig(dev._bridge)
dev2.load()
tc = dev2.get_temperature_calibration("hts221")
result = (
dev2.get_boot_count() == 12
and tc["gain"] == 1.05
and tc["offset"] == -0.3
)
expect_true: true
mode: [hardware]
Loading