From d6d36693099032934c9ee3dcec9d946dc0cb0b58 Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Mon, 16 Feb 2026 10:56:15 +0100 Subject: [PATCH 1/2] imu: Allow access to iio on Linux Linux phones have IMU units. Allow applications to use them same way they would access IMU on esp32 based devices. Lightly tested on PinePhone. Same applications useful on esp32 (compass, step counter) are likely to be useful on Linux phones, too. --- internal_filesystem/lib/mpos/board/linux.py | 9 +- .../lib/mpos/sensor_manager.py | 235 +++++++++++++++--- 2 files changed, 214 insertions(+), 30 deletions(-) diff --git a/internal_filesystem/lib/mpos/board/linux.py b/internal_filesystem/lib/mpos/board/linux.py index b8cf1495..c5b28ca2 100644 --- a/internal_filesystem/lib/mpos/board/linux.py +++ b/internal_filesystem/lib/mpos/board/linux.py @@ -127,7 +127,14 @@ def adc_to_voltage(adc_value): # Initialize with no I2C bus - will detect MCU temp if available # (On Linux desktop, this will fail gracefully but set _initialized flag) -SensorManager.init(None) +SensorManager.init_iio() + +# In app: +if False and SensorManager.is_available(): + accel = SensorManager.get_default_sensor(SensorManager.TYPE_ACCELEROMETER) + print(accel) + ax, ay, az = SensorManager.read_sensor_once(accel) # Returns m/s² + print(ax, ay, az) # === CAMERA HARDWARE === diff --git a/internal_filesystem/lib/mpos/sensor_manager.py b/internal_filesystem/lib/mpos/sensor_manager.py index fa5eab57..5ed04656 100644 --- a/internal_filesystem/lib/mpos/sensor_manager.py +++ b/internal_filesystem/lib/mpos/sensor_manager.py @@ -19,6 +19,7 @@ """ import time +import os try: import _thread _lock = _thread.allocate_lock() @@ -28,6 +29,7 @@ # Sensor type constants (matching Android SensorManager) TYPE_ACCELEROMETER = 1 # Units: m/s² (meters per second squared) +TYPE_MAGNETIC_FIELD = 2 # Units: μT (micro Teslas) TYPE_GYROSCOPE = 4 # Units: deg/s (degrees per second) TYPE_TEMPERATURE = 13 # Units: °C (generic, returns first available - deprecated) TYPE_IMU_TEMPERATURE = 14 # Units: °C (IMU chip temperature) @@ -101,6 +103,7 @@ class SensorManager: # Class-level constants TYPE_ACCELEROMETER = TYPE_ACCELEROMETER + TYPE_MAGNETIC_FIELD = TYPE_MAGNETIC_FIELD TYPE_GYROSCOPE = TYPE_GYROSCOPE TYPE_TEMPERATURE = TYPE_TEMPERATURE TYPE_IMU_TEMPERATURE = TYPE_IMU_TEMPERATURE @@ -120,6 +123,7 @@ def get(cls): if cls._instance is None: cls._instance = cls() return cls._instance + def init(self, i2c_bus, address=0x6B, mounted_position=FACING_SKY): """Initialize SensorManager. MCU temperature initializes immediately, IMU initializes on first use. @@ -146,6 +150,43 @@ def init(self, i2c_bus, address=0x6B, mounted_position=FACING_SKY): self._initialized = True return True + + def init_iio(self): + self._imu_driver = _IIODriver() + self._sensor_list = [ + Sensor( + name="Accelerometer", + sensor_type=TYPE_ACCELEROMETER, + vendor="Linux IIO", + version=1, + max_range="±8G (78.4 m/s²)", + resolution="0.0024 m/s²", + power_ma=0.2 + ), + Sensor( + name="Gyroscope", + sensor_type=TYPE_GYROSCOPE, + vendor="Linux IIO", + version=1, + max_range="±256 deg/s", + resolution="0.002 deg/s", + power_ma=0.7 + ), + Sensor( + name="Temperature", + sensor_type=TYPE_IMU_TEMPERATURE, + vendor="Linux IIO", + version=1, + max_range="-40°C to +85°C", + resolution="0.004°C", + power_ma=0 + ) + ] + + self._load_calibration() + + self._initialized = True + return True def _ensure_imu_initialized(self): """Perform IMU initialization on first use (lazy initialization). @@ -227,7 +268,35 @@ def get_default_sensor(self, sensor_type): if sensor.type == sensor_type: return sensor return None - + + def read_sensor_once(self, sensor): + if sensor.type == TYPE_ACCELEROMETER: + if self._imu_driver: + ax, ay, az = self._imu_driver.read_acceleration() + if self._mounted_position == FACING_EARTH: + az *= -1 + return (ax, ay, az) + elif sensor.type == TYPE_GYROSCOPE: + if self._imu_driver: + return self._imu_driver.read_gyroscope() + elif sensor.type == TYPE_IMU_TEMPERATURE: + if self._imu_driver: + return self._imu_driver.read_temperature() + elif sensor.type == TYPE_SOC_TEMPERATURE: + if self._has_mcu_temperature: + import esp32 + return esp32.mcu_temperature() + elif sensor.type == TYPE_TEMPERATURE: + # Generic temperature - return first available (backward compatibility) + if self._imu_driver: + temp = self._imu_driver.read_temperature() + if temp is not None: + return temp + if self._has_mcu_temperature: + import esp32 + return esp32.mcu_temperature() + return None + def read_sensor(self, sensor): """Read sensor data synchronously. @@ -258,32 +327,7 @@ def read_sensor(self, sensor): for attempt in range(max_retries): try: - if sensor.type == TYPE_ACCELEROMETER: - if self._imu_driver: - ax, ay, az = self._imu_driver.read_acceleration() - if self._mounted_position == FACING_EARTH: - az *= -1 - return (ax, ay, az) - elif sensor.type == TYPE_GYROSCOPE: - if self._imu_driver: - return self._imu_driver.read_gyroscope() - elif sensor.type == TYPE_IMU_TEMPERATURE: - if self._imu_driver: - return self._imu_driver.read_temperature() - elif sensor.type == TYPE_SOC_TEMPERATURE: - if self._has_mcu_temperature: - import esp32 - return esp32.mcu_temperature() - elif sensor.type == TYPE_TEMPERATURE: - # Generic temperature - return first available (backward compatibility) - if self._imu_driver: - temp = self._imu_driver.read_temperature() - if temp is not None: - return temp - if self._has_mcu_temperature: - import esp32 - return esp32.mcu_temperature() - return None + return self.read_sensor_once(sensor) except Exception as e: error_msg = str(e) # Retry if sensor data not ready, otherwise fail immediately @@ -292,6 +336,7 @@ def read_sensor(self, sensor): time.sleep_ms(retry_delay_ms) continue else: + print("Exception reading sensor:", error_msg) return None return None @@ -717,6 +762,138 @@ def set_calibration(self, accel_offsets, gyro_offsets): raise NotImplementedError +class _IIODriver(_IMUDriver): + """ + Read sensor data via Linux IIO sysfs. + + Typical base path: + /sys/bus/iio/devices/iio:device0 + """ + accel_path: str + + def __init__(self): + self.accel_path = self.find_iio_device_with_file("in_accel_x_raw") + print("path:", self.accel_path) + + def _p(self, name: str): + return self.accel_path + "/" + name + + def _exists(self, name): + try: + os.stat(name) + return True + except OSError: + return False + + def _is_dir(self, path): + # MicroPython: stat tuple, mode is [0] + try: + st = os.stat(path) + mode = st[0] + # directory bit (POSIX): 0o040000 + return (mode & 0o170000) == 0o040000 + except OSError: + return False + + def find_iio_device_with_file(self, filename, base_dir="/sys/bus/iio/devices/"): + """ + Returns full path to iio:deviceX that contains given filename, + e.g. "/sys/bus/iio/devices/iio:device0" + + Returns None if not found. + """ + + print("Is dir? ", self._is_dir(base_dir), base_dir) + try: + entries = os.listdir(base_dir) + except OSError: + print("Error listing dir") + return None + + for e in entries: + print("Entry:", e) + if not e.startswith("iio:device"): + continue + + print("Entry:", e) + + dev_path = base_dir + "/" + e + if not self._is_dir(dev_path): + continue + + if self._exists(dev_path + "/" + filename): + return dev_path + + return None + + def _read_text(self, name: str) -> str: + p = name + print("Read: ", p) + f = open(p, "r") + try: + return f.readline().strip() + finally: + f.close() + + def _read_float(self, name: str) -> float: + return float(self._read_text(name)) + + def _read_int(self, name: str) -> int: + return int(self._read_text(name), 10) + + def _read_raw_scaled(self, raw_name: str, scale_name: str) -> float: + raw = self._read_int(raw_name) + scale = self._read_float(scale_name) + return raw * scale + + # ---------------------------- + # Public API (replacing I2C) + # ---------------------------- + + def read_temperature(self) -> float: + """ + Tries common IIO patterns: + - in_temp_input (already scaled, usually millidegree C) + - in_temp_raw + in_temp_scale + """ + if False: # os.path.exists(self._p("in_temp_input")): + v = self._read_float(self.accel_path + "/" + "in_temp_input") + # Many drivers expose millidegree Celsius here. + if abs(v) > 200: # heuristic: 25000 means 25°C + return v / 1000.0 + return v + + # Fallback: raw + scale + return self._read_raw_scaled(self.accel_path + "/" + "in_temp_raw", self.accel_path + "/" + "in_temp_scale") + + def read_acceleration(self) -> tuple[float, float, float]: + """ + Returns acceleration in m/s^2 if the kernel driver uses standard IIO scale. + Common names: + in_accel_{x,y,z}_raw + in_accel_scale + """ + scale_name = self.accel_path + "/" + "in_accel_scale" + + ax = self._read_raw_scaled(self.accel_path + "/" + "in_accel_x_raw", scale_name) + 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) + + def read_gyroscope(self) -> tuple[float, float, float]: + """ + Returns angular velocity in rad/s if the kernel driver uses standard IIO scale. + Common names: + in_anglvel_{x,y,z}_raw + in_anglvel_scale + """ + scale_name = self.accel_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) + + return (gx, gy, gz) + class _QMI8658Driver(_IMUDriver): """Wrapper for QMI8658 IMU (Waveshare board).""" @@ -918,8 +1095,8 @@ def set_calibration(self, accel_offsets, gyro_offsets): _original_methods = {} _methods_to_delegate = [ - 'init', 'is_available', 'get_sensor_list', 'get_default_sensor', - 'read_sensor', 'calibrate_sensor', 'check_calibration_quality', + 'init', 'init_iio', 'is_available', 'get_sensor_list', 'get_default_sensor', + 'read_sensor', 'read_sensor_once', 'calibrate_sensor', 'check_calibration_quality', 'check_stationarity' ] From 4a9493ae7ed6fc47449209f3419fca3c6dd29478 Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Mon, 16 Feb 2026 20:54:21 +0100 Subject: [PATCH 2/2] imu: cleanup stale comments, revert unneccessary changes --- internal_filesystem/lib/mpos/board/linux.py | 10 -------- .../lib/mpos/sensor_manager.py | 23 +++++++++---------- 2 files changed, 11 insertions(+), 22 deletions(-) diff --git a/internal_filesystem/lib/mpos/board/linux.py b/internal_filesystem/lib/mpos/board/linux.py index c5b28ca2..a1f464b5 100644 --- a/internal_filesystem/lib/mpos/board/linux.py +++ b/internal_filesystem/lib/mpos/board/linux.py @@ -122,20 +122,10 @@ def adc_to_voltage(adc_value): # LightsManager will not be initialized (functions will return False) # === SENSOR HARDWARE === -# Note: Desktop builds have no sensor hardware from mpos import SensorManager -# Initialize with no I2C bus - will detect MCU temp if available -# (On Linux desktop, this will fail gracefully but set _initialized flag) SensorManager.init_iio() -# In app: -if False and SensorManager.is_available(): - accel = SensorManager.get_default_sensor(SensorManager.TYPE_ACCELEROMETER) - print(accel) - ax, ay, az = SensorManager.read_sensor_once(accel) # Returns m/s² - print(ax, ay, az) - # === CAMERA HARDWARE === def init_cam(width, height, colormode): diff --git a/internal_filesystem/lib/mpos/sensor_manager.py b/internal_filesystem/lib/mpos/sensor_manager.py index 5ed04656..d5d3d7e1 100644 --- a/internal_filesystem/lib/mpos/sensor_manager.py +++ b/internal_filesystem/lib/mpos/sensor_manager.py @@ -29,7 +29,7 @@ # Sensor type constants (matching Android SensorManager) TYPE_ACCELEROMETER = 1 # Units: m/s² (meters per second squared) -TYPE_MAGNETIC_FIELD = 2 # Units: μT (micro Teslas) +TYPE_MAGNETIC_FIELD = 2 # Units: μT (micro teslas) TYPE_GYROSCOPE = 4 # Units: deg/s (degrees per second) TYPE_TEMPERATURE = 13 # Units: °C (generic, returns first available - deprecated) TYPE_IMU_TEMPERATURE = 14 # Units: °C (IMU chip temperature) @@ -124,7 +124,6 @@ def get(cls): cls._instance = cls() return cls._instance - def init(self, i2c_bus, address=0x6B, mounted_position=FACING_SKY): """Initialize SensorManager. MCU temperature initializes immediately, IMU initializes on first use. @@ -159,30 +158,30 @@ def init_iio(self): sensor_type=TYPE_ACCELEROMETER, vendor="Linux IIO", version=1, - max_range="±8G (78.4 m/s²)", - resolution="0.0024 m/s²", - power_ma=0.2 + max_range="?", + resolution="?", + power_ma=10 ), Sensor( name="Gyroscope", sensor_type=TYPE_GYROSCOPE, vendor="Linux IIO", version=1, - max_range="±256 deg/s", - resolution="0.002 deg/s", - power_ma=0.7 + max_range="?", + resolution="?", + power_ma=10 ), Sensor( name="Temperature", sensor_type=TYPE_IMU_TEMPERATURE, vendor="Linux IIO", version=1, - max_range="-40°C to +85°C", - resolution="0.004°C", - power_ma=0 + max_range="?", + resolution="?", + power_ma=10 ) ] - + self._load_calibration() self._initialized = True