From ee22c3f5d8f811bdef226d145ea2e756791d2183 Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Fri, 27 Feb 2026 12:56:02 +0100 Subject: [PATCH 01/10] iio: switch to maximum sampling frequency, apply mount matrix --- .../lib/mpos/imu/drivers/iio.py | 140 +++++++++++++++++- 1 file changed, 139 insertions(+), 1 deletion(-) diff --git a/internal_filesystem/lib/mpos/imu/drivers/iio.py b/internal_filesystem/lib/mpos/imu/drivers/iio.py index 71d71830..bc677cae 100644 --- a/internal_filesystem/lib/mpos/imu/drivers/iio.py +++ b/internal_filesystem/lib/mpos/imu/drivers/iio.py @@ -17,7 +17,9 @@ class IIODriver(IMUDriverBase): def __init__(self): super().__init__() self.accel_path = self.find_iio_device_with_file("in_accel_x_raw") + self.ensure_sampling_frequency_max(self.accel_path) self.mag_path = self.find_iio_device_with_file("in_magn_x_raw") + self.ensure_sampling_frequency_max(self.mag_path) def _p(self, name: str): return self.accel_path + "/" + name @@ -76,6 +78,92 @@ def _read_text(self, name: str) -> str: finally: f.close() + def _parse_available_freqs(self, text): + """ + IIO typically uses either: + "12.5 25 50 100" + or + "0.5 1 2 4 8 16" + + Returns list of floats. + """ + out = [] + for tok in text.replace(",", " ").split(): + out.append(float(tok)) + return out + + def _format_freq_for_sysfs(self, f): + """ + Kernel sysfs usually accepts either integer or decimal. + We'll keep it minimal: + - if f is whole number -> "100" + - else -> "12.5" + """ + if int(f) == f: + return str(int(f)) + # avoid scientific notation + s = ("%.6f" % f).rstrip("0").rstrip(".") + return s + + def _try_set_via_sudo_tee(self, path, value_str): + """ + Executes: + sh -c 'echo VALUE | sudo tee PATH' + Returns True if command returns 0. + """ + cmd = "sh -c 'echo %s | sudo tee %s >/dev/null'" % (value_str, path) + rc = os.system(cmd) + return rc == 0 + + def ensure_sampling_frequency_max(self, dev_path): + """ + dev_path: "/sys/bus/iio/devices/iio:deviceX" + + Returns: + (changed: bool, max_freq: float or None, current: float or None) + """ + sf = dev_path + "/sampling_frequency" + sfa = dev_path + "/sampling_frequency_available" + + # read current + cur_s = self._read_text(sf) + cur = float(cur_s) + + avail_s = self._read_text(sfa) + avail = self._parse_available_freqs(avail_s) + + maxf = max(avail) + + # already max (tolerate float fuzz) + if abs(cur - maxf) < 1e-6: + print("Already at max frequency") + return (False, maxf, cur) + + max_str = self._format_freq_for_sysfs(maxf) + + # Fallback: sudo tee + ok = self._try_set_via_sudo_tee(sf, max_str) + if not ok: + print("Can't switch to max frequency") + return (False, maxf, cur) + + new_cur = float(self._read_text(sf)) + + return (True, maxf, new_cur) + + def ensure_sampling_frequency_max_for_device_with_file(self, filename): + """ + Convenience wrapper: + - finds iio device containing filename + - sets sampling_frequency to maximum + """ + dev = self.find_iio_device_with_file(filename) + if dev is None: + return (None, False, None, None) + + changed, maxf, cur = self.ensure_sampling_frequency_max(dev) + return (dev, changed, maxf, cur) + def _read_float(self, name: str) -> float: return float(self._read_text(name)) @@ -93,6 +181,7 @@ def read_temperature(self) -> float: - in_temp_input (already scaled, usually millidegree C) - in_temp_raw + in_temp_scale """ + return 12.34 if not self.accel_path: return None @@ -102,6 +191,55 @@ def read_temperature(self) -> float: return None return self._read_raw_scaled(raw_path, scale_path) + def _read_mount_matrix(self): + """ + Reads IIO mount matrix from: + in_accel_mount_matrix + + Format example: + "0, 1, 0; -1, 0, 0; 0, 0, 1" + + Returns: + 3x3 matrix as tuple of tuples (float) + """ + if not self.accel_path: + return None + + path = self.accel_path + "/" + "in_accel_mount_matrix" + if not self._exists(path): + # Strange, librem 5 has different filename + path = self.accel_path + "/" + "mount_matrix" + if not self._exists(path): + return None + + text = self._read_text(path).strip() + + rows = [] + for row in text.split(";"): + rows.append(tuple(float(x.strip()) for x in row.split(","))) + + if len(rows) != 3 or any(len(r) != 3 for r in rows): + raise ValueError("Invalid mount matrix format") + + return tuple(rows) + + + def _apply_mount_matrix(self, ax, ay, az): + """ + Applies IIO mount matrix to acceleration vector. + + Returns rotated (ax, ay, az). + """ + M = self._read_mount_matrix() + if M is None: + return (ax, ay, az) + + x = M[0][0]*ax + M[0][1]*ay + M[0][2]*az + y = M[1][0]*ax + M[1][1]*ay + M[1][2]*az + z = M[2][0]*ax + M[2][1]*ay + M[2][2]*az + + return (x, y, z) + def _raw_acceleration_mps2(self): if not self.accel_path: return (0.0, 0.0, 0.0) @@ -111,7 +249,7 @@ def _raw_acceleration_mps2(self): ay = self._read_raw_scaled(self.accel_path + "/" + "in_accel_y_raw", scale_name) az = self._read_raw_scaled(self.accel_path + "/" + "in_accel_z_raw", scale_name) - return (ax, ay, az) + return self._apply_mount_matrix(ax, ay, az) def _raw_gyroscope_dps(self): if not self.accel_path: From 508625f1749729bc474be98dd794fbffa51bf792 Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Sun, 1 Mar 2026 15:52:14 +0100 Subject: [PATCH 02/10] mag: Apply mount matrix for magnetometer, too --- .../lib/mpos/imu/drivers/iio.py | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/internal_filesystem/lib/mpos/imu/drivers/iio.py b/internal_filesystem/lib/mpos/imu/drivers/iio.py index bc677cae..4ec469ae 100644 --- a/internal_filesystem/lib/mpos/imu/drivers/iio.py +++ b/internal_filesystem/lib/mpos/imu/drivers/iio.py @@ -191,10 +191,9 @@ def read_temperature(self) -> float: return None return self._read_raw_scaled(raw_path, scale_path) - def _read_mount_matrix(self): + def _read_mount_matrix(self, p): """ - Reads IIO mount matrix from: - in_accel_mount_matrix + Reads IIO mount matrix from *mount_matrix Format example: "0, 1, 0; -1, 0, 0; 0, 0, 1" @@ -202,10 +201,7 @@ def _read_mount_matrix(self): Returns: 3x3 matrix as tuple of tuples (float) """ - if not self.accel_path: - return None - - path = self.accel_path + "/" + "in_accel_mount_matrix" + path = p + "/" + "in_accel_mount_matrix" if not self._exists(path): # Strange, librem 5 has different filename path = self.accel_path + "/" + "mount_matrix" @@ -224,13 +220,13 @@ def _read_mount_matrix(self): return tuple(rows) - def _apply_mount_matrix(self, ax, ay, az): + def _apply_mount_matrix(self, ax, ay, az, p): """ Applies IIO mount matrix to acceleration vector. Returns rotated (ax, ay, az). """ - M = self._read_mount_matrix() + M = self._read_mount_matrix(p) if M is None: return (ax, ay, az) @@ -249,7 +245,7 @@ def _raw_acceleration_mps2(self): ay = self._read_raw_scaled(self.accel_path + "/" + "in_accel_y_raw", scale_name) az = self._read_raw_scaled(self.accel_path + "/" + "in_accel_z_raw", scale_name) - return self._apply_mount_matrix(ax, ay, az) + return self._apply_mount_matrix(ax, ay, az, self.accel_path) def _raw_gyroscope_dps(self): if not self.accel_path: @@ -260,7 +256,7 @@ def _raw_gyroscope_dps(self): gy = self._read_raw_scaled(self.accel_path + "/" + "in_anglvel_y_raw", scale_name) gz = self._read_raw_scaled(self.accel_path + "/" + "in_anglvel_z_raw", scale_name) - return (gx, gy, gz) + return self._apply_mount_matrix(gx, gy, gz, self.accel_path) def read_acceleration(self): ax, ay, az = self._raw_acceleration_mps2() @@ -283,4 +279,4 @@ def read_magnetometer(self) -> tuple[float, float, float]: gy = self._read_raw_scaled(self.mag_path + "/" + "in_magn_y_raw", self.mag_path + "/" + "in_magn_y_scale") gz = self._read_raw_scaled(self.mag_path + "/" + "in_magn_z_raw", self.mag_path + "/" + "in_magn_z_scale") - return (gx, gy, gz) + return self._apply_mount_matrix(gx, gy, gz, self.mag_path) From 7403d25f1a41d3d54f02f4a187b3d9eeebaed3fa Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Mon, 2 Mar 2026 19:07:48 +0100 Subject: [PATCH 03/10] iio: allow separate path for gyro sensor --- internal_filesystem/lib/mpos/imu/drivers/iio.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/internal_filesystem/lib/mpos/imu/drivers/iio.py b/internal_filesystem/lib/mpos/imu/drivers/iio.py index 4ec469ae..a26835ef 100644 --- a/internal_filesystem/lib/mpos/imu/drivers/iio.py +++ b/internal_filesystem/lib/mpos/imu/drivers/iio.py @@ -13,6 +13,7 @@ class IIODriver(IMUDriverBase): accel_path: str mag_path: str + gyro_path: str def __init__(self): super().__init__() @@ -20,7 +21,9 @@ def __init__(self): self.ensure_sampling_frequency_max(self.accel_path) self.mag_path = self.find_iio_device_with_file("in_magn_x_raw") self.ensure_sampling_frequency_max(self.mag_path) - + self.gyro_path = self.find_iio_device_with_file("in_anglvel_x_raw") + self.ensure_sampling_frequency_max(self.gyro_path) + def _p(self, name: str): return self.accel_path + "/" + name @@ -248,15 +251,15 @@ def _raw_acceleration_mps2(self): return self._apply_mount_matrix(ax, ay, az, self.accel_path) def _raw_gyroscope_dps(self): - if not self.accel_path: + if not self.gyro_path: return (0.0, 0.0, 0.0) - scale_name = self.accel_path + "/" + "in_anglvel_scale" + scale_name = self.gyro_path + "/" + "in_anglvel_scale" - gx = self._read_raw_scaled(self.accel_path + "/" + "in_anglvel_x_raw", scale_name) - gy = self._read_raw_scaled(self.accel_path + "/" + "in_anglvel_y_raw", scale_name) - gz = self._read_raw_scaled(self.accel_path + "/" + "in_anglvel_z_raw", scale_name) + gx = self._read_raw_scaled(self.gyro_path + "/" + "in_anglvel_x_raw", scale_name) + gy = self._read_raw_scaled(self.gyro_path + "/" + "in_anglvel_y_raw", scale_name) + gz = self._read_raw_scaled(self.gyro_path + "/" + "in_anglvel_z_raw", scale_name) - return self._apply_mount_matrix(gx, gy, gz, self.accel_path) + return self._apply_mount_matrix(gx, gy, gz, self.gyro_path) def read_acceleration(self): ax, ay, az = self._raw_acceleration_mps2() From 8854439c3cbf1814f80c053528790121a544059e Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Mon, 2 Mar 2026 21:28:13 +0100 Subject: [PATCH 04/10] gyro: fork from compass, get drawing back to work --- .../cz.ucw.pavel.gyro/META-INF/MANIFEST.JSON | 24 + .../apps/cz.ucw.pavel.gyro/assets/main.py | 527 ++++++++++++++++++ .../res/mipmap-mdpi/icon_64x64.png | Bin 0 -> 6765 bytes 3 files changed, 551 insertions(+) create mode 100644 internal_filesystem/apps/cz.ucw.pavel.gyro/META-INF/MANIFEST.JSON create mode 100644 internal_filesystem/apps/cz.ucw.pavel.gyro/assets/main.py create mode 100644 internal_filesystem/apps/cz.ucw.pavel.gyro/res/mipmap-mdpi/icon_64x64.png diff --git a/internal_filesystem/apps/cz.ucw.pavel.gyro/META-INF/MANIFEST.JSON b/internal_filesystem/apps/cz.ucw.pavel.gyro/META-INF/MANIFEST.JSON new file mode 100644 index 00000000..9e6b0742 --- /dev/null +++ b/internal_filesystem/apps/cz.ucw.pavel.gyro/META-INF/MANIFEST.JSON @@ -0,0 +1,24 @@ +{ +"name": "Xxx", +"publisher": "Pavel Machek", +"short_description": "Xxx", +"long_description": "Simple xxx app.", +"icon_url": "https://apps.micropythonos.com/apps/cz.ucw.pavel.xxx/icons/cz.ucw.pavel.xxx_0.0.1_64x64.png", +"download_url": "https://apps.micropythonos.com/apps/cz.ucw.pavel.xxx/mpks/cz.ucw.pavel.xxx_0.0.1.mpk", +"fullname": "cz.ucw.pavel.xxx", +"version": "0.0.1", +"category": "utilities", +"activities": [ + { + "entrypoint": "assets/main.py", + "classname": "Main", + "intent_filters": [ + { + "action": "main", + "category": "launcher" + } + ] + } + ] +} + diff --git a/internal_filesystem/apps/cz.ucw.pavel.gyro/assets/main.py b/internal_filesystem/apps/cz.ucw.pavel.gyro/assets/main.py new file mode 100644 index 00000000..479fd29e --- /dev/null +++ b/internal_filesystem/apps/cz.ucw.pavel.gyro/assets/main.py @@ -0,0 +1,527 @@ +""" +xxx + + +""" + +import time +import os +import math + +try: + import lvgl as lv +except ImportError: + pass + +from mpos import Activity, MposKeyboard, SensorManager + +# ----------------------------- +# Utilities +# ----------------------------- + +def clamp(v, lo, hi): + if v < lo: + return lo + if v > hi: + return hi + return v + +def to_rad(deg): + return deg * math.pi / 180.0 + +def to_deg(rad): + return rad * 180.0 / math.pi + +# ----------------------------- +# Calibration + heading +# ----------------------------- + +class Gyro: + def reset(self): + pass + +class TiltGyro(Gyro): + def __init__(self): + super().__init__() + self.heading = 0 + self.last = time.time() + self.smooth = 0.0 + + def heading_tilted(self): + """ + Returns heading 0..360 + + iio is in rads/second + """ + t = time.time() + v = self.val[1] * 57.2957795 + coef = 0.1 + self.smooth = self.smooth * (1-coef) + v * coef + self.heading += self.smooth * (t - self.last) + self.last = t + self.angvel = v + return self.smooth + +# ----------------------------- +# Canvas (LVGL) +# ----------------------------- + +class Canvas: + """ + LVGL canvas + layer drawing Canvas. + + This matches ports where: + - lv.canvas has init_layer() / finish_layer() + - primitives are drawn via lv.draw_* into lv.layer_t + """ + + def __init__(self, scr, canvas): + self.scr = scr + + # Screen size + self.W = scr.get_width() + self.H = scr.get_height() + + # Bottom button bar + self.margin = 2 + self.bar_h = 39 + + # Canvas drawing area (everything above button bar) + self.draw_w = self.W + self.draw_h = self.H - (self.bar_h + self.margin * 2) + + self.canvas = canvas + + # Background: white (change if you want dark theme) + self.canvas.set_style_bg_color(lv.color_white(), lv.PART.MAIN) + + # Buffer: your working example uses 4 bytes/pixel + # Reality filter: this depends on LV_COLOR_DEPTH; but your example proves it works. + self.buf = bytearray(self.draw_w * self.draw_h * 4) + self.canvas.set_buffer(self.buf, self.draw_w, self.draw_h, lv.COLOR_FORMAT.NATIVE) + + # Layer used for draw engine + self.layer = lv.layer_t() + self.canvas.init_layer(self.layer) + + # Persistent draw descriptors (avoid allocations) + self._line_dsc = lv.draw_line_dsc_t() + lv.draw_line_dsc_t.init(self._line_dsc) + self._line_dsc.width = 1 + self._line_dsc.color = lv.color_black() + self._line_dsc.round_end = 1 + self._line_dsc.round_start = 1 + + self._label_dsc = lv.draw_label_dsc_t() + lv.draw_label_dsc_t.init(self._label_dsc) + self._label_dsc.color = lv.color_black() + self._label_dsc.font = lv.font_montserrat_24 + + self._rect_dsc = lv.draw_rect_dsc_t() + lv.draw_rect_dsc_t.init(self._rect_dsc) + self._rect_dsc.bg_opa = lv.OPA.TRANSP + self._rect_dsc.border_opa = lv.OPA.COVER + self._rect_dsc.border_width = 1 + self._rect_dsc.border_color = lv.color_black() + + self._fill_dsc = lv.draw_rect_dsc_t() + lv.draw_rect_dsc_t.init(self._fill_dsc) + self._fill_dsc.bg_opa = lv.OPA.COVER + self._fill_dsc.bg_color = lv.color_black() + self._fill_dsc.border_width = 1 + + # Clear once + self.clear() + + # ---------------------------- + # Layer lifecycle + # ---------------------------- + + def _begin(self): + # Start drawing into the layer + self.canvas.init_layer(self.layer) + + def _end(self): + # Commit drawing + self.canvas.finish_layer(self.layer) + + # ---------------------------- + # Public API: drawing + # ---------------------------- + + def clear(self): + # Clear the canvas background + self.canvas.fill_bg(lv.color_white(), lv.OPA.COVER) + + def text(self, x, y, s, fg = lv.color_black()): + self._begin() + + dsc = lv.draw_label_dsc_t() + lv.draw_label_dsc_t.init(dsc) + dsc.text = str(s) + dsc.font = lv.font_montserrat_24 + dsc.color = lv.color_black() + + area = lv.area_t() + area.x1 = x + area.y1 = y + area.x2 = x + self.W + area.y2 = y + self.H + + lv.draw_label(self.layer, dsc, area) + + self._end() + + def line(self, x1, y1, x2, y2, fg = lv.color_black()): + self._begin() + + dsc = self._line_dsc + dsc.p1 = lv.point_precise_t() + dsc.p2 = lv.point_precise_t() + dsc.p1.x = int(x1) + dsc.p1.y = int(y1) + dsc.p2.x = int(x2) + dsc.p2.y = int(y2) + + lv.draw_line(self.layer, dsc) + + self._end() + + def circle(self, x, y, r, fg = lv.color_black()): + # Rounded rectangle trick (works everywhere) + self._begin() + + a = lv.area_t() + a.x1 = int(x - r) + a.y1 = int(y - r) + a.x2 = int(x + r) + a.y2 = int(y + r) + + dsc = self._rect_dsc + dsc.radius = lv.RADIUS_CIRCLE + dsc.border_color = fg + + lv.draw_rect(self.layer, dsc, a) + + self._end() + + def fill_circle(self, x, y, r, fg = lv.color_black(), bg = lv.color_white()): + self._begin() + + a = lv.area_t() + a.x1 = int(x - r) + a.y1 = int(y - r) + a.x2 = int(x + r) + a.y2 = int(y + r) + + dsc = self._rect_dsc + dsc.radius = lv.RADIUS_CIRCLE + dsc.border_color = fg + dsc.bg_color = bg + + lv.draw_rect(self.layer, dsc, a) + + self._end() + + def fill_rect(self, x, y, sx, sy, fg = lv.color_black(), bg = lv.color_white()): + self._begin() + + a = lv.area_t() + a.x1 = x + a.y1 = y + a.x2 = x+sx + a.y2 = y+sy + + dsc = self._fill_dsc + dsc.border_color = fg + dsc.bg_color = bg + + lv.draw_rect(self.layer, dsc, a) + + self._end() + + def update(self): + # Nothing needed; drawing is committed per primitive. + # If you want, you can change the implementation so that: + # - draw ops happen between clear() and update() + # But then you must ensure the app calls update() once per frame. + pass + +# ---------------------------- +# App logic +# ---------------------------- + +class PagedCanvas(Activity): + def __init__(self): + super().__init__() + self.page = 0 + self.pages = 3 + + def onCreate(self): + self.scr = lv.obj() + scr = self.scr + + # Screen size + self.W = scr.get_width() + self.H = scr.get_height() + + # Bottom button bar + self.margin = 2 + self.bar_h = 39 + + # Canvas drawing area (everything above button bar) + self.draw_w = self.W + self.draw_h = self.H - (self.bar_h + self.margin * 2) + + # Canvas + self.canvas = lv.canvas(self.scr) + self.canvas.set_size(self.draw_w, self.draw_h) + self.canvas.align(lv.ALIGN.TOP_LEFT, 0, 0) + self.canvas.set_style_border_width(0, 0) + + self.c = Canvas(self.scr, self.canvas) + + # Build buttons + self.build_buttons() + self.setContentView(self.c.scr) + + # ---------------------------- + # Button bar + # ---------------------------- + + def _make_btn(self, parent, x, y, w, h, label): + b = lv.button(parent) + b.set_pos(x, y) + b.set_size(w, h) + + l = lv.label(b) + l.set_text(label) + l.center() + + return b + + def _btn_cb(self, evt, tag): + self.page = tag + + def template_buttons(self, names): + margin = self.margin + y = self.H - self.bar_h - margin + + num = len(names) + if num == 0: + self.buttons = [] + return + + w = (self.W - margin * (num + 1)) // num + h = self.bar_h + x0 = margin + + self.buttons = [] + + for i, label in enumerate(names): + x = x0 + (w + margin) * i + btn = self._make_btn(self.scr, x, y, w, h, label) + + # capture index correctly + btn.add_event_cb( + lambda evt, idx=i: self._btn_cb(evt, idx), + lv.EVENT.CLICKED, + None + ) + + self.buttons.append(btn) + + def build_buttons(self): + self.template_buttons(["Pg0", "Pg1", "Pg2", "Pg3", "..."]) + + def onResume(self, screen): + self.timer = lv.timer_create(self.tick, 1000, None) + + def onPause(self, screen): + if self.timer: + self.timer.delete() + self.timer = None + + def tick(self, t): + self.update() + self.draw() + + def update(self): + pass + + def draw_page_example(self): + ui = self.c + ui.clear() + + st = 28 + y = 2*st + ui.text(0, y, "Hello world, page is %d" % self.page) + y += st + + def draw(self): + self.draw_page_example() + + def handle_buttons(self): + ui = self.c + +# ---------------------------- +# App logic +# ---------------------------- + +class UGyro(TiltGyro): + def __init__(self): + super().__init__() + + self.accel = SensorManager.get_default_sensor(SensorManager.TYPE_ACCELEROMETER) + self.magn = SensorManager.get_default_sensor(SensorManager.TYPE_MAGNETIC_FIELD) + self.gyro = SensorManager.get_default_sensor(SensorManager.TYPE_GYROSCOPE) + + self.val = None + self.vfirst = None + + self.heading = 45 + + def update(self): + acc = SensorManager.read_sensor_once(self.accel) + sc = 1/9.81 + acc = ( -acc[0] * sc, acc[1] * sc, acc[2] * sc ) + self.acc = acc + + self.val = SensorManager.read_sensor_once(self.gyro) + +class Main(PagedCanvas): + def __init__(self): + super().__init__() + + self.cal = UGyro() + + self.heading = 0.0 + + self.Ypos = 40 + + def draw(self): + pass + + def onResume(self, screen): + self.timer = lv.timer_create(self.tick, 50, None) + + def update(self): + self.c.clear() + + y = 20 + st = 20 + + self.cal.update() + if self.cal.val is None: + self.c.text(0, y, f"No compass data") + y += st + return + + self.heading = self.cal.heading_tilted() + + if self.page == 0: + self.draw_top(self.cal.acc) + if self.page == 1: + self.draw_values() + elif self.page == 2: + self.c.text(0, y, f"Resetting calibration") + self.page = 0 + self.cal.reset() + + def build_buttons(self): + self.template_buttons(["Pg0", "Pg1", "Pg2"]) + + def draw_values(self): + x, y, z = self.cal.acc[0], self.cal.acc[1], self.cal.acc[2] + total = math.sqrt(x*x+y*y+z*z) + s = "" + if x > .6: + s += ", left" + if x < -.6: + s += ", right" + if y > .6: + s += ", up" + if y < -.6: + s += ", down" + if z > .6: + s += ", below" + if z < -.6: + s += ", above" + + self.c.text(0, 7, f""" +^ Up -> Right +|| Acc +X {self.cal.acc[0]:.2f} Y {self.cal.acc[1]:.2f} Z {self.cal.acc[2]:.2f} +{total*100:.2f}% {s} +X {self.cal.val[0]:.2f}\nY {self.cal.val[1]:.2f}\nZ {self.cal.val[2]:.2f} +""") + + def _px_per_deg(self): + # JS used deg->px: (deg/90)*(width/2.1) + s = min(self.c.W, self.c.H) + return (s / 2.1) / 90.0 + + def _degrees_to_pixels(self, deg): + return deg * self._px_per_deg() + + # ---- TOP VIEW ---- + + def draw_top(self, acc): + heading=self.heading + heading2=self.cal.angvel + vmin=0 + vmax=20 + v=self.cal.val + + cx = self.c.W // 2 + cy = self.c.H // 2 + + # Crosshair + self.c.line(0, cy, self.c.W, cy) + self.c.line(cx, 0, cx, self.c.H) + + # Circles (30/60/90 deg) + for rdeg in (30, 60, 90): + r = int(self._degrees_to_pixels(rdeg)) + self.c.circle(cx, cy, r) + + # Accel circle + if acc is not None: + self._draw_accel(acc) + + # Heading arrow(s) + self._draw_heading_arrow(heading, color=lv.color_make(255, 0, 0)) + self.c.text(265, 22, "%d°" % int(heading)) + if heading2 is not None: + self._draw_heading_arrow(heading2, color=lv.color_make(255, 255, 255), size = 100) + self.c.text(10, 22, "%d°" % int(heading2)) + + def _draw_heading_arrow(self, heading, color, size = 80): + cx = self.c.W / 2.0 + cy = self.c.H / 2.0 + + rad = -to_rad(heading) + x2 = cx + math.sin(rad - 0.1) * size + y2 = cy - math.cos(rad - 0.1) * size + x3 = cx + math.sin(rad + 0.1) * size + y3 = cy - math.cos(rad + 0.1) * size + + poly = [ + int(cx), int(cy), + int(x2), int(y2), + int(x3), int(y3), + ] + + self.c.line(poly[0], poly[1], poly[2], poly[3]) + self.c.line(poly[2], poly[3], poly[4], poly[5]) + self.c.line(poly[4], poly[5], poly[0], poly[1]) + + def _draw_accel(self, acc): + ax, ay, az = acc + cx = self.c.W / 2.0 + cy = self.c.H / 2.0 + + x2 = cx + ax * self.c.W + y2 = cy + ay * self.c.W + + self.c.circle(int(x2), int(y2), int(self.c.W / 8)) diff --git a/internal_filesystem/apps/cz.ucw.pavel.gyro/res/mipmap-mdpi/icon_64x64.png b/internal_filesystem/apps/cz.ucw.pavel.gyro/res/mipmap-mdpi/icon_64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..316c5f2366bdcab259ac9fb75ce7c3db1dfc4c65 GIT binary patch literal 6765 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 Date: Mon, 2 Mar 2026 22:05:48 +0100 Subject: [PATCH 05/10] iio: add todo. --- internal_filesystem/lib/mpos/imu/drivers/iio.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal_filesystem/lib/mpos/imu/drivers/iio.py b/internal_filesystem/lib/mpos/imu/drivers/iio.py index a26835ef..6af5c766 100644 --- a/internal_filesystem/lib/mpos/imu/drivers/iio.py +++ b/internal_filesystem/lib/mpos/imu/drivers/iio.py @@ -255,6 +255,8 @@ def _raw_gyroscope_dps(self): return (0.0, 0.0, 0.0) scale_name = self.gyro_path + "/" + "in_anglvel_scale" + # fixme: iio probably uses radians, so needs * 57.2957795 + gx = self._read_raw_scaled(self.gyro_path + "/" + "in_anglvel_x_raw", scale_name) gy = self._read_raw_scaled(self.gyro_path + "/" + "in_anglvel_y_raw", scale_name) gz = self._read_raw_scaled(self.gyro_path + "/" + "in_anglvel_z_raw", scale_name) From 2a9dc7e0f81d5691ee875c6bc5babc029da62cde Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Mon, 2 Mar 2026 22:28:10 +0100 Subject: [PATCH 06/10] gyro: gyro seems to work way better on pinephone --- .../apps/cz.ucw.pavel.gyro/assets/main.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/internal_filesystem/apps/cz.ucw.pavel.gyro/assets/main.py b/internal_filesystem/apps/cz.ucw.pavel.gyro/assets/main.py index 479fd29e..3e8ea3c8 100644 --- a/internal_filesystem/apps/cz.ucw.pavel.gyro/assets/main.py +++ b/internal_filesystem/apps/cz.ucw.pavel.gyro/assets/main.py @@ -54,13 +54,15 @@ def heading_tilted(self): iio is in rads/second """ t = time.time() - v = self.val[1] * 57.2957795 - coef = 0.1 + # pp: val[1] seems to be rotation "away" and "towards" the user, like pitch in plane ... or maybe roll? + # val[2] sseems to be rotation -- as useful for compass on table + v = self.val[2] * 57.2957795 + coef = 0.8 self.smooth = self.smooth * (1-coef) + v * coef - self.heading += self.smooth * (t - self.last) + self.heading -= self.smooth * (t - self.last) self.last = t - self.angvel = v - return self.smooth + self.angvel = -self.smooth + return self.heading # ----------------------------- # Canvas (LVGL) From 9e17aa53628f03ceed57f875845a050883a15043 Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Mon, 2 Mar 2026 22:37:30 +0100 Subject: [PATCH 07/10] iio: move scale conversion where it belongs --- internal_filesystem/lib/mpos/imu/drivers/iio.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/internal_filesystem/lib/mpos/imu/drivers/iio.py b/internal_filesystem/lib/mpos/imu/drivers/iio.py index 6af5c766..f32966ba 100644 --- a/internal_filesystem/lib/mpos/imu/drivers/iio.py +++ b/internal_filesystem/lib/mpos/imu/drivers/iio.py @@ -254,12 +254,11 @@ def _raw_gyroscope_dps(self): if not self.gyro_path: return (0.0, 0.0, 0.0) scale_name = self.gyro_path + "/" + "in_anglvel_scale" + mul = 57.2957795 - # fixme: iio probably uses radians, so needs * 57.2957795 - - gx = self._read_raw_scaled(self.gyro_path + "/" + "in_anglvel_x_raw", scale_name) - gy = self._read_raw_scaled(self.gyro_path + "/" + "in_anglvel_y_raw", scale_name) - gz = self._read_raw_scaled(self.gyro_path + "/" + "in_anglvel_z_raw", scale_name) + gx = mul * self._read_raw_scaled(self.gyro_path + "/" + "in_anglvel_x_raw", scale_name) + gy = mul * self._read_raw_scaled(self.gyro_path + "/" + "in_anglvel_y_raw", scale_name) + gz = mul * self._read_raw_scaled(self.gyro_path + "/" + "in_anglvel_z_raw", scale_name) return self._apply_mount_matrix(gx, gy, gz, self.gyro_path) From 9a880ee377355c40964dc05ea32bd595577c3bfd Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Mon, 2 Mar 2026 22:38:10 +0100 Subject: [PATCH 08/10] gyro: rely of iio driver providing right scale of values --- internal_filesystem/apps/cz.ucw.pavel.gyro/assets/main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal_filesystem/apps/cz.ucw.pavel.gyro/assets/main.py b/internal_filesystem/apps/cz.ucw.pavel.gyro/assets/main.py index 3e8ea3c8..687fc99d 100644 --- a/internal_filesystem/apps/cz.ucw.pavel.gyro/assets/main.py +++ b/internal_filesystem/apps/cz.ucw.pavel.gyro/assets/main.py @@ -56,8 +56,8 @@ def heading_tilted(self): t = time.time() # pp: val[1] seems to be rotation "away" and "towards" the user, like pitch in plane ... or maybe roll? # val[2] sseems to be rotation -- as useful for compass on table - v = self.val[2] * 57.2957795 - coef = 0.8 + v = self.val[2] + coef = 1.0 self.smooth = self.smooth * (1-coef) + v * coef self.heading -= self.smooth * (t - self.last) self.last = t @@ -455,7 +455,7 @@ def draw_values(self): || Acc X {self.cal.acc[0]:.2f} Y {self.cal.acc[1]:.2f} Z {self.cal.acc[2]:.2f} {total*100:.2f}% {s} -X {self.cal.val[0]:.2f}\nY {self.cal.val[1]:.2f}\nZ {self.cal.val[2]:.2f} +X {self.cal.val[0]:.2f} Y {self.cal.val[1]:.2f} Z {self.cal.val[2]:.2f} """) def _px_per_deg(self): From e5668d13d152ef244382a0f63c2d5ed7a0db9e8b Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Mon, 2 Mar 2026 22:46:32 +0100 Subject: [PATCH 09/10] iio: Turn down debugging, but I still get framerate drops --- internal_filesystem/lib/mpos/imu/drivers/iio.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal_filesystem/lib/mpos/imu/drivers/iio.py b/internal_filesystem/lib/mpos/imu/drivers/iio.py index f32966ba..d849a2a3 100644 --- a/internal_filesystem/lib/mpos/imu/drivers/iio.py +++ b/internal_filesystem/lib/mpos/imu/drivers/iio.py @@ -74,7 +74,8 @@ def find_iio_device_with_file(self, filename, base_dir="/sys/bus/iio/devices/"): return None def _read_text(self, name: str) -> str: - print("Read: ", name) + if False: + print("Read: ", name) f = open(name, "r") try: return f.readline().strip() From c07121a940e1cf849df19ccb47643e64ec4de6c9 Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Tue, 3 Mar 2026 23:01:26 +0100 Subject: [PATCH 10/10] gyro: add reset and calibration support, introduce vectors, display help image --- .../cz.ucw.pavel.gyro/META-INF/MANIFEST.JSON | 12 +- .../apps/cz.ucw.pavel.gyro/assets/main.py | 218 +++++++++++++----- .../apps/cz.ucw.pavel.gyro/res/gyro-help.png | Bin 0 -> 28684 bytes .../res/mipmap-mdpi/icon_64x64.png | Bin 6765 -> 7808 bytes 4 files changed, 163 insertions(+), 67 deletions(-) create mode 100644 internal_filesystem/apps/cz.ucw.pavel.gyro/res/gyro-help.png diff --git a/internal_filesystem/apps/cz.ucw.pavel.gyro/META-INF/MANIFEST.JSON b/internal_filesystem/apps/cz.ucw.pavel.gyro/META-INF/MANIFEST.JSON index 9e6b0742..bd365e8e 100644 --- a/internal_filesystem/apps/cz.ucw.pavel.gyro/META-INF/MANIFEST.JSON +++ b/internal_filesystem/apps/cz.ucw.pavel.gyro/META-INF/MANIFEST.JSON @@ -1,11 +1,11 @@ { -"name": "Xxx", +"name": "Gyro", "publisher": "Pavel Machek", -"short_description": "Xxx", -"long_description": "Simple xxx app.", -"icon_url": "https://apps.micropythonos.com/apps/cz.ucw.pavel.xxx/icons/cz.ucw.pavel.xxx_0.0.1_64x64.png", -"download_url": "https://apps.micropythonos.com/apps/cz.ucw.pavel.xxx/mpks/cz.ucw.pavel.xxx_0.0.1.mpk", -"fullname": "cz.ucw.pavel.xxx", +"short_description": "Gyro", +"long_description": "Simple gyro app.", +"icon_url": "https://apps.micropythonos.com/apps/cz.ucw.pavel.gyro/icons/cz.ucw.pavel.gyro_0.0.1_64x64.png", +"download_url": "https://apps.micropythonos.com/apps/cz.ucw.pavel.gyro/mpks/cz.ucw.pavel.gyro_0.0.1.mpk", +"fullname": "cz.ucw.pavel.gyro", "version": "0.0.1", "category": "utilities", "activities": [ diff --git a/internal_filesystem/apps/cz.ucw.pavel.gyro/assets/main.py b/internal_filesystem/apps/cz.ucw.pavel.gyro/assets/main.py index 687fc99d..f6127684 100644 --- a/internal_filesystem/apps/cz.ucw.pavel.gyro/assets/main.py +++ b/internal_filesystem/apps/cz.ucw.pavel.gyro/assets/main.py @@ -1,6 +1,5 @@ """ -xxx - +Test/visualization of gyroscope / accelerometer """ @@ -32,37 +31,118 @@ def to_rad(deg): def to_deg(rad): return rad * 180.0 / math.pi +class Vec3: + def __init__(self): + pass + + def init3(self, x, y, z): + self.x = float(x) + self.y = float(y) + self.z = float(z) + return self + + def init_v(self, v): + self.x = v[0] + self.y = v[1] + self.z = v[2] + return self + + def __add__(self, other): + return vec3( + self.x + other.x, + self.y + other.y, + self.z + other.z + ) + + def __sub__(self, other): + return vec3( + self.x - other.x, + self.y - other.y, + self.z - other.z + ) + + def __mul__(self, scalar): + return vec3( + self.x * scalar, + self.y * scalar, + self.z * scalar + ) + + def __truediv__(self, scalar): + return vec3( + self.x / scalar, + self.y / scalar, + self.z / scalar + ) + + __rmul__ = __mul__ + + def __repr__(self): + return f"X {self.x:.2f} Y {self.y:.2f} Z {self.z:.2f}" + +def vec3(x, y, z): return Vec3().init3(x, y, z) +def vec0(): return Vec3().init3(0, 0, 0) + # ----------------------------- # Calibration + heading # ----------------------------- class Gyro: - def reset(self): - pass - -class TiltGyro(Gyro): def __init__(self): super().__init__() - self.heading = 0 + self.rot = vec0() self.last = time.time() - self.smooth = 0.0 + self.last_reset = self.last + self.smooth = vec0() + self.calibration = vec0() + + def reset(self): + now = time.time() + self.calibration = self.rot / (now - self.last_reset) + print("Reset... ", self.calibration) + self.last_reset = now + self.rot = vec0() - def heading_tilted(self): + def update(self): """ Returns heading 0..360 iio is in rads/second """ t = time.time() - # pp: val[1] seems to be rotation "away" and "towards" the user, like pitch in plane ... or maybe roll? - # val[2] sseems to be rotation -- as useful for compass on table - v = self.val[2] - coef = 1.0 + # pp: gyr[1] seems to be rotation "away" and "towards" the user, like pitch in plane ... or maybe roll? + # gyr[2] sseems to be rotation -- as useful for compass on table + v = self.gyr + coef = 1 self.smooth = self.smooth * (1-coef) + v * coef - self.heading -= self.smooth * (t - self.last) + self.rot -= self.smooth * (t - self.last) self.last = t - self.angvel = -self.smooth - return self.heading + + def angle(self): + now = time.time() + return self.rot - (now - self.last_reset) * self.calibration + + def angvel(self): + return vec0()-self.smooth + +class UGyro(Gyro): + def __init__(self): + super().__init__() + + self.accel = SensorManager.get_default_sensor(SensorManager.TYPE_ACCELEROMETER) + self.magn = SensorManager.get_default_sensor(SensorManager.TYPE_MAGNETIC_FIELD) + self.gyro = SensorManager.get_default_sensor(SensorManager.TYPE_GYROSCOPE) + + self.gyr = None + + def update(self): + acc = SensorManager.read_sensor_once(self.accel) + sc = 1/9.81 + acc = vec3( -acc[0] * sc, acc[1] * sc, acc[2] * sc ) + self.acc = acc + + self.gyr = Vec3().init_v(SensorManager.read_sensor_once(self.gyro)) + super().update() # ----------------------------- # Canvas (LVGL) @@ -370,36 +450,29 @@ def handle_buttons(self): # App logic # ---------------------------- -class UGyro(TiltGyro): - def __init__(self): - super().__init__() - - self.accel = SensorManager.get_default_sensor(SensorManager.TYPE_ACCELEROMETER) - self.magn = SensorManager.get_default_sensor(SensorManager.TYPE_MAGNETIC_FIELD) - self.gyro = SensorManager.get_default_sensor(SensorManager.TYPE_GYROSCOPE) - - self.val = None - self.vfirst = None - - self.heading = 45 - - def update(self): - acc = SensorManager.read_sensor_once(self.accel) - sc = 1/9.81 - acc = ( -acc[0] * sc, acc[1] * sc, acc[2] * sc ) - self.acc = acc - - self.val = SensorManager.read_sensor_once(self.gyro) - class Main(PagedCanvas): + ASSET_PATH = "M:apps/cz.ucw.pavel.gyro/res/gyro-help.png" + def __init__(self): super().__init__() self.cal = UGyro() + self.Ypos = 40 - self.heading = 0.0 + img = lv.image(lv.layer_top()) + img.set_src(f"{self.ASSET_PATH}") + self.help_img = img + self.hide_img() - self.Ypos = 40 + def hide_img(self): + self.help_img.add_flag(lv.obj.FLAG.HIDDEN) + + def draw_img(self): + img = self.help_img + img.remove_flag(lv.obj.FLAG.HIDDEN) + img.set_pos(60, 18) + #img.set_size(640, 640) + img.set_rotation(0) def draw(self): pass @@ -414,48 +487,71 @@ def update(self): st = 20 self.cal.update() - if self.cal.val is None: + if self.cal.gyr is None: self.c.text(0, y, f"No compass data") y += st return - self.heading = self.cal.heading_tilted() - + if self.page == 2: + self.draw_img() + return + self.hide_img() + if self.page == 0: self.draw_top(self.cal.acc) - if self.page == 1: + elif self.page == 1: self.draw_values() - elif self.page == 2: + elif self.page == 3: self.c.text(0, y, f"Resetting calibration") self.page = 0 self.cal.reset() def build_buttons(self): - self.template_buttons(["Pg0", "Pg1", "Pg2"]) + self.template_buttons(["Graph", "Values", "Help", "Reset"]) def draw_values(self): - x, y, z = self.cal.acc[0], self.cal.acc[1], self.cal.acc[2] + x, y, z = self.cal.acc.x, self.cal.acc.y, self.cal.acc.z total = math.sqrt(x*x+y*y+z*z) s = "" if x > .6: - s += ", left" + s += " left" if x < -.6: - s += ", right" + s += " right" if y > .6: - s += ", up" + s += " up" if y < -.6: - s += ", down" + s += " down" if z > .6: - s += ", below" + s += " below" if z < -.6: - s += ", above" + s += " above" + + t = "" + lim = 25 + angvel = self.cal.angvel() + if angvel.z > lim: + # top part moves to the right + t += " yaw+" + if angvel.z < -lim: + t += " yaw-" + if angvel.x > lim: + # top part goes up + t += " pitch+" + if angvel.x < -lim: + t += " pitch-" + if angvel.y > lim: + # right part goes down + t += " roll+" + if angvel.y < -lim: + t += " roll-" self.c.text(0, 7, f""" -^ Up -> Right -|| Acc -X {self.cal.acc[0]:.2f} Y {self.cal.acc[1]:.2f} Z {self.cal.acc[2]:.2f} -{total*100:.2f}% {s} -X {self.cal.val[0]:.2f} Y {self.cal.val[1]:.2f} Z {self.cal.val[2]:.2f} +^ Up -> Right +|| Acc +{self.cal.acc} +Earth is{s}, {total*100:.0f}% +{self.cal.gyr} +Rotation is{t} """) def _px_per_deg(self): @@ -469,11 +565,11 @@ def _degrees_to_pixels(self, deg): # ---- TOP VIEW ---- def draw_top(self, acc): - heading=self.heading - heading2=self.cal.angvel + heading=self.cal.angle().z + heading2=self.cal.angvel().z vmin=0 vmax=20 - v=self.cal.val + v=self.cal.gyr cx = self.c.W // 2 cy = self.c.H // 2 @@ -519,7 +615,7 @@ def _draw_heading_arrow(self, heading, color, size = 80): self.c.line(poly[4], poly[5], poly[0], poly[1]) def _draw_accel(self, acc): - ax, ay, az = acc + ax, ay, az = acc.x, acc.y, acc.z cx = self.c.W / 2.0 cy = self.c.H / 2.0 diff --git a/internal_filesystem/apps/cz.ucw.pavel.gyro/res/gyro-help.png b/internal_filesystem/apps/cz.ucw.pavel.gyro/res/gyro-help.png new file mode 100644 index 0000000000000000000000000000000000000000..4b54b993402a0f2db0ef3e740fdab3f132e02411 GIT binary patch literal 28684 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 IpaB^>EX>4U6ba`-PAZ2)IW&i+q+U;0dk|Ze( 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