Skip to content

Commit fd7145b

Browse files
committed
steami_config: Add persistent configuration module.
1 parent 18eb8aa commit fd7145b

10 files changed

Lines changed: 606 additions & 2 deletions

File tree

lib/steami_config/README.md

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# STeaMi Config
2+
3+
Persistent configuration module for the STeaMi board.
4+
5+
Configuration data (board info, sensor calibration) is stored as compact JSON
6+
in the STM32F103 internal flash config zone (1 KB) and survives firmware
7+
updates and `clear_flash` operations.
8+
9+
---
10+
11+
# Dependencies
12+
13+
* `daplink_flash` — low-level config zone access
14+
15+
---
16+
17+
# Basic Usage
18+
19+
```python
20+
from machine import I2C
21+
from daplink_flash import DaplinkFlash
22+
from steami_config import SteamiConfig
23+
24+
i2c = I2C(1)
25+
config = SteamiConfig(DaplinkFlash(i2c))
26+
config.load()
27+
28+
print(config.board_name)
29+
print(config.board_revision)
30+
```
31+
32+
---
33+
34+
# API
35+
36+
## Persistence
37+
38+
```python
39+
config.load() # read config zone -> JSON -> internal dict
40+
config.save() # internal dict -> JSON -> config zone
41+
```
42+
43+
---
44+
45+
## Board Info
46+
47+
```python
48+
config.board_revision # -> int or None
49+
config.board_revision = 3
50+
51+
config.board_name # -> str or None
52+
config.board_name = "STeaMi-01"
53+
```
54+
55+
---
56+
57+
## Temperature Calibration
58+
59+
Five sensors are supported: `hts221`, `lis2mdl`, `ism330dl`, `wsen_hids`,
60+
`wsen_pads`.
61+
62+
### Store calibration
63+
64+
```python
65+
config.set_temperature_calibration("hts221", gain=1.0, offset=-0.5)
66+
```
67+
68+
### Read calibration
69+
70+
```python
71+
cal = config.get_temperature_calibration("hts221")
72+
# -> {"gain": 1.0, "offset": -0.5} or None
73+
```
74+
75+
### Apply calibration to a sensor
76+
77+
```python
78+
from hts221 import HTS221
79+
80+
hts = HTS221(i2c)
81+
config.apply_temperature_calibration(hts)
82+
# hts._temp_gain and hts._temp_offset are now set
83+
```
84+
85+
The sensor class name is used for lookup (`HTS221` -> `"hts221"`).
86+
87+
---
88+
89+
# JSON Format
90+
91+
Data is stored as compact JSON to fit within 1 KB:
92+
93+
```json
94+
{"rev":3,"name":"STeaMi-01","tc":{"hts":{"g":1.0,"o":-0.5},"pad":{"g":1.0,"o":-1.73}}}
95+
```
96+
97+
| Key | Content |
98+
| --- | ------- |
99+
| `rev` | Board revision (int) |
100+
| `name` | Board name (str) |
101+
| `tc` | Temperature calibration dict |
102+
| `tc.<key>.g` | Gain factor |
103+
| `tc.<key>.o` | Offset in °C |
104+
105+
Sensor short keys: `hts` (HTS221), `mag` (LIS2MDL), `ism` (ISM330DL),
106+
`hid` (WSEN-HIDS), `pad` (WSEN-PADS).
107+
108+
---
109+
110+
# Examples
111+
112+
| Example | Description |
113+
| ------- | ----------- |
114+
| `show_config.py` | Display current board configuration |
115+
| `calibrate_temperature.py` | Calibrate all sensors against WSEN-HIDS reference |
116+
117+
Run with mpremote:
118+
119+
```sh
120+
mpremote mount lib exec "
121+
import sys
122+
sys.path.insert(0, '/remote/steami_config')
123+
sys.path.insert(0, '/remote/daplink_flash')
124+
exec(open('/remote/steami_config/examples/show_config.py').read())
125+
"
126+
```
127+
128+
---
129+
130+
# Memory Note
131+
132+
Loading all five sensor drivers simultaneously exceeds the STM32WB55 RAM.
133+
Use `gc.collect()` between sensor imports (see `calibrate_temperature.py`).
134+
See issue #175 for investigation.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""Calibrate all temperature sensors against WSEN-HIDS reference.
2+
3+
This example reads each sensor one at a time to stay within RAM
4+
limits on the STM32WB55. Calibration offsets are stored in the
5+
persistent config zone and survive power cycles.
6+
"""
7+
8+
import gc
9+
from machine import I2C
10+
from time import sleep_ms
11+
12+
from daplink_flash.device import DaplinkFlash
13+
from steami_config.device import SteamiConfig
14+
from wsen_hids.device import WSEN_HIDS
15+
16+
i2c = I2C(1)
17+
flash = DaplinkFlash(i2c)
18+
config = SteamiConfig(flash)
19+
config.load()
20+
21+
# Read reference temperature from WSEN-HIDS (most accurate at ambient)
22+
ref_temp = WSEN_HIDS(i2c).temperature()
23+
print("Reference (WSEN-HIDS): {:.2f} C".format(ref_temp))
24+
config.set_temperature_calibration("wsen_hids", gain=1.0, offset=0.0)
25+
del WSEN_HIDS
26+
gc.collect()
27+
28+
# Calibrate each sensor one at a time to save RAM
29+
SENSORS = [
30+
("hts221", "hts221.device", "HTS221", "temperature"),
31+
("wsen_pads", "wsen_pads.device", "WSEN_PADS", "temperature"),
32+
("lis2mdl", "lis2mdl.device", "LIS2MDL", "temperature"),
33+
("ism330dl", "ism330dl.device", "ISM330DL", "temperature_c"),
34+
]
35+
36+
for config_name, module, class_name, method in SENSORS:
37+
mod = __import__(module, None, None, [class_name])
38+
cls = getattr(mod, class_name)
39+
sensor = cls(i2c)
40+
if config_name == "ism330dl":
41+
sleep_ms(200)
42+
raw = getattr(sensor, method)()
43+
offset = ref_temp - raw
44+
config.set_temperature_calibration(config_name, gain=1.0, offset=offset)
45+
print(" {:10s}: {:6.2f} C -> offset {:+.2f}".format(config_name, raw, offset))
46+
del sensor, cls, mod
47+
gc.collect()
48+
49+
config.save()
50+
print("\nCalibration saved.")
51+
52+
# Verify by reloading
53+
gc.collect()
54+
config2 = SteamiConfig(flash)
55+
config2.load()
56+
57+
print("\nVerification (after reload):")
58+
for config_name, module, class_name, method in SENSORS:
59+
mod = __import__(module, None, None, [class_name])
60+
cls = getattr(mod, class_name)
61+
sensor = cls(i2c)
62+
if config_name == "ism330dl":
63+
sleep_ms(200)
64+
config2.apply_temperature_calibration(sensor)
65+
print(" {:10s}: {:6.2f} C".format(config_name, getattr(sensor, method)()))
66+
del sensor, cls, mod
67+
gc.collect()
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""Display the current board configuration stored in the config zone."""
2+
3+
from machine import I2C
4+
5+
from daplink_flash import DaplinkFlash
6+
from steami_config import SteamiConfig
7+
8+
i2c = I2C(1)
9+
config = SteamiConfig(DaplinkFlash(i2c))
10+
config.load()
11+
12+
print("=== STeaMi Configuration ===")
13+
print()
14+
15+
rev = config.board_revision
16+
name = config.board_name
17+
print("Board revision:", rev if rev is not None else "(not set)")
18+
print("Board name: ", name if name is not None else "(not set)")
19+
20+
sensors = ["hts221", "lis2mdl", "ism330dl", "wsen_hids", "wsen_pads"]
21+
print()
22+
print("Temperature calibration:")
23+
has_cal = False
24+
for sensor in sensors:
25+
cal = config.get_temperature_calibration(sensor)
26+
if cal is not None:
27+
has_cal = True
28+
print(" {:10s} gain={:.4f} offset={:+.2f} C".format(
29+
sensor, cal["gain"], cal["offset"],
30+
))
31+
if not has_cal:
32+
print(" (none)")
33+
34+
print()
35+
print("Raw JSON:", config._data)

lib/steami_config/manifest.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
metadata(
2+
description="Persistent configuration for the STeaMi board",
3+
version="0.0.1",
4+
)
5+
6+
package("steami_config")
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from steami_config.device import SteamiConfig
2+
3+
__all__ = [
4+
"SteamiConfig",
5+
]
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import json
2+
3+
4+
# Map sensor class names to short JSON keys to save space.
5+
_SENSOR_KEYS = {
6+
"hts221": "hts",
7+
"lis2mdl": "mag",
8+
"ism330dl": "ism",
9+
"wsen_hids": "hid",
10+
"wsen_pads": "pad",
11+
}
12+
13+
# Reverse map: short key -> sensor name.
14+
_KEY_SENSORS = {v: k for k, v in _SENSOR_KEYS.items()}
15+
16+
17+
class SteamiConfig(object):
18+
"""Persistent configuration stored in the DAPLink F103 config zone.
19+
20+
Data is serialised as compact JSON and written via
21+
``DaplinkFlash.write_config()`` / ``read_config()``.
22+
23+
Args:
24+
flash: a ``DaplinkFlash`` instance.
25+
"""
26+
27+
def __init__(self, flash):
28+
self._flash = flash
29+
self._data = {}
30+
31+
# --------------------------------------------------
32+
# Persistence
33+
# --------------------------------------------------
34+
35+
def load(self):
36+
"""Load configuration from the config zone."""
37+
raw = self._flash.read_config()
38+
if raw:
39+
self._data = json.loads(raw)
40+
else:
41+
self._data = {}
42+
43+
def save(self):
44+
"""Save configuration to the config zone."""
45+
self._flash.clear_config()
46+
self._flash.write_config(json.dumps(self._data, separators=(",", ":")))
47+
48+
# --------------------------------------------------
49+
# Board info
50+
# --------------------------------------------------
51+
52+
@property
53+
def board_revision(self):
54+
"""Board hardware revision number, or None."""
55+
return self._data.get("rev")
56+
57+
@board_revision.setter
58+
def board_revision(self, value):
59+
self._data["rev"] = int(value)
60+
61+
@property
62+
def board_name(self):
63+
"""Board name string, or None."""
64+
return self._data.get("name")
65+
66+
@board_name.setter
67+
def board_name(self, value):
68+
self._data["name"] = str(value)
69+
70+
# --------------------------------------------------
71+
# Temperature calibration
72+
# --------------------------------------------------
73+
74+
def set_temperature_calibration(self, sensor, gain=1.0, offset=0.0):
75+
"""Store temperature calibration for a sensor.
76+
77+
Args:
78+
sensor: sensor name (e.g. ``"hts221"``, ``"wsen_pads"``).
79+
gain: multiplicative gain factor.
80+
offset: additive offset in degrees Celsius.
81+
"""
82+
key = _SENSOR_KEYS.get(sensor)
83+
if key is None:
84+
raise ValueError("unknown sensor: {}".format(sensor))
85+
tc = self._data.get("tc")
86+
if tc is None:
87+
tc = {}
88+
self._data["tc"] = tc
89+
tc[key] = {"g": gain, "o": offset}
90+
91+
def get_temperature_calibration(self, sensor):
92+
"""Return temperature calibration for a sensor.
93+
94+
Returns:
95+
dict with ``"gain"`` and ``"offset"`` keys, or None.
96+
"""
97+
key = _SENSOR_KEYS.get(sensor)
98+
if key is None:
99+
raise ValueError("unknown sensor: {}".format(sensor))
100+
tc = self._data.get("tc")
101+
if tc is None:
102+
return None
103+
entry = tc.get(key)
104+
if entry is None:
105+
return None
106+
return {"gain": entry["g"], "offset": entry["o"]}
107+
108+
def apply_temperature_calibration(self, sensor_instance):
109+
"""Apply stored calibration to a sensor instance.
110+
111+
The sensor class name is used to look up calibration data.
112+
The instance must have ``_temp_gain`` and ``_temp_offset``
113+
attributes (all STeaMi temperature sensors do).
114+
115+
Args:
116+
sensor_instance: a driver instance (e.g. ``HTS221``).
117+
"""
118+
class_name = type(sensor_instance).__name__.lower()
119+
cal = self.get_temperature_calibration(class_name)
120+
if cal is None:
121+
return
122+
sensor_instance._temp_gain = cal["gain"]
123+
sensor_instance._temp_offset = cal["offset"]

tests/runner/executor.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,9 @@ def run_action(action, driver_instance):
154154
return method(*args)
155155

156156
if action_type == "script":
157-
script_vars = {"dev": driver_instance, "i2c": driver_instance.i2c}
157+
script_vars = {"dev": driver_instance, "i2c": getattr(driver_instance, "i2c", None)}
158+
# Expose the driver class so scripts can create new instances
159+
script_vars[type(driver_instance).__name__] = type(driver_instance)
158160
exec(action["script"], script_vars, script_vars)
159161
if "result" not in script_vars:
160162
raise ValueError("Script must set a 'result' variable")

0 commit comments

Comments
 (0)