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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ Commit messages follow the [Conventional Commits](https://www.conventionalcommit

**Scopes** (optional but enforced): if provided, the scope **must** be one of the allowed values. The scope is recommended for driver-specific changes but can be omitted for cross-cutting changes.

- Driver scopes: `apds9960`, `bme280`, `bq27441`, `daplink_bridge`, `daplink_flash`, `gc9a01`, `hts221`, `im34dt05`, `ism330dl`, `lis2mdl`, `mcp23009e`, `ssd1327`, `steami_config`, `vl53l1x`, `wsen-hids`, `wsen-pads`
- Driver scopes: `apds9960`, `bme280`, `bq27441`, `daplink_bridge`, `daplink_flash`, `gc9a01`, `hts221`, `im34dt05`, `ism330dl`, `lis2mdl`, `mcp23009e`, `ssd1327`, `steami_config`, `vl53l1x`, `wsen-hids`, `wsen-pads`, `steami_screen`
- Domain scopes: `ci`, `docs`, `style`, `tests`, `tooling`

### Examples
Expand Down
1 change: 1 addition & 0 deletions commitlint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ module.exports = {
'style',
'tests',
'tooling',
'steami_screen'
],
],
'type-enum': [
Expand Down
198 changes: 104 additions & 94 deletions lib/steami_config/examples/calibrate_magnetometer.py
Original file line number Diff line number Diff line change
@@ -1,140 +1,150 @@
"""Calibrate the LIS2MDL magnetometer and save to persistent config.

This example runs a 3D min/max calibration by collecting samples while
the user rotates the board in all directions. The computed hard-iron
offsets and soft-iron scale factors are stored in the config zone and
survive power cycles.

Instructions and a countdown are displayed on the SSD1327 OLED screen.
Press MENU to start the calibration.
"""
"""Calibrate the LIS2MDL magnetometer using steami_screen UI."""

import gc
from time import sleep_ms

import ssd1327
from daplink_bridge import DaplinkBridge
from lis2mdl import LIS2MDL
from machine import I2C, SPI, Pin
from ssd1327 import WS_OLED_128X128_SPI
from steami_config import SteamiConfig
from steami_screen import Screen, SSD1327Display

# --- Hardware init ---

i2c = I2C(1)
oled = WS_OLED_128X128_SPI(
SPI(1),
Pin("DATA_COMMAND_DISPLAY"),
Pin("RST_DISPLAY"),
Pin("CS_DISPLAY"),

spi = SPI(1)
dc = Pin("DATA_COMMAND_DISPLAY")
res = Pin("RST_DISPLAY")
cs = Pin("CS_DISPLAY")

display = SSD1327Display(
ssd1327.WS_OLED_128X128_SPI(spi, dc, res, cs)
)

screen = Screen(display)

btn_menu = Pin("MENU_BUTTON", Pin.IN, Pin.PULL_UP)

bridge = DaplinkBridge(i2c)
config = SteamiConfig(bridge)
config.load()

mag = LIS2MDL(i2c)
config.apply_magnetometer_calibration(mag)


# --- Helper functions ---


def show(lines):
"""Display centered text lines on the round OLED screen."""
oled.fill(0)
th = len(lines) * 12
ys = max(0, (128 - th) // 2)
for i, line in enumerate(lines):
x = max(0, (128 - len(line) * 8) // 2)
oled.text(line, x, ys + i * 12, 15)
oled.show()


def draw_degree(x, y, col=15):
"""Draw a tiny degree symbol (3x3 circle) at pixel position."""
oled.pixel(x + 1, y, col)
oled.pixel(x, y + 1, col)
oled.pixel(x + 2, y + 1, col)
oled.pixel(x + 1, y + 2, col)

# --- Helpers ---

def wait_menu():
"""Wait for MENU button press then release."""
while btn_menu.value() == 1:
sleep_ms(10)
while btn_menu.value() == 0:
sleep_ms(10)


# --- Step 1: Display instructions and wait for MENU ---
def show_intro():
screen.clear()
screen.title("COMPAS")

screen.text("Tournez la", at=(22, 38))
screen.text("carte dans", at=(22, 50))
screen.text("toutes les", at=(22, 62))
screen.text("directions", at=(24, 74))
screen.text("Menu=demarrer", at=(10, 90))

screen.show()


def show_progress(remaining):
screen.clear()
screen.title("COMPAS")

screen.text("Acquisition...", at=(12, 44))
screen.value(remaining, at=(30, 60))
screen.text("Tournez", at=(30, 80))
screen.text("la carte", at=(28, 92))

screen.show()


def show_message(*lines):
screen.clear()
screen.title("COMPAS")
screen.subtitle(*lines)
screen.show()


def show_results(readings):
screen.clear()
screen.title("COMPAS")

screen.text("Resultats:", at=(24, 34))

y = 48
for i, heading in enumerate(readings):
line = "{}: {} deg".format(i + 1, int(heading))
screen.text(line, at=(16, y))
y += 12

screen.text("Termine !", at=(28, 112))
screen.show()


# --- Step 1: Instructions ---

print("=== Magnetometer Calibration ===\n")
print("Current offsets: x={:.1f} y={:.1f} z={:.1f}".format(
mag.x_off, mag.y_off, mag.z_off))
print("Current scales: x={:.3f} y={:.3f} z={:.3f}\n".format(
mag.x_scale, mag.y_scale, mag.z_scale))

show([
"COMPAS",
"",
"Tournez la",
"carte dans",
"toutes les",
"directions",
"",
"MENU = demarrer",
])

show_intro()

print("Press MENU to start calibration...")
wait_menu()
print("Starting calibration...\n")

# --- Step 2: Acquisition with countdown ---

# --- Step 2: Acquisition ---

samples = 600
delay = 20
total_sec = (samples * delay) // 1000

xmin = ymin = zmin = 1e9
xmax = ymax = zmax = -1e9

for s in range(samples):
x, y, z = mag.magnetic_field()

xmin = min(xmin, x)
xmax = max(xmax, x)
ymin = min(ymin, y)
ymax = max(ymax, y)
zmin = min(zmin, z)
zmax = max(zmax, z)

if s % 50 == 0:
remain = total_sec - (s * delay) // 1000
show([
"COMPAS",
"",
"Acquisition...",
"",
"Continuez a",
"tourner",
"",
"{} sec".format(remain),
])
remaining = total_sec - (s * delay) // 1000
show_progress(remaining)

sleep_ms(delay)


# --- Compute calibration ---

mag.x_off = (xmax + xmin) / 2.0
mag.y_off = (ymax + ymin) / 2.0
mag.z_off = (zmax + zmin) / 2.0

mag.x_scale = (xmax - xmin) / 2.0 or 1.0
mag.y_scale = (ymax - ymin) / 2.0 or 1.0
mag.z_scale = (zmax - zmin) / 2.0 or 1.0

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

# --- Step 3: Save to config zone ---

show(["COMPAS", "", "Sauvegarde..."])
# --- Step 3: Save ---

show_message("Sauvegarde...")

config.set_magnetometer_calibration(
hard_iron_x=mag.x_off,
Expand All @@ -144,42 +154,42 @@ def wait_menu():
soft_iron_y=mag.y_scale,
soft_iron_z=mag.z_scale,
)

config.save()
print("Calibration saved to config zone.\n")
sleep_ms(500)


# --- Step 4: Verify ---

show(["COMPAS", "", "Sauvegarde OK", "", "Verification..."])
show_message("Sauvegarde OK", "", "Verification...")

gc.collect()

config2 = SteamiConfig(bridge)
config2.load()

mag2 = LIS2MDL(i2c)
config2.apply_magnetometer_calibration(mag2)

print("Verification (5 heading readings after reload):")
result_lines = ["COMPAS", "", "Resultats:"]
print("Verification (5 readings):")

readings = []

for i in range(5):
heading = mag2.heading_flat_only()
line = " {}: cap={:.0f}".format(i + 1, heading)
print(" Reading {}: heading={:.1f} deg".format(i + 1, heading))
result_lines.append(line)
readings.append(heading)

screen.clear()
screen.title("COMPAS")
screen.value(int(heading), unit="deg", label="Mesure {}".format(i + 1))
screen.show()

print("Reading {}: {:.1f} deg".format(i + 1, heading))
sleep_ms(500)

result_lines.append("")
result_lines.append("Termine !")

# Draw results with degree symbols
oled.fill(0)
th = len(result_lines) * 12
ys = max(0, (128 - th) // 2)
for i, line in enumerate(result_lines):
x = max(0, (128 - len(line) * 8) // 2)
oled.text(line, x, ys + i * 12, 15)
if "cap=" in line:
draw_degree(x + len(line) * 8 + 1, ys + i * 12)
oled.show()

print("\nDone! Calibration is stored and will be restored at next boot.")

# --- Done ---

show_results(readings)

print("\nDone! Calibration stored.")
Loading
Loading