Skip to content
Merged
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
202 changes: 112 additions & 90 deletions lib/steami_config/examples/calibrate_magnetometer.py
Original file line number Diff line number Diff line change
@@ -1,140 +1,162 @@
"""Calibrate the LIS2MDL magnetometer and save to persistent config.
"""Calibrate the LIS2MDL magnetometer using the steami_screen UI.

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.
This example walks through the full calibration flow:

- press MENU to start the calibration,
- rotate the board in all directions while samples are acquired,
- compute hard-iron offsets and soft-iron scale factors,
- save the calibration to persistent SteamiConfig storage,
- reload and verify the calibration survives a fresh config load.

Instructions and a countdown are displayed on the SSD1327 OLED screen.
Press MENU to start the calibration.
"""

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.subtitle(
"Tournez la",
"carte dans",
"toutes les",
"directions",
)
screen.text("Menu=demarrer", at="S")

Comment on lines +58 to +69
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

The new UI still relies on several hard-coded pixel coordinates (e.g., intro/progress text blocks). Since steami_screen provides cardinal positioning (at="N"/"CENTER"/"S") and widgets like subtitle() for multi-line layouts, consider using those instead of magic numbers to keep the example easier to tweak and more resilient if the display size/font rendering changes.

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.

Fixed: replaced hard-coded pixel coordinates with screen.subtitle() for multi-line text and at="S" for bottom positioning. Only show_progress line 75 still uses at=(12, 44) for the "Acquisition..." text which needs a specific vertical position above the countdown value.

screen.show()


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

screen.text("Acquisition...", at=(12, 44))
screen.value(remaining, unit="s")
screen.subtitle("Tournez", "la carte")

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 +166,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