Skip to content

Commit ec87ada

Browse files
committed
fixes missing type definitions for new style SPI.
Also adds the start of an IMU sensor framework and fusion math.
1 parent db0d39d commit ec87ada

File tree

6 files changed

+747
-0
lines changed

6 files changed

+747
-0
lines changed
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# Copyright (c) 2024 - 2025 Kevin G. Schlosser
2+
3+
import lvgl as lv
4+
5+
6+
class AutoRotation:
7+
8+
def __init__(self, device, delay, lock_rotation=False, adjustment=0.0):
9+
10+
if adjustment > 180 or adjustment < -180:
11+
raise ValueError('adjustment range is -180.0 to +180.0')
12+
13+
scrn = lv.screen_active()
14+
disp = scrn.get_display()
15+
16+
self._device = device
17+
18+
self._timer = lv.timer_create(self._timer_cb, delay)
19+
self._timer.set_repeat_count(-1)
20+
21+
self._fusion = fusion.Fusion()
22+
self._lock_rotation = lock_rotation
23+
24+
self._last_rotation = disp.get_rotation()
25+
self._last_free_rotation = 0
26+
self._adjustment = adjustment
27+
28+
@property
29+
def adjustment(self):
30+
return self._adjustment
31+
32+
@adjustment.setter
33+
def adjustment(self, value):
34+
self._adjustment = value
35+
self._timer_cb(None)
36+
37+
@property
38+
def lock_rotation(self):
39+
return self._lock_rotation
40+
41+
@lock_rotation.setter
42+
def lock_rotation(self, value):
43+
self._lock_rotation = value
44+
self._timer_cb(None)
45+
46+
def __del__(self):
47+
if self._timer is not None:
48+
self._timer.delete()
49+
50+
def _timer_cb(self, _):
51+
roll, pitch, yaw = self._device.read()
52+
53+
roll += self._adjustment
54+
55+
if roll > 180.00:
56+
roll -= 360.0
57+
58+
if roll < -180.0:
59+
roll += 360.0
60+
61+
if roll < 0.0:
62+
roll += 360.0
63+
64+
scrn = lv.screen_active()
65+
disp = scrn.get_display()
66+
67+
if self._lock_rotation:
68+
69+
if 45 <= roll < 135:
70+
new_rotation = lv.DISPLAY_ROTATION._90
71+
72+
elif 135 <= roll < 225:
73+
new_rotation = lv.DISPLAY_ROTATION._180
74+
75+
elif 225 <= roll < 315:
76+
new_rotation = lv.DISPLAY_ROTATION._270
77+
78+
else:
79+
new_rotation = lv.DISPLAY_ROTATION._0
80+
81+
if new_rotation != self._last_rotation:
82+
self._last_rotation = new_rotation
83+
disp.set_rotation(new_rotation)
84+
else:
85+
roll = int(roll * 10.0)
86+
87+
if self._last_rotation != lv.DISPLAY_ROTATION._0:
88+
self._last_rotation = lv.DISPLAY_ROTATION._0
89+
disp.set_rotation(lv.DISPLAY_ROTATION._0)
90+
91+
if roll != self._last_free_rotation:
92+
self._last_free_rotation = roll
93+
top_layer = disp.get_layer_top()
94+
sys_layer = disp.get_layer_sys()
95+
bottom_layer = disp.get_layer_bottom()
96+
97+
top_layer_pivot_x = int(top_layer.get_width() / 2)
98+
top_layer_pivot_y = int(top_layer.get_height() / 2)
99+
top_layer.set_style_transform_pivot_x(top_layer_pivot_x, lv.PART.ANY)
100+
top_layer.set_style_transform_pivot_y(top_layer_pivot_y, lv.PART.ANY)
101+
top_layer.set_style_transform_rotation(roll, lv.PART.ANY)
102+
103+
sys_layer_pivot_x = int(sys_layer.get_width() / 2)
104+
sys_layer_pivot_y = int(sys_layer.get_height() / 2)
105+
sys_layer.set_style_transform_pivot_x(sys_layer_pivot_x, lv.PART.ANY)
106+
sys_layer.set_style_transform_pivot_y(sys_layer_pivot_y, lv.PART.ANY)
107+
sys_layer.set_style_transform_rotation(roll, lv.PART.ANY)
108+
109+
bottom_layer_pivot_x = int(bottom_layer.get_width() / 2)
110+
bottom_layer_pivot_y = int(bottom_layer.get_height() / 2)
111+
bottom_layer.set_style_transform_pivot_x(bottom_layer_pivot_x, lv.PART.ANY)
112+
bottom_layer.set_style_transform_pivot_y(bottom_layer_pivot_y, lv.PART.ANY)
113+
bottom_layer.set_style_transform_rotation(roll, lv.PART.ANY)
114+
115+
scrn_pivot_x = int(scrn.get_width() / 2)
116+
scrn_pivot_y = int(scrn.get_height() / 2)
117+
scrn.set_style_transform_pivot_x(scrn_pivot_x, lv.PART.ANY)
118+
scrn.set_style_transform_pivot_y(scrn_pivot_y, lv.PART.ANY)
119+
scrn.set_style_transform_rotation(roll, lv.PART.ANY)
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
from micropython import const # NOQA
2+
import imu_sensor_framework # NOQA
3+
4+
5+
_VERSION_REG = const(0x0)
6+
_REVISION_REG = const(0x1)
7+
8+
_TIME_REG = const(0x30)
9+
_TEMP_REG = const(0x33)
10+
_ACCEL_REG = const(0x35)
11+
_GYRO_REG = const(0x3B)
12+
13+
_CONFIG2_REG = const(0x02)
14+
_CONFIG5_REG = const(0x05)
15+
_CONFIG6_REG = const(0x06)
16+
_CONFIG7_REG = const(0x07)
17+
18+
_ACCEL_SETTING_REG = const(0x03)
19+
_GYRO_SETTING_REG = const(0x04)
20+
_ENABLE_REG = const(0x08)
21+
22+
# STANDARD_GRAVITY = 9.80665
23+
24+
25+
def _decode_settings(value):
26+
rnge = value >> 4
27+
rate = value & 0xF
28+
29+
return rnge, rate
30+
31+
32+
def _encode_setting(rnge, rate):
33+
return ((rnge & 0x7) << 4) | (rate & 0xF)
34+
35+
36+
ACCEL_RANGE_2 = const(0) # +/- 2g
37+
ACCEL_RANGE_4 = const(0x10) # +/- 4g
38+
ACCEL_RANGE_8 = const(0x20) # +/- 8g (default value)
39+
ACCEL_RANGE_16 = const(0x30) # +/- 16g
40+
41+
GYRO_RANGE_16 = const(0) # +/- 16 deg/s
42+
GYRO_RANGE_32 = const(0x10) # +/- 32 deg/s
43+
GYRO_RANGE_64 = const(0x20) # +/- 64 deg/s
44+
GYRO_RANGE_128 = const(0x30) # +/- 128 deg/s
45+
GYRO_RANGE_256 = const(0x40) # +/- 256 deg/s (default value)
46+
GYRO_RANGE_512 = const(0x50) # +/- 512 deg/s
47+
GYRO_RANGE_1024 = const(0x60) # +/- 1024 deg/s
48+
GYRO_RANGE_2048 = const(0x70) # +/- 2048 deg/s
49+
50+
ACCEL_RATE_8000_HZ = const(0)
51+
ACCEL_RATE_4000_HZ = const(1)
52+
ACCEL_RATE_2000_HZ = const(2)
53+
ACCEL_RATE_1000_HZ = const(3)
54+
ACCEL_RATE_500_HZ = const(4)
55+
ACCEL_RATE_250_HZ = const(5)
56+
ACCEL_RATE_125_HZ = const(6) # (default value)
57+
ACCEL_RATE_62_HZ = const(7)
58+
ACCEL_RATE_31_HZ = const(8)
59+
ACCEL_RATE_LP_128_HZ = const(12)
60+
ACCEL_RATE_LP_21_HZ = const(13)
61+
ACCEL_RATE_LP_11_HZ = const(14)
62+
ACCEL_RATE_LP_3_HZ = const(15)
63+
64+
65+
GYRO_RATE_8000_HZ = const(0)
66+
GYRO_RATE_4000_HZ = const(1)
67+
GYRO_RATE_2000_HZ = const(2)
68+
GYRO_RATE_1000_HZ = const(3)
69+
GYRO_RATE_500_HZ = const(4)
70+
GYRO_RATE_250_HZ = const(5)
71+
GYRO_RATE_125_HZ = const(6) # (default value)
72+
GYRO_RATE_62_HZ = const(7)
73+
GYRO_RATE_31_HZ = const(8)
74+
75+
I2C_ADDR = 0x6B
76+
BITS = 8
77+
78+
79+
class QMI8658C(imu_sensor_framework.IMUSensorFramework):
80+
81+
def _read_reg(self, reg):
82+
self._device.read_mem(reg, self._rx_mv[:1])
83+
return self._rx_buf[0]
84+
85+
def _write_reg(self, reg, data):
86+
self._tx_buf[0] = data
87+
self._device.write_mem(reg, self._tx_mv[:1])
88+
89+
def __init__(self, device, delay_between_samples=100):
90+
91+
super().__init__(device, 0.0, delay_between_samples)
92+
self._device = device
93+
self._tx_buf = bytearray(1)
94+
self._tx_mv = memoryview(self._tx_buf)
95+
self._rx_buf = bytearray(6)
96+
self._rx_mv = memoryview(self._rx_buf)
97+
98+
if self._read_reg(_VERSION_REG) != 0x05:
99+
raise RuntimeError("Failed to find QMI8658C")
100+
101+
self._accel_range = ACCEL_RANGE_8
102+
self._accel_rate = ACCEL_RATE_125_HZ
103+
104+
self._gyro_range = GYRO_RANGE_256
105+
self._gyro_rate = GYRO_RATE_125_HZ
106+
107+
self._write_reg(_CONFIG2_REG, 0x60)
108+
self._write_reg(_ACCEL_SETTING_REG, _encode_setting(ACCEL_RANGE_8, ACCEL_RATE_125_HZ))
109+
self._write_reg(_GYRO_SETTING_REG, _encode_setting(GYRO_RANGE_256, GYRO_RATE_125_HZ))
110+
111+
self._write_reg(_CONFIG5_REG, 0x00) # No magnetometer
112+
self._write_reg(_CONFIG6_REG, 0x00) # Disables Gyroscope And Accelerometer Low-Pass Filter
113+
self._write_reg(_CONFIG7_REG, 0x00) # Disables Motion on Demand.
114+
self._write_reg(_ENABLE_REG, 0x03) # enable accel and gyro
115+
116+
@property
117+
def accel_range(self):
118+
return self._accel_range
119+
120+
@accel_range.setter
121+
def accel_range(self, value):
122+
self._accel_range = value
123+
self._write_reg(_ACCEL_SETTING_REG, _encode_setting(value, self._accel_rate))
124+
125+
@property
126+
def accel_rate(self):
127+
return self._accel_rate
128+
129+
@accel_rate.setter
130+
def accel_rate(self, value):
131+
self._accel_rate = value
132+
self._write_reg(_ACCEL_SETTING_REG, _encode_setting(self._accel_range, value))
133+
134+
@property
135+
def gyro_range(self):
136+
return self._gyro_range
137+
138+
@gyro_range.setter
139+
def gyro_range(self, value):
140+
self._gyro_range = value
141+
self._write_reg(_ACCEL_SETTING_REG, _encode_setting(value, self._gyro_rate))
142+
143+
@property
144+
def gyro_rate(self):
145+
return self._gyro_rate
146+
147+
@gyro_rate.setter
148+
def gyro_rate(self, value):
149+
self._gyro_rate = value
150+
self._write_reg(_ACCEL_SETTING_REG, _encode_setting(self._gyro_range, value))
151+
152+
@property
153+
def timestamp(self) -> int:
154+
self._device.read_mem(_TIME_REG, self._rx_mv[:3])
155+
return self._rx_buf[0] + (self._rx_buf[1] << 8) + (self._rx_buf[2] << 16)
156+
157+
@property
158+
def temperature(self) -> float:
159+
"""Chip temperature"""
160+
self._device.read_mem(_TEMP_REG, self._rx_mv[:2])
161+
temp = self._rx_buf[0] / 256 + self._rx_buf[1]
162+
return temp
163+
164+
def _get_accelerometer(self):
165+
self._device.read_mem(_ACCEL_REG, self._rx_mv[:6])
166+
167+
buf = self._rx_buf
168+
169+
x = buf[0] << 8 | buf[1]
170+
y = buf[2] << 8 | buf[3]
171+
z = buf[4] << 8 | buf[5]
172+
173+
return x, y, z
174+
175+
def _get_gyrometer(self):
176+
self._device.read_mem(_GYRO_REG, self._rx_mv[:6])
177+
buf = self._rx_buf
178+
179+
x = buf[0] << 8 | buf[1]
180+
y = buf[2] << 8 | buf[3]
181+
z = buf[4] << 8 | buf[5]
182+
183+
return x, y, z
184+
185+
def _get_magnetometer(self): # NOQA
186+
return None
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import fusion
2+
import time
3+
4+
5+
class IMUSensorFramework:
6+
# delay between samples is nanosecond resolution. 1000 nanoseconds = 1 millisecond
7+
8+
def __init__(self, device, declination_adjustment=0.0, delay_between_samples=100):
9+
self._device = device
10+
self._fusion = fusion.Fusion(declination=declination_adjustment)
11+
self._delay_between_samples = delay_between_samples
12+
self._roll = 0.0
13+
self._pitch = 0.0
14+
self._yaw = 0.0
15+
16+
@property
17+
def roll(self):
18+
return self._roll
19+
20+
@property
21+
def pitch(self):
22+
return self._pitch
23+
24+
@property
25+
def yaw(self):
26+
return self._yaw
27+
28+
def _get_gyrometer(self):
29+
raise NotImplementedError
30+
31+
def _get_accelerometer(self):
32+
raise NotImplementedError
33+
34+
def _get_magnetometer(self):
35+
raise NotImplementedError
36+
37+
def calibrate(self, sample_count=5):
38+
ts = [time.ticks_ns()]
39+
count = [0]
40+
41+
def _stop_func():
42+
if count[0] == sample_count:
43+
return True
44+
45+
now_ts = time.ticks_ns()
46+
diff = time.ticks_diff(now_ts, ts[0])
47+
remaining = self._delay_between_samples - diff
48+
if remaining > 0:
49+
time.sleep_ns(remaining)
50+
51+
ts[0] = time.ticks_ns()
52+
count[0] += 1
53+
return False
54+
55+
self._fusion.calibrate(self._get_magnetometer, _stop_func)
56+
57+
def read(self):
58+
accel = self._get_accelerometer()
59+
gyro = self._get_gyrometer()
60+
61+
try:
62+
mag = self._get_magnetometer()
63+
except NotImplementedError:
64+
mag = None
65+
66+
self._roll, self._pitch, self._yaw = self._fusion.update(accel, gyro, mag)
67+
68+
return self._roll, self._pitch, self._yaw

0 commit comments

Comments
 (0)