From 6dcf7e1613b8456f4696ee1a7d8625dcbb9bca77 Mon Sep 17 00:00:00 2001 From: TechCabin Date: Sun, 7 Jun 2026 23:07:55 +0100 Subject: [PATCH 1/2] updated firmware to add GPSEvents --- .DS_Store | Bin 0 -> 6148 bytes EEPROM/gps.mpy | Bin 1517 -> 1352 bytes EEPROM/gps.py | 163 +++++++++++++++++++++++++++++------------------ hexpansions.json | 55 +++++----------- 4 files changed, 116 insertions(+), 102 deletions(-) create mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..a8f9139a4edc6cd3cae85ee7e2b9123089604cb2 GIT binary patch literal 6148 zcmeHKOH0E*5Z<-5O({YS3Oz1(E!dVI6fYsRM^PHlgGxYjfSF7ml~c%H}@udHR_AeaJ=n{ z&3H8E3+vR{Zac=&@yXe2{t~^$$(tgQ1Lsl>G?wrVN@Yz!`V{#v$1Xn1QbMDW1&!P9Z=!*8U0N}6wvW4fhaWE8ViNs0pThY zP^EJH#Na9&>_W%c8ViLgopCub^rJ^EA1_>v4t62K8MhTuO$-nNRR$WWY2x`mg0nzby~XS^?z|3JS&*sDOZ8xdcE1_mQ4-D!)J-;%tqD VLhJ?YG98dE0+JA_h=E^V;0qfmN^SrE literal 0 HcmV?d00001 diff --git a/EEPROM/gps.mpy b/EEPROM/gps.mpy index ebf465aa4810c1cd921db1815bd4d9954c4f02d2..ca6ece88edb4192a16a6ff31759fadf44405ba65 100644 GIT binary patch literal 1352 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 diff --git a/EEPROM/gps.py b/EEPROM/gps.py index 3e36ce0..716d6dc 100644 --- a/EEPROM/gps.py +++ b/EEPROM/gps.py @@ -1,89 +1,126 @@ -# Minimal length method names to make the mpy file as small as possible so it will fit in the 2k hexpansion EEPROM. -# Minimal functionality to get a GPS fix and display it -# import app -from app_components.tokens import button_labels -from events.input import Buttons, BUTTON_TYPES +import asyncio +import time + +from events import Event from system.eventbus import eventbus -from system.scheduler.events import RequestForegroundPushEvent, RequestStopAppEvent from machine import UART, Pin -class GPSApp(app.App): # pylint: disable=no-member - """ App to get GPS data from a GPS module connected to the hexpansion and display it on the badge. """ - VERSION = 1 # Increment this when making changes to the app that require the hexpansion app to be re-flashed with the new code. + +class GPSApp(app.App): + """Provides a GPS API for apps to use directly and GPS Events that other apps may subscribe to.""" + + VERSION = 2 # Increment this when making changes to the app that require the hexpansion app to be re-flashed with the new code. + + class GPSEvent(Event): + def __init__(self, position, speed, bearing): + self.position = position + self.speed = speed + self.bearing = bearing + + def __str__(self): + return f"GPS fix {self.position}, speed {self.speed} knots, bearing {self.bearing}°" + def __init__(self, config=None): super().__init__() + + # Config is mandatory, we're running from the EEPROM if config is None: - raise TypeError # The app should not be run without a config as it won't work (shouldn't happen anyway if run from the hexpansion EEPROM) - self.config = config # Enables HexManager to check which port app is associated with - self.t = config.pin[0] - self.x = config.pin[1] + raise TypeError + self.config = config + + # GPS fix data + self._position = None + self._bearing = 0.0 + self._speed = 0.0 + + # Specifying a small time out to wait before giving up on receiving + # more characters ensures we always read full messages from the UART + # This reduces parse errors due to only having half a message + self.to = 10 + self.uart = UART(1, baudrate=9600, tx=config.pin[0], rx=config.pin[1], timeout=self.to) + + # Reset pin self.r = config.pin[2] - self.b = Buttons(self) - self.l = None # Last GPS fix as a string in the format "lat,lon" or None if no fix yet. Latitude and longitude are rounded to 5 decimal places which gives a precision of about 1 meter, more than enough for badgebot's purposes. - self.u = UART(1, baudrate=9600, tx=self.t, rx=self.x) self.r.init(mode=Pin.OUT) self.r.value(1) - self.z = -1 # Ticks timer - time since GPS reset, used to control when to release the GPS from reset after resetting it - # and then used to time how long since last valid GPS fix. - # also used as a flag (-1) to indicate that ForegroundPushEvent has not yet been emitted - eventbus.on_async(RequestStopAppEvent, self.s, self) - async def s(self, _e: RequestStopAppEvent): - """ handle app stop """ - if _e.app == self: - self.r.value(1) - self.u.deinit() + # Time since last valid GPS fix + self.z = 0 + + @property + def position(self): + """Position as a (latitude, longitude) tuple""" + return self._position + + @property + def bearing(self): + """Course over ground in degrees from true north""" + return self._bearing - def update(self, _d): - """ Update the app state - expire last_fix if it is too old """ - if self.b.get(BUTTON_TYPES["CANCEL"]): - self.b.clear() - self.minimise() + @property + def speed(self): + """Ground speed in knots""" + return round(self._speed, 2) - if self.z < 0: - eventbus.emit(RequestForegroundPushEvent(self)) + async def background_task(self): + """Override the default background task behaviour to give more time to other apps""" + last = time.ticks_ms() + while True: + start = time.ticks_ms() + delta = time.ticks_diff(start, last) + result = self.background_update(delta) + # Get successive messages fast, but yield more time to other apps + # if there was nothing to read, this lowers the frequency of + # occurrances of blocking for the full read timeout to elapse when + # nothing is being sent over the UART + await asyncio.sleep_ms(25 if result else 250 - self.to) + last = start - self.z +=_d + def background_update(self, delta): + self.z += delta + # Delay releasing the reset pin a little bit if self.r.value(): if self.z > 99: self.r.value(0) - if self.l: + # Clear fix data if we haven't had a fix for a while + if self._position: if self.z > 9999: - self.l = None + self._position = None + self._speed = 0 - def background_update(self, _d): - """ Update the app state in the background - read GPS data """ - l = self.u.readline() + l = self.uart.readline() if l: - #print(l) try: p = l.decode().strip().split(',') - if (p[0] != "$GPRMC" and p[0] != "$GNRMC") or p[2] != "A" or not p[3] or not p[5]: - return None - t = float(p[3][:2]) + float(p[3][2:]) / 60 - n = float(p[5][:3]) + float(p[5][3:]) / 60 - if p[4] == "S": - t = -t - if p[6] == "W": - n = -n - self.l = str(round(t, 5)) - self.n = str(round(n, 5)) - self.z = 0 - except (UnicodeError, ValueError, AttributeError): + if p[0] == "$GPRMC" or p[0] == "$GNRMC": + if p[2] == "A": + lat = float(p[3][:2]) + float(p[3][2:]) / 60 + lon = float(p[5][:3]) + float(p[5][3:]) / 60 + if p[4] == "S": + lat = -lat + if p[6] == "W": + lon = -lon + self._position = (round(lat, 5), round(lon, 5)) + self._speed = float(p[7]) + self._bearing = float(p[8]) + + # Eliminate satellite jitter when stationary by rounding + # very small velocities to zero + if self._speed < 1: + self._speed = 0 + + # Reset the time since last fix if we successfully got a valid fix message + self.z = 0 + + # Send event to subscribers + eventbus.emit(self.GPSEvent(self._position, self._speed, self._bearing)) + except (UnicodeError, ValueError, AttributeError, IndexError): pass + return True + return False + - def draw(self, _c): - _c.font_size = 40 # not using defined sizes to save bytes in the mpy file - _c.rgb(0, 0.2, 0).rectangle(-120, -120, 240, 240).fill() - _c.rgb(0, 1, 0).move_to(-35, -50).text("GPS") - if self.l: - _c.move_to(-110, 0).text("Lat:" + self.l) - _c.move_to(-110, 40).text("Lon:" + self.n) - else: - _c.move_to(-110, 0).text("Searching...") - button_labels(_c, cancel_label="Back") - -__app_export__ = GPSApp #pylint: disable=invalid-name +__app_export__ = GPSApp # pylint: disable=invalid-name diff --git a/hexpansions.json b/hexpansions.json index 36c8d1b..089d763 100644 --- a/hexpansions.json +++ b/hexpansions.json @@ -10,12 +10,11 @@ ], "fields": { "pid": "REQUIRED. Product ID (16-bit integer), e.g. \"0xD15C\".", - "name": "REQUIRED. Human-friendly display name shown in HexManager, e.g. 'HexDrive'.", - "friendly_name": "OPTIONAL. Short name written into the EEPROM header and shown by BadgeOS insertion notifications. Maximum 9 chars. Defaults to 'name'.", + "name": "REQUIRED. Human-friendly display name shown on screen, e.g. 'HexDrive'. MAXIMUM 9 chars.", "sub_type": "OPTIONAL. Short label for this specific variant, e.g. '2 Motor'. Shown to the user alongside the name.", "vid": "OPTIONAL. Vendor ID (16-bit integer), e.g. \"0xCAFE\". Default 0xCAFE which is the UHB-IF uncontrolled VID.", - "eeprom_total_size": "OPTIONAL. Total EEPROM size in BYTES.", - "eeprom_page_size": "OPTIONAL. EEPROM page size in BYTES.", + "eeprom_total_size": "OPTIONAL. Total EEPROM size in BYTES. Default 8192 (8 KB).", + "eeprom_page_size": "OPTIONAL. EEPROM page size in BYTES. Default 32.", "app_mpy_name": "OPTIONAL. Filename (without .mpy extension) of the compiled app to write to the hexpansion EEPROM. File must be in the EEPROM sub-folder of the HexManager app on the badge (e.g. /apps/TeamRobotmad_HexManager/EEPROM/myhex.mpy). NOTE: the file is renamed to 'app.mpy' when programmed onto the EEPROM so that Badge OS automatically detects and runs it when the hexpansion is inserted into a badge slot.", "app_mpy_version": "OPTIONAL. Version number (integer or string) of the .mpy app. Used to detect when an upgrade is needed.", "app_name": "OPTIONAL. Python class name of the hexpansion app, e.g. 'MyHexApp'. Used to check if the app is running." @@ -48,63 +47,35 @@ "sub_type": "2 Line Sensors" }, { - "pid": "0x10C8", "vid": "0xCBCB", "name": "HexDriveV2", "sub_type": "Uncommitted", + "pid": "0x10CB", "vid": "0xCBCB", "name": "HexDriveV2", "sub_type": "Uncommitted", "eeprom_total_size": 32768, "eeprom_page_size": 64, - "app_mpy_name": "hexdrive2", "app_mpy_version": 7, "app_name": "HexDriveApp" - }, - { - "pid": "0x10C9", "vid": "0xCBCB", "name": "HexDriveV2", "sub_type": "2 Servo", - "eeprom_total_size": 32768, "eeprom_page_size": 64, - "app_mpy_name": "hexdrive2", "app_mpy_version": 7, "app_name": "HexDriveApp" + "app_mpy_name": "hexdrive", "app_mpy_version": 6, "app_name": "HexDriveApp" }, { "pid": "0x10CA", "vid": "0xCBCB", "name": "HexDriveV2", "sub_type": "2 Motor", "eeprom_total_size": 32768, "eeprom_page_size": 64, - "app_mpy_name": "hexdrive2", "app_mpy_version": 7, "app_name": "HexDriveApp" - }, - { - "pid": "0x11CE", "vid": "0xCBCB", "name": "HexDriveV2", "sub_type": "Left Motor", - "eeprom_total_size": 32768, "eeprom_page_size": 64, - "app_mpy_name": "hexdrive2", "app_mpy_version": 7, "app_name": "HexDriveApp" - }, - { - "pid": "0x12CE", "vid": "0xCBCB", "name": "HexDriveV2", "sub_type": "Right Motor", - "eeprom_total_size": 32768, "eeprom_page_size": 64, - "app_mpy_name": "hexdrive2", "app_mpy_version": 7, "app_name": "HexDriveApp" + "app_mpy_name": "hexdrive", "app_mpy_version": 6, "app_name": "HexDriveApp" }, { - "pid": "0x10CF", "vid": "0xCBCB", "name": "HexDriveV2", "sub_type": "1 Mot 1 Srvo", + "pid": "0x10CC", "vid": "0xCBCB", "name": "HexDriveV2", "sub_type": "2 Servo", "eeprom_total_size": 32768, "eeprom_page_size": 64, - "app_mpy_name": "hexdrive2", "app_mpy_version": 7, "app_name": "HexDriveApp" + "app_mpy_name": "hexdrive", "app_mpy_version": 6, "app_name": "HexDriveApp" }, { "pid": "0x3000", "vid": "0xCBCB", "name": "HexTest", "eeprom_total_size": 65536, "eeprom_page_size": 128 }, - { - "pid": "0x5000", "vid": "0xCBCB", "name": "HexCurrent", "friendly_name": "HexCurent", - "eeprom_total_size": 65536, "eeprom_page_size": 128, - "app_mpy_name": "hexcurrent", "app_mpy_version": 1, "app_name": "HexCurrentApp" - }, { "pid": "0x4000", "vid": "0xCBCB", "name": "HexDiag", "eeprom_total_size": 65536, "eeprom_page_size": 128 }, { - "pid": "0x6000", "vid": "0xCBCB", "name": "XYStage", - "eeprom_total_size": 8192, "eeprom_page_size": 32 - }, - { - "pid": "0x6001", "vid": "0xCBCB", "name": "Joystick", - "eeprom_total_size": 8192, "eeprom_page_size": 32 - }, - { - "pid": "0x1295", "vid": "0x7CAB", "name": "GPS", "sub_type": "L80K", + "pid": "0x1295", "vid": "0xCAFE", "name": "GPS", "sub_type": "L80K", "eeprom_total_size": 2048, "eeprom_page_size": 16, "app_mpy_name": "gps", "app_mpy_version": 1, "app_name": "GPSApp" }, { - "_comment": "Flopagon - small 2048-byte EEPROM. EEPROM too small for the app.", + "_comment": "Flopagon - small 2048-byte EEPROM. EEPROM too small for an app.", "pid": "0xD15C", "vid": "0xCAFE", "name": "Flopagon", "eeprom_total_size": 2048, "eeprom_page_size": 16 }, @@ -113,6 +84,12 @@ "pid": "0xCAFF", "vid": "0xCAFE", "name": "Club Mate", "eeprom_total_size": 8192, "eeprom_page_size": 32, "app_mpy_name": "caffeine", "app_mpy_version": 1, "app_name": "CaffeineJitter" + }, + { + "_comment": "GPS hexpansion.", + "pid": "0xBEAC", "vid": "0x7CAB", "name": "GPS", + "eeprom_total_size": 2048, "eeprom_page_size": 16, + "app_mpy_name": "gps", "app_mpy_version": 1, "app_name": "GPS" } ] } From 61334f338cdbd46e9e2497a6a29b2c0f3c42da4c Mon Sep 17 00:00:00 2001 From: TechCabin Date: Sun, 7 Jun 2026 23:10:27 +0100 Subject: [PATCH 2/2] removed previous GPS firmware option --- hexpansions.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/hexpansions.json b/hexpansions.json index 089d763..a2aafe0 100644 --- a/hexpansions.json +++ b/hexpansions.json @@ -69,11 +69,6 @@ "pid": "0x4000", "vid": "0xCBCB", "name": "HexDiag", "eeprom_total_size": 65536, "eeprom_page_size": 128 }, - { - "pid": "0x1295", "vid": "0xCAFE", "name": "GPS", "sub_type": "L80K", - "eeprom_total_size": 2048, "eeprom_page_size": 16, - "app_mpy_name": "gps", "app_mpy_version": 1, "app_name": "GPSApp" - }, { "_comment": "Flopagon - small 2048-byte EEPROM. EEPROM too small for an app.", "pid": "0xD15C", "vid": "0xCAFE", "name": "Flopagon",