|
| 1 | +# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries |
| 2 | +# SPDX-License-Identifier: MIT |
| 3 | +# |
| 4 | +# Adapted from QualiaS3 Compass Learn Guide by Liz Clark (Adafruit Industries) |
| 5 | +# https://learn.adafruit.com/qualia-s3-compass/ |
| 6 | + |
| 7 | +import time |
| 8 | +from math import atan2, degrees, radians |
| 9 | +import adafruit_lis3mdl |
| 10 | +import board |
| 11 | +from adafruit_lsm6ds.lsm6dsox import LSM6DSOX |
| 12 | +from gamblor21_ahrs import mahony |
| 13 | +import bitmaptools |
| 14 | +from adafruit_gc9a01a import GC9A01A |
| 15 | +import adafruit_imageload |
| 16 | +import displayio |
| 17 | +from fourwire import FourWire |
| 18 | + |
| 19 | + |
| 20 | +# change these values to your calibration values |
| 21 | +MAG_MIN = [-75.1973, -22.5665, -34.5221] |
| 22 | +MAG_MAX = [-1.2131, 68.1379, 20.8126] |
| 23 | +GYRO_CAL = [-0.0038, -0.0026, -0.0011] |
| 24 | + |
| 25 | + |
| 26 | +# use filter for more accurate, but slightly slower readings |
| 27 | +# otherwise just reads from magnetometer |
| 28 | +ahrs = True |
| 29 | +center_x, center_y = 120, 120 |
| 30 | + |
| 31 | +i2c = board.STEMMA_I2C() |
| 32 | +accel_gyro = LSM6DSOX(i2c) |
| 33 | +magnetometer = adafruit_lis3mdl.LIS3MDL(i2c) |
| 34 | +# Create the AHRS filter |
| 35 | +ahrs_filter = mahony.Mahony(50, 5, 100) |
| 36 | + |
| 37 | +# Variable to account for the offset between raw heading values |
| 38 | +# and the orientation of the display. |
| 39 | +offset_angle = 90 |
| 40 | + |
| 41 | + |
| 42 | +def map_range(x, in_min, in_max, out_min, out_max): |
| 43 | + """ |
| 44 | + Maps a value from one range to another. |
| 45 | +
|
| 46 | + :param x: The value to map |
| 47 | + :param in_min: The minimum value of the input range |
| 48 | + :param in_max: The maximum value of the input range |
| 49 | + :param out_min: The minimum value of the output range |
| 50 | + :param out_max: The maximum value of the output range |
| 51 | +
|
| 52 | + :return: The value mapped to the output range |
| 53 | + """ |
| 54 | + mapped = (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min |
| 55 | + if out_min <= out_max: |
| 56 | + return max(min(mapped, out_max), out_min) |
| 57 | + |
| 58 | + return min(max(mapped, out_max), out_min) |
| 59 | + |
| 60 | +last_heading = offset_angle |
| 61 | +heading = offset_angle |
| 62 | +last_update = time.monotonic() # last time we printed the yaw/pitch/roll values |
| 63 | +timestamp = time.monotonic_ns() # used to tune the frequency to approx 100 Hz |
| 64 | + |
| 65 | +# Display Setup |
| 66 | +spi = board.SPI() |
| 67 | +tft_cs = board.TX |
| 68 | +tft_dc = board.RX |
| 69 | +displayio.release_displays() |
| 70 | +display_bus = FourWire(spi, command=tft_dc, chip_select=tft_cs, reset=None) |
| 71 | +display = GC9A01A(display_bus, width=240, height=240) |
| 72 | +display.rotation = 90 |
| 73 | + |
| 74 | +# group to hold all of our visual elements |
| 75 | +main_group = displayio.Group() |
| 76 | +display.root_group = main_group |
| 77 | + |
| 78 | +# load the compass rose background image |
| 79 | +rose_bmp, rose_palette = adafruit_imageload.load("compass_rose.png") |
| 80 | +rose_tg = displayio.TileGrid(bitmap=rose_bmp, pixel_shader=rose_palette) |
| 81 | + |
| 82 | +# bitmap for the pointer needle |
| 83 | +pointer = displayio.Bitmap(5, 90, 2) |
| 84 | + |
| 85 | +# bitmap for erasing the pointer needle |
| 86 | +pointer_eraser = displayio.Bitmap(5, 90, 1) |
| 87 | + |
| 88 | +# pointer needle palette, red foreground, transparent background |
| 89 | +pointer_palette = displayio.Palette(2) |
| 90 | +pointer_palette[0] = 0x000000 |
| 91 | +pointer_palette[1] = 0xFF0000 |
| 92 | +pointer_palette.make_transparent(0) |
| 93 | +pointer.fill(1) |
| 94 | + |
| 95 | +# display sized bitmap to paste the rotated needle into |
| 96 | +rotated_pointer = displayio.Bitmap(240, 240, 2) |
| 97 | + |
| 98 | +# tilegrid for the rotated pointer needle |
| 99 | +pointer_tg = displayio.TileGrid(rotated_pointer, pixel_shader=pointer_palette) |
| 100 | + |
| 101 | +# add rose then pointer to the displaying group |
| 102 | +main_group.append(rose_tg) |
| 103 | +main_group.append(pointer_tg) |
| 104 | + |
| 105 | +while True: |
| 106 | + # if it's time to take a compass reading from the mag/gyro |
| 107 | + if (time.monotonic_ns() - timestamp) > 6500000: |
| 108 | + # read magnetic data |
| 109 | + mx, my, mz = magnetometer.magnetic |
| 110 | + |
| 111 | + # map it to the calibrated values |
| 112 | + cal_x = map_range(mx, MAG_MIN[0], MAG_MAX[0], -1, 1) |
| 113 | + cal_y = map_range(my, MAG_MIN[1], MAG_MAX[1], -1, 1) |
| 114 | + cal_z = map_range(mz, MAG_MIN[2], MAG_MAX[2], -1, 1) |
| 115 | + |
| 116 | + # if using ahrs filter |
| 117 | + if ahrs: |
| 118 | + # get accel/gyro data |
| 119 | + ax, ay, az, gx, gy, gz = accel_gyro.acceleration + accel_gyro.gyro |
| 120 | + |
| 121 | + # apply callibration offset |
| 122 | + gx += GYRO_CAL[0] |
| 123 | + gy += GYRO_CAL[1] |
| 124 | + gz += GYRO_CAL[2] |
| 125 | + |
| 126 | + # update filter |
| 127 | + ahrs_filter.update(gx, gy, -gz, ax, ay, az, cal_x, -cal_y, cal_z) |
| 128 | + |
| 129 | + # get yaw |
| 130 | + yaw_degree = ahrs_filter.yaw |
| 131 | + |
| 132 | + # convert radians to degrees |
| 133 | + heading = degrees(yaw_degree) |
| 134 | + |
| 135 | + else: # not using ahrs filter |
| 136 | + # calculate heading from calibrated mag data |
| 137 | + # and convert from radians to degrees |
| 138 | + heading = degrees(atan2(cal_y, cal_x)) |
| 139 | + |
| 140 | + # save time to compare next iteration |
| 141 | + timestamp = time.monotonic_ns() |
| 142 | + |
| 143 | + # if it's time to update the display |
| 144 | + if time.monotonic() > last_update + 0.2: |
| 145 | + # wrap negative heading values |
| 146 | + if heading < 0: |
| 147 | + heading += 360 |
| 148 | + |
| 149 | + # if the heading is sufficiently different from previous heading |
| 150 | + if abs(last_heading - heading) >= 2: |
| 151 | + #print(heading) |
| 152 | + |
| 153 | + # erase the previous pointer needle |
| 154 | + bitmaptools.rotozoom(rotated_pointer, pointer_eraser, |
| 155 | + ox=120, oy=120, |
| 156 | + px=pointer.width // 2, py=pointer.height, |
| 157 | + angle=radians(last_heading + offset_angle)) |
| 158 | + |
| 159 | + # draw the new pointer needle |
| 160 | + bitmaptools.rotozoom(rotated_pointer, pointer, |
| 161 | + ox=120, oy=120, |
| 162 | + px=pointer.width // 2, py=pointer.height, |
| 163 | + angle=radians(heading + offset_angle)) |
| 164 | + |
| 165 | + # set the previous heading to compare next iteration |
| 166 | + last_heading = heading |
| 167 | + |
| 168 | + # set the last update time to compare next iteration |
| 169 | + last_update = time.monotonic() |
0 commit comments