|
| 1 | +# SPDX-FileCopyrightText: 2024 Liz Clark for Adafruit Industries |
| 2 | +# SPDX-License-Identifier: MIT |
| 3 | +# Written by Liz Clark (Adafruit Industries) |
| 4 | +# with OpenAI ChatGPT v4 Jan 10, 2024 build |
| 5 | +# https://help.openai.com/en/articles/6825453-chatgpt-release-notes |
| 6 | + |
| 7 | +# https://chat.openai.com/share/19de8f24-3191-43b8-a9c9-95f1b8000e80 |
| 8 | + |
| 9 | +import time |
| 10 | +from math import atan2, degrees, cos, sin, radians |
| 11 | +import adafruit_lis3mdl |
| 12 | +import vectorio |
| 13 | +import displayio |
| 14 | +from adafruit_display_text import bitmap_label |
| 15 | +from adafruit_bitmap_font import bitmap_font |
| 16 | +from adafruit_qualia.graphics import Graphics, Displays |
| 17 | +from adafruit_lsm6ds.lsm6dsox import LSM6DSOX |
| 18 | +from gamblor21_ahrs import mahony |
| 19 | +import bitmaptools |
| 20 | +from jpegio import JpegDecoder |
| 21 | +# change these values to your calibration values |
| 22 | +MAG_MIN = [-11.5902, -47.1353, -28.7635] |
| 23 | +MAG_MAX = [79.7866, 48.0854, 63.461] |
| 24 | +GYRO_CAL = [-7.3934, -0.000100605, 2.7703] |
| 25 | +# use filter for more accurate, but slightly slower readings |
| 26 | +# otherwise just reads from magnetometer |
| 27 | +ahrs = True |
| 28 | +center_x, center_y = 240, 240 |
| 29 | + |
| 30 | +graphics = Graphics(Displays.ROUND21, default_bg=None, auto_refresh=False) |
| 31 | + |
| 32 | +i2c = graphics.i2c_bus |
| 33 | +accel_gyro = LSM6DSOX(i2c) |
| 34 | +magnetometer = adafruit_lis3mdl.LIS3MDL(i2c) |
| 35 | +# Create the AHRS filter |
| 36 | +ahrs_filter = mahony.Mahony(50, 5, 100) |
| 37 | + |
| 38 | +group = displayio.Group() |
| 39 | +# palette for vectorio graphics |
| 40 | +pointer_pal = displayio.Palette(5) |
| 41 | +pointer_pal[0] = 0xFFFF00 |
| 42 | +pointer_pal[1] = 0x000000 |
| 43 | +pointer_pal[2] = 0xFFFFFF |
| 44 | +pointer_pal[3] = 0xFF0000 |
| 45 | +pointer_pal[4] = 0x0000FF |
| 46 | +pointer_pal.make_transparent(0) |
| 47 | +# compass image is a jpeg |
| 48 | +decoder = JpegDecoder() |
| 49 | +width, height = decoder.open("/compass.jpg") |
| 50 | +bitmap_compass = displayio.Bitmap(width, height, 20) |
| 51 | +palette_compass = displayio.ColorConverter(input_colorspace = displayio.Colorspace.RGB565_SWAPPED) |
| 52 | +decoder.decode(bitmap_compass) |
| 53 | +# blank bitmap for rotozoom |
| 54 | +compass_blank = displayio.Bitmap(width, height, 1) |
| 55 | +# carrier bitmap for compass for rotozoom |
| 56 | +compass_scribble = displayio.Bitmap(width, height, 20) |
| 57 | +tile_grid = displayio.TileGrid(compass_scribble, pixel_shader=palette_compass) |
| 58 | +# only tilegrid is added to group |
| 59 | +group.append(tile_grid) |
| 60 | + |
| 61 | +radius = center_x |
| 62 | +angle = 225 |
| 63 | +rad_angle = radians(angle) |
| 64 | + |
| 65 | +# place small circle to denote heading direction from 9-DoF relative to display |
| 66 | +circle_radius = 5 |
| 67 | +header_angle = radians(135) |
| 68 | +edge_x = center_x + radius * cos(header_angle) |
| 69 | +edge_y = center_y + radius * sin(header_angle) |
| 70 | +adjusted_x = edge_x - circle_radius * cos(header_angle) |
| 71 | +adjusted_y = edge_y - circle_radius * sin(header_angle) |
| 72 | +header = vectorio.Circle(pixel_shader=pointer_pal, color_index = 2, |
| 73 | + radius=circle_radius, x=int(adjusted_x), y=int(adjusted_y)) |
| 74 | + |
| 75 | +center = vectorio.Circle(pixel_shader=pointer_pal, color_index = 2, radius=50, x=240, y=240) |
| 76 | + |
| 77 | +font_file = "/Roboto-Regular-47.pcf" |
| 78 | +font = bitmap_font.load_font(font_file) |
| 79 | + |
| 80 | +direction_text = bitmap_label.Label(font, text="000", color=None) |
| 81 | +direction_text.x = center.x - 40 |
| 82 | +direction_text.y = center.y |
| 83 | +direction_bitmap = direction_text.bitmap |
| 84 | + |
| 85 | +direction_blank = displayio.Bitmap(direction_text.bounding_box[2], |
| 86 | + direction_text.bounding_box[2], 1) |
| 87 | +direction_scribble = displayio.Bitmap(direction_text.bounding_box[2], |
| 88 | + direction_text.bounding_box[2], len(pointer_pal)) |
| 89 | +direction_grid = displayio.TileGrid(direction_scribble, pixel_shader=pointer_pal, x=200, y=200) |
| 90 | + |
| 91 | +def create_line_of_squares(l, n, color): |
| 92 | + squares = [] |
| 93 | + square_size = l // n |
| 94 | + for i in range(n): |
| 95 | + x = center_x + i * square_size |
| 96 | + y = center_y - square_size // 2 |
| 97 | + square_points = [(0, 0), (square_size, 0), |
| 98 | + (square_size, square_size), (0, square_size)] |
| 99 | + square = vectorio.Polygon(pixel_shader=pointer_pal, |
| 100 | + color_index=color, points=square_points, x=x, y=y) |
| 101 | + group.append(square) |
| 102 | + squares.append(square) |
| 103 | + return squares |
| 104 | + |
| 105 | +def update_line_of_squares(squares, h, l): |
| 106 | + r = radians(-h) |
| 107 | + n = len(squares) |
| 108 | + square_size = l // n |
| 109 | + x = center_x - (square_size - 2) |
| 110 | + y = center_y - (square_size - 2) |
| 111 | + for i, square in enumerate(squares): |
| 112 | + offset_x = x + i * square_size - x |
| 113 | + offset_y = -square_size // 2 |
| 114 | + rotated_x = offset_x * cos(r) - offset_y * sin(r) |
| 115 | + rotated_y = offset_x * sin(r) + offset_y * cos(r) |
| 116 | + square.x = int(x + rotated_x) |
| 117 | + square.y = int(y + rotated_y) |
| 118 | + |
| 119 | +length = 200 # Length of the lines |
| 120 | +num_squares = 20 # Number of squares for each line |
| 121 | + |
| 122 | +vertical_squares = create_line_of_squares(length, num_squares, 3) |
| 123 | +update_line_of_squares(vertical_squares, angle, length) |
| 124 | + |
| 125 | +def map_range(x, in_min, in_max, out_min, out_max): |
| 126 | + mapped = (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min |
| 127 | + if out_min <= out_max: |
| 128 | + return max(min(mapped, out_max), out_min) |
| 129 | + |
| 130 | + return min(max(mapped, out_max), out_min) |
| 131 | + |
| 132 | +group.append(center) |
| 133 | +group.append(header) |
| 134 | +group.append(direction_text) |
| 135 | +group.append(direction_grid) |
| 136 | + |
| 137 | +graphics.display.root_group = group |
| 138 | + |
| 139 | +last_heading = angle |
| 140 | +heading = angle |
| 141 | +last_update = time.monotonic() # last time we printed the yaw/pitch/roll values |
| 142 | +timestamp = time.monotonic_ns() # used to tune the frequency to approx 100 Hz |
| 143 | +spin_rose = True |
| 144 | + |
| 145 | +graphics.display.refresh() |
| 146 | + |
| 147 | +while True: |
| 148 | + if graphics.touch.touched: |
| 149 | + spin_rose = not spin_rose |
| 150 | + # reset last_heading to trigger an update |
| 151 | + last_heading = heading + 5 |
| 152 | + if spin_rose: |
| 153 | + direction_text.color = None |
| 154 | + update_line_of_squares(vertical_squares, angle, length) |
| 155 | + else: |
| 156 | + bitmaptools.rotozoom(compass_scribble, bitmap_compass, angle = radians(0)) |
| 157 | + direction_text.color = pointer_pal[1] |
| 158 | + graphics.display.refresh() |
| 159 | + # touch debounce delay |
| 160 | + time.sleep(0.2) |
| 161 | + if (time.monotonic_ns() - timestamp) > 6500000: |
| 162 | + mx, my, mz = magnetometer.magnetic |
| 163 | + cal_x = map_range(mx, MAG_MIN[0], MAG_MAX[0], -1, 1) |
| 164 | + cal_y = map_range(my, MAG_MIN[1], MAG_MAX[1], -1, 1) |
| 165 | + cal_z = map_range(mz, MAG_MIN[2], MAG_MAX[2], -1, 1) |
| 166 | + if ahrs: |
| 167 | + ax, ay, az, gx, gy, gz = accel_gyro.acceleration + accel_gyro.gyro |
| 168 | + gx += GYRO_CAL[0] |
| 169 | + gy += GYRO_CAL[1] |
| 170 | + gz += GYRO_CAL[2] |
| 171 | + ahrs_filter.update(gx, gy, -gz, ax, ay, az, cal_x, -cal_y, cal_z) |
| 172 | + yaw_degree = ahrs_filter.yaw |
| 173 | + heading = degrees(yaw_degree) |
| 174 | + else: |
| 175 | + heading = degrees(atan2(cal_y, cal_x)) |
| 176 | + timestamp = time.monotonic_ns() |
| 177 | + if time.monotonic() > last_update + 0.2: |
| 178 | + if heading < 0: |
| 179 | + heading += 360 |
| 180 | + if abs(last_heading - heading) >= 2: |
| 181 | + direction_text.text = str(int(heading)) |
| 182 | + if spin_rose: |
| 183 | + direction_bitmap = direction_text.bitmap |
| 184 | + |
| 185 | + bitmaptools.rotozoom(compass_scribble, bitmap_compass, |
| 186 | + angle = radians(-heading+angle)) |
| 187 | + bitmaptools.rotozoom(direction_scribble, direction_bitmap, angle = rad_angle) |
| 188 | + graphics.display.refresh() |
| 189 | + bitmaptools.rotozoom(direction_scribble, direction_blank, angle = rad_angle) |
| 190 | + bitmaptools.rotozoom(compass_scribble, compass_blank, |
| 191 | + angle = radians(-heading+angle)) |
| 192 | + else: |
| 193 | + update_line_of_squares(vertical_squares, -heading + 90, length) |
| 194 | + graphics.display.refresh() |
| 195 | + last_heading = heading |
| 196 | + last_update = time.monotonic() |
0 commit comments