Skip to content

Commit 3918349

Browse files
committed
feat(steami_config): Clean up magnetometer calibration example and docs.
1 parent 9173fa6 commit 3918349

2 files changed

Lines changed: 154 additions & 84 deletions

File tree

lib/steami_config/README.md

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,44 @@ The sensor class name is used for lookup (`HTS221` -> `"hts221"`).
8686

8787
---
8888

89+
## Magnetometer Calibration
90+
91+
Store and restore hard-iron and soft-iron calibration for the LIS2MDL.
92+
93+
### Store calibration
94+
95+
```python
96+
config.set_magnetometer_calibration(
97+
hard_iron_x=12.3, hard_iron_y=-5.1, hard_iron_z=0.8,
98+
soft_iron_x=1.01, soft_iron_y=0.98, soft_iron_z=1.0,
99+
)
100+
```
101+
102+
### Read calibration
103+
104+
```python
105+
cal = config.get_magnetometer_calibration()
106+
# -> {"hard_iron_x": 12.3, ..., "soft_iron_z": 1.0} or None
107+
```
108+
109+
### Apply calibration to a sensor
110+
111+
```python
112+
from lis2mdl import LIS2MDL
113+
114+
mag = LIS2MDL(i2c)
115+
config.apply_magnetometer_calibration(mag)
116+
# mag.x_off, y_off, z_off, x_scale, y_scale, z_scale are now set
117+
```
118+
119+
---
120+
89121
# JSON Format
90122

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

93125
```json
94-
{"rev":3,"name":"STeaMi-01","tc":{"hts":{"g":1.0,"o":-0.5},"pad":{"g":1.0,"o":-1.73}}}
126+
{"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}}
95127
```
96128

97129
| Key | Content |
@@ -101,6 +133,9 @@ Data is stored as compact JSON to fit within 1 KB:
101133
| `tc` | Temperature calibration dict |
102134
| `tc.<key>.g` | Gain factor |
103135
| `tc.<key>.o` | Offset in °C |
136+
| `cm` | Magnetometer calibration dict |
137+
| `cm.hx/hy/hz` | Hard-iron offsets (X, Y, Z) |
138+
| `cm.sx/sy/sz` | Soft-iron scale factors (X, Y, Z) |
104139

105140
Sensor short keys: `hts` (HTS221), `mag` (LIS2MDL), `ism` (ISM330DL),
106141
`hid` (WSEN-HIDS), `pad` (WSEN-PADS).
@@ -113,6 +148,7 @@ Sensor short keys: `hts` (HTS221), `mag` (LIS2MDL), `ism` (ISM330DL),
113148
| ------- | ----------- |
114149
| `show_config.py` | Display current board configuration |
115150
| `calibrate_temperature.py` | Calibrate all sensors against WSEN-HIDS reference |
151+
| `calibrate_magnetometer.py` | Calibrate LIS2MDL with OLED display and persistent storage |
116152

117153
Run with mpremote:
118154

lib/steami_config/examples/calibrate_magnetometer.py

Lines changed: 117 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -5,107 +5,136 @@
55
offsets and soft-iron scale factors are stored in the config zone and
66
survive power cycles.
77
8-
Instructions are displayed on the SSD1327 OLED screen when available.
9-
10-
Usage:
11-
mpremote mount lib/ run lib/steami_config/examples/calibrate_magnetometer.py
12-
13-
When prompted, slowly rotate the board in all directions (tilt, roll,
14-
yaw) for about 12 seconds. The script then saves the calibration and
15-
verifies it by displaying corrected heading readings.
8+
Instructions and a countdown are displayed on the SSD1327 OLED screen.
9+
Press MENU to start the calibration.
1610
"""
1711

1812
import gc
19-
import sys
2013
from time import sleep_ms
2114

22-
from machine import I2C
15+
from daplink_flash import DaplinkFlash
16+
from lis2mdl import LIS2MDL
17+
from machine import I2C, SPI, Pin
18+
from ssd1327 import WS_OLED_128X128_SPI
19+
from steami_config import SteamiConfig
2320

24-
# Add driver paths when running via mpremote mount lib/
25-
for p in ("daplink_flash", "steami_config", "lis2mdl", "ssd1327"):
26-
path = "/remote/" + p
27-
if path not in sys.path:
28-
sys.path.insert(0, path)
21+
# --- Hardware init ---
2922

23+
i2c = I2C(1)
24+
oled = WS_OLED_128X128_SPI(
25+
SPI(1),
26+
Pin("DATA_COMMAND_DISPLAY"),
27+
Pin("RST_DISPLAY"),
28+
Pin("CS_DISPLAY"),
29+
)
30+
btn_menu = Pin("MENU_BUTTON", Pin.IN, Pin.PULL_UP)
3031

31-
def show_screen(i2c, lines):
32-
"""Display text lines on the OLED screen, then free the driver."""
33-
try:
34-
from ssd1327.device import WS_OLED_128X128_I2C
32+
flash = DaplinkFlash(i2c)
33+
config = SteamiConfig(flash)
34+
config.load()
35+
mag = LIS2MDL(i2c)
36+
config.apply_magnetometer_calibration(mag)
3537

36-
oled = WS_OLED_128X128_I2C(i2c)
37-
oled.fill(0)
38-
for i, line in enumerate(lines):
39-
oled.text(line, 0, i * 12, 15)
40-
oled.show()
41-
del oled
42-
except Exception:
43-
pass
44-
sys.modules.pop("ssd1327.device", None)
45-
gc.collect()
4638

39+
# --- Helper functions ---
4740

48-
i2c = I2C(1)
4941

50-
# --- Step 1: Load config and magnetometer ---
42+
def show(lines):
43+
"""Display centered text lines on the round OLED screen."""
44+
oled.fill(0)
45+
th = len(lines) * 12
46+
ys = max(0, (128 - th) // 2)
47+
for i, line in enumerate(lines):
48+
x = max(0, (128 - len(line) * 8) // 2)
49+
oled.text(line, x, ys + i * 12, 15)
50+
oled.show()
5151

52-
from daplink_flash.device import DaplinkFlash # noqa: E402
53-
from steami_config.device import SteamiConfig # noqa: E402
5452

55-
flash = DaplinkFlash(i2c)
56-
config = SteamiConfig(flash)
57-
config.load()
53+
def draw_degree(x, y, col=15):
54+
"""Draw a tiny degree symbol (3x3 circle) at pixel position."""
55+
oled.pixel(x + 1, y, col)
56+
oled.pixel(x, y + 1, col)
57+
oled.pixel(x + 2, y + 1, col)
58+
oled.pixel(x + 1, y + 2, col)
5859

59-
from lis2mdl.device import LIS2MDL # noqa: E402
6060

61-
mag = LIS2MDL(i2c)
61+
def wait_menu():
62+
"""Wait for MENU button press then release."""
63+
while btn_menu.value() == 1:
64+
sleep_ms(10)
65+
while btn_menu.value() == 0:
66+
sleep_ms(10)
67+
68+
69+
# --- Step 1: Display instructions and wait for MENU ---
6270

63-
# Show current state
6471
print("=== Magnetometer Calibration ===\n")
6572
print("Current offsets: x={:.1f} y={:.1f} z={:.1f}".format(
6673
mag.x_off, mag.y_off, mag.z_off))
6774
print("Current scales: x={:.3f} y={:.3f} z={:.3f}\n".format(
6875
mag.x_scale, mag.y_scale, mag.z_scale))
6976

70-
# --- Step 2: Display instructions on screen ---
71-
72-
show_screen(i2c, [
73-
"=== COMPAS ===",
74-
"",
75-
"Calibration du",
76-
"magnetometre",
77-
"",
78-
"Tournez la carte",
79-
"dans toutes les",
80-
"directions...",
81-
"",
82-
"12 secondes",
83-
])
84-
85-
print("Rotate the board slowly in ALL directions for 12 seconds...")
86-
print("(tilt, roll, turn upside down, spin...)\n")
87-
sleep_ms(2000)
88-
89-
# --- Step 3: Run 3D calibration ---
90-
91-
show_screen(i2c, [
92-
"=== COMPAS ===",
77+
show([
78+
"COMPAS",
9379
"",
94-
"Acquisition...",
80+
"Tournez la",
81+
"carte dans",
82+
"toutes les",
83+
"directions",
9584
"",
96-
"Continuez a",
97-
"tourner la carte",
85+
"MENU = demarrer",
9886
])
9987

100-
mag.calibrate_minmax_3d(samples=600, delay_ms=20)
88+
print("Press MENU to start calibration...")
89+
wait_menu()
90+
print("Starting calibration...\n")
91+
92+
# --- Step 2: Acquisition with countdown ---
93+
94+
samples = 600
95+
delay = 20
96+
total_sec = (samples * delay) // 1000
97+
xmin = ymin = zmin = 1e9
98+
xmax = ymax = zmax = -1e9
99+
100+
for s in range(samples):
101+
x, y, z = mag.magnetic_field()
102+
xmin = min(xmin, x)
103+
xmax = max(xmax, x)
104+
ymin = min(ymin, y)
105+
ymax = max(ymax, y)
106+
zmin = min(zmin, z)
107+
zmax = max(zmax, z)
108+
if s % 50 == 0:
109+
remain = total_sec - (s * delay) // 1000
110+
show([
111+
"COMPAS",
112+
"",
113+
"Acquisition...",
114+
"",
115+
"Continuez a",
116+
"tourner",
117+
"",
118+
"{} sec".format(remain),
119+
])
120+
sleep_ms(delay)
121+
122+
mag.x_off = (xmax + xmin) / 2.0
123+
mag.y_off = (ymax + ymin) / 2.0
124+
mag.z_off = (zmax + zmin) / 2.0
125+
mag.x_scale = (xmax - xmin) / 2.0 or 1.0
126+
mag.y_scale = (ymax - ymin) / 2.0 or 1.0
127+
mag.z_scale = (zmax - zmin) / 2.0 or 1.0
101128

102129
print("Calibration complete!")
103130
print(" Hard-iron offsets: x={:.1f} y={:.1f} z={:.1f}".format(
104131
mag.x_off, mag.y_off, mag.z_off))
105132
print(" Soft-iron scales: x={:.3f} y={:.3f} z={:.3f}\n".format(
106133
mag.x_scale, mag.y_scale, mag.z_scale))
107134

108-
# --- Step 4: Save to config zone ---
135+
# --- Step 3: Save to config zone ---
136+
137+
show(["COMPAS", "", "Sauvegarde..."])
109138

110139
config.set_magnetometer_calibration(
111140
hard_iron_x=mag.x_off,
@@ -117,17 +146,11 @@ def show_screen(i2c, lines):
117146
)
118147
config.save()
119148
print("Calibration saved to config zone.\n")
149+
sleep_ms(500)
120150

121-
show_screen(i2c, [
122-
"=== COMPAS ===",
123-
"",
124-
"Calibration",
125-
"sauvegardee !",
126-
"",
127-
"Verification...",
128-
])
151+
# --- Step 4: Verify ---
129152

130-
# --- Step 5: Verify ---
153+
show(["COMPAS", "", "Sauvegarde OK", "", "Verification..."])
131154

132155
gc.collect()
133156
config2 = SteamiConfig(flash)
@@ -137,15 +160,26 @@ def show_screen(i2c, lines):
137160
config2.apply_magnetometer_calibration(mag2)
138161

139162
print("Verification (5 heading readings after reload):")
140-
lines = ["=== COMPAS ===", "", "Verification:"]
163+
result_lines = ["COMPAS", "", "Resultats:"]
141164
for i in range(5):
142165
heading = mag2.heading_flat_only()
143-
norm = mag2.calibrated_field()
144-
line = " {}: cap={:.0f} deg".format(i + 1, heading)
145-
print(" Reading {}: heading={:.1f} deg norm=({:.3f}, {:.3f}, {:.3f})".format(
146-
i + 1, heading, norm[0], norm[1], norm[2]))
147-
lines.append(line)
166+
line = " {}: cap={:.0f}".format(i + 1, heading)
167+
print(" Reading {}: heading={:.1f} deg".format(i + 1, heading))
168+
result_lines.append(line)
148169
sleep_ms(500)
149170

150-
show_screen(i2c, lines + ["", "Termine !"]) # noqa: RUF005
171+
result_lines.append("")
172+
result_lines.append("Termine !")
173+
174+
# Draw results with degree symbols
175+
oled.fill(0)
176+
th = len(result_lines) * 12
177+
ys = max(0, (128 - th) // 2)
178+
for i, line in enumerate(result_lines):
179+
x = max(0, (128 - len(line) * 8) // 2)
180+
oled.text(line, x, ys + i * 12, 15)
181+
if "cap=" in line:
182+
draw_degree(x + len(line) * 8 + 1, ys + i * 12)
183+
oled.show()
184+
151185
print("\nDone! Calibration is stored and will be restored at next boot.")

0 commit comments

Comments
 (0)