1919"""
2020
2121import time
22+ import os
2223try :
2324 import _thread
2425 _lock = _thread .allocate_lock ()
2829
2930# Sensor type constants (matching Android SensorManager)
3031TYPE_ACCELEROMETER = 1 # Units: m/s² (meters per second squared)
32+ TYPE_MAGNETIC_FIELD = 2 # Units: μT (micro teslas)
3133TYPE_GYROSCOPE = 4 # Units: deg/s (degrees per second)
3234TYPE_TEMPERATURE = 13 # Units: °C (generic, returns first available - deprecated)
3335TYPE_IMU_TEMPERATURE = 14 # Units: °C (IMU chip temperature)
@@ -101,6 +103,7 @@ class SensorManager:
101103
102104 # Class-level constants
103105 TYPE_ACCELEROMETER = TYPE_ACCELEROMETER
106+ TYPE_MAGNETIC_FIELD = TYPE_MAGNETIC_FIELD
104107 TYPE_GYROSCOPE = TYPE_GYROSCOPE
105108 TYPE_TEMPERATURE = TYPE_TEMPERATURE
106109 TYPE_IMU_TEMPERATURE = TYPE_IMU_TEMPERATURE
@@ -120,7 +123,7 @@ def get(cls):
120123 if cls ._instance is None :
121124 cls ._instance = cls ()
122125 return cls ._instance
123-
126+
124127 def init (self , i2c_bus , address = 0x6B , mounted_position = FACING_SKY ):
125128 """Initialize SensorManager. MCU temperature initializes immediately, IMU initializes on first use.
126129
@@ -146,6 +149,43 @@ def init(self, i2c_bus, address=0x6B, mounted_position=FACING_SKY):
146149
147150 self ._initialized = True
148151 return True
152+
153+ def init_iio (self ):
154+ self ._imu_driver = _IIODriver ()
155+ self ._sensor_list = [
156+ Sensor (
157+ name = "Accelerometer" ,
158+ sensor_type = TYPE_ACCELEROMETER ,
159+ vendor = "Linux IIO" ,
160+ version = 1 ,
161+ max_range = "?" ,
162+ resolution = "?" ,
163+ power_ma = 10
164+ ),
165+ Sensor (
166+ name = "Gyroscope" ,
167+ sensor_type = TYPE_GYROSCOPE ,
168+ vendor = "Linux IIO" ,
169+ version = 1 ,
170+ max_range = "?" ,
171+ resolution = "?" ,
172+ power_ma = 10
173+ ),
174+ Sensor (
175+ name = "Temperature" ,
176+ sensor_type = TYPE_IMU_TEMPERATURE ,
177+ vendor = "Linux IIO" ,
178+ version = 1 ,
179+ max_range = "?" ,
180+ resolution = "?" ,
181+ power_ma = 10
182+ )
183+ ]
184+
185+ self ._load_calibration ()
186+
187+ self ._initialized = True
188+ return True
149189
150190 def _ensure_imu_initialized (self ):
151191 """Perform IMU initialization on first use (lazy initialization).
@@ -245,7 +285,35 @@ def get_default_sensor(self, sensor_type):
245285 if sensor .type == sensor_type :
246286 return sensor
247287 return None
248-
288+
289+ def read_sensor_once (self , sensor ):
290+ if sensor .type == TYPE_ACCELEROMETER :
291+ if self ._imu_driver :
292+ ax , ay , az = self ._imu_driver .read_acceleration ()
293+ if self ._mounted_position == FACING_EARTH :
294+ az *= - 1
295+ return (ax , ay , az )
296+ elif sensor .type == TYPE_GYROSCOPE :
297+ if self ._imu_driver :
298+ return self ._imu_driver .read_gyroscope ()
299+ elif sensor .type == TYPE_IMU_TEMPERATURE :
300+ if self ._imu_driver :
301+ return self ._imu_driver .read_temperature ()
302+ elif sensor .type == TYPE_SOC_TEMPERATURE :
303+ if self ._has_mcu_temperature :
304+ import esp32
305+ return esp32 .mcu_temperature ()
306+ elif sensor .type == TYPE_TEMPERATURE :
307+ # Generic temperature - return first available (backward compatibility)
308+ if self ._imu_driver :
309+ temp = self ._imu_driver .read_temperature ()
310+ if temp is not None :
311+ return temp
312+ if self ._has_mcu_temperature :
313+ import esp32
314+ return esp32 .mcu_temperature ()
315+ return None
316+
249317 def read_sensor (self , sensor ):
250318 """Read sensor data synchronously.
251319
@@ -276,32 +344,7 @@ def read_sensor(self, sensor):
276344
277345 for attempt in range (max_retries ):
278346 try :
279- if sensor .type == TYPE_ACCELEROMETER :
280- if self ._imu_driver :
281- ax , ay , az = self ._imu_driver .read_acceleration ()
282- if self ._mounted_position == FACING_EARTH :
283- az *= - 1
284- return (ax , ay , az )
285- elif sensor .type == TYPE_GYROSCOPE :
286- if self ._imu_driver :
287- return self ._imu_driver .read_gyroscope ()
288- elif sensor .type == TYPE_IMU_TEMPERATURE :
289- if self ._imu_driver :
290- return self ._imu_driver .read_temperature ()
291- elif sensor .type == TYPE_SOC_TEMPERATURE :
292- if self ._has_mcu_temperature :
293- import esp32
294- return esp32 .mcu_temperature ()
295- elif sensor .type == TYPE_TEMPERATURE :
296- # Generic temperature - return first available (backward compatibility)
297- if self ._imu_driver :
298- temp = self ._imu_driver .read_temperature ()
299- if temp is not None :
300- return temp
301- if self ._has_mcu_temperature :
302- import esp32
303- return esp32 .mcu_temperature ()
304- return None
347+ return self .read_sensor_once (sensor )
305348 except Exception as e :
306349 error_msg = str (e )
307350 # Retry if sensor data not ready, otherwise fail immediately
@@ -310,6 +353,7 @@ def read_sensor(self, sensor):
310353 time .sleep_ms (retry_delay_ms )
311354 continue
312355 else :
356+ print ("Exception reading sensor:" , error_msg )
313357 return None
314358
315359 return None
@@ -767,6 +811,138 @@ def set_calibration(self, accel_offsets, gyro_offsets):
767811 raise NotImplementedError
768812
769813
814+ class _IIODriver (_IMUDriver ):
815+ """
816+ Read sensor data via Linux IIO sysfs.
817+
818+ Typical base path:
819+ /sys/bus/iio/devices/iio:device0
820+ """
821+ accel_path : str
822+
823+ def __init__ (self ):
824+ self .accel_path = self .find_iio_device_with_file ("in_accel_x_raw" )
825+ print ("path:" , self .accel_path )
826+
827+ def _p (self , name : str ):
828+ return self .accel_path + "/" + name
829+
830+ def _exists (self , name ):
831+ try :
832+ os .stat (name )
833+ return True
834+ except OSError :
835+ return False
836+
837+ def _is_dir (self , path ):
838+ # MicroPython: stat tuple, mode is [0]
839+ try :
840+ st = os .stat (path )
841+ mode = st [0 ]
842+ # directory bit (POSIX): 0o040000
843+ return (mode & 0o170000 ) == 0o040000
844+ except OSError :
845+ return False
846+
847+ def find_iio_device_with_file (self , filename , base_dir = "/sys/bus/iio/devices/" ):
848+ """
849+ Returns full path to iio:deviceX that contains given filename,
850+ e.g. "/sys/bus/iio/devices/iio:device0"
851+
852+ Returns None if not found.
853+ """
854+
855+ print ("Is dir? " , self ._is_dir (base_dir ), base_dir )
856+ try :
857+ entries = os .listdir (base_dir )
858+ except OSError :
859+ print ("Error listing dir" )
860+ return None
861+
862+ for e in entries :
863+ print ("Entry:" , e )
864+ if not e .startswith ("iio:device" ):
865+ continue
866+
867+ print ("Entry:" , e )
868+
869+ dev_path = base_dir + "/" + e
870+ if not self ._is_dir (dev_path ):
871+ continue
872+
873+ if self ._exists (dev_path + "/" + filename ):
874+ return dev_path
875+
876+ return None
877+
878+ def _read_text (self , name : str ) -> str :
879+ p = name
880+ print ("Read: " , p )
881+ f = open (p , "r" )
882+ try :
883+ return f .readline ().strip ()
884+ finally :
885+ f .close ()
886+
887+ def _read_float (self , name : str ) -> float :
888+ return float (self ._read_text (name ))
889+
890+ def _read_int (self , name : str ) -> int :
891+ return int (self ._read_text (name ), 10 )
892+
893+ def _read_raw_scaled (self , raw_name : str , scale_name : str ) -> float :
894+ raw = self ._read_int (raw_name )
895+ scale = self ._read_float (scale_name )
896+ return raw * scale
897+
898+ # ----------------------------
899+ # Public API (replacing I2C)
900+ # ----------------------------
901+
902+ def read_temperature (self ) -> float :
903+ """
904+ Tries common IIO patterns:
905+ - in_temp_input (already scaled, usually millidegree C)
906+ - in_temp_raw + in_temp_scale
907+ """
908+ if False : # os.path.exists(self._p("in_temp_input")):
909+ v = self ._read_float (self .accel_path + "/" + "in_temp_input" )
910+ # Many drivers expose millidegree Celsius here.
911+ if abs (v ) > 200 : # heuristic: 25000 means 25°C
912+ return v / 1000.0
913+ return v
914+
915+ # Fallback: raw + scale
916+ return self ._read_raw_scaled (self .accel_path + "/" + "in_temp_raw" , self .accel_path + "/" + "in_temp_scale" )
917+
918+ def read_acceleration (self ) -> tuple [float , float , float ]:
919+ """
920+ Returns acceleration in m/s^2 if the kernel driver uses standard IIO scale.
921+ Common names:
922+ in_accel_{x,y,z}_raw + in_accel_scale
923+ """
924+ scale_name = self .accel_path + "/" + "in_accel_scale"
925+
926+ ax = self ._read_raw_scaled (self .accel_path + "/" + "in_accel_x_raw" , scale_name )
927+ ay = self ._read_raw_scaled (self .accel_path + "/" + "in_accel_y_raw" , scale_name )
928+ az = self ._read_raw_scaled (self .accel_path + "/" + "in_accel_z_raw" , scale_name )
929+
930+ return (ax , ay , az )
931+
932+ def read_gyroscope (self ) -> tuple [float , float , float ]:
933+ """
934+ Returns angular velocity in rad/s if the kernel driver uses standard IIO scale.
935+ Common names:
936+ in_anglvel_{x,y,z}_raw + in_anglvel_scale
937+ """
938+ scale_name = self .accel_path + "/" + "in_anglvel_scale"
939+
940+ gx = self ._read_raw_scaled (self .accel_path + "/" + "in_anglvel_x_raw" , scale_name )
941+ gy = self ._read_raw_scaled (self .accel_path + "/" + "in_anglvel_y_raw" , scale_name )
942+ gz = self ._read_raw_scaled (self .accel_path + "/" + "in_anglvel_z_raw" , scale_name )
943+
944+ return (gx , gy , gz )
945+
770946class _QMI8658Driver (_IMUDriver ):
771947 """Wrapper for QMI8658 IMU (Waveshare board)."""
772948
@@ -1054,8 +1230,8 @@ def set_calibration(self, accel_offsets, gyro_offsets):
10541230
10551231_original_methods = {}
10561232_methods_to_delegate = [
1057- 'init' , 'is_available' , 'get_sensor_list' , 'get_default_sensor' ,
1058- 'read_sensor' , 'calibrate_sensor' , 'check_calibration_quality' ,
1233+ 'init' , 'init_iio' , ' is_available' , 'get_sensor_list' , 'get_default_sensor' ,
1234+ 'read_sensor' , 'read_sensor_once' , ' calibrate_sensor' , 'check_calibration_quality' ,
10591235 'check_stationarity'
10601236]
10611237
0 commit comments