Skip to content

Development#11

Merged
Gadgetoid merged 87 commits intomainfrom
dev-jan-2026
Feb 26, 2026
Merged

Development#11
Gadgetoid merged 87 commits intomainfrom
dev-jan-2026

Conversation

@Gadgetoid
Copy link
Member

@Gadgetoid Gadgetoid commented Jan 27, 2026

⚠️ You will need to flash the with-filesystem build since there are breaking changes which require the apps to be updated. Back up your filesystem first!

Latest experimental build here: https://github.com/pimoroni/tufty2350/actions/runs/22391690121/artifacts/5651200980

Release Notes (Draft)

Building upon the shipping firmware we've made many changes to the shape and
function of the Badgeware API. A lot of these changes are breaking, and you
will have to update your code for this release.

Bugfixes

FAT Filesystem Corruption

Users were encountering problems with the FAT "/system" partition spontaneously
erasing itself. This usually presents as the badge becoming unresponsive, with
Badger in particular showing the same screen (since it's never updated again.)

After much deliberation I realised we're setting the filesystem label every
time the board boots. This means there's a write to the FAT filesystem which
could potentially go wrong†. This is now removed, with the label set in the
filesystem image instead. This should ensure a more robust filesystem.

† - by wrong, I mean that a flash write starts with a block erase and if your
board resets between the erase and the new data being written then you have
a 4k empty block where there should have been 1/4th of file allocation table.

Other Bugfixes

  • Fix the right-hand edge of some vector shapes being cut off
  • Fix image blit overflow
  • Fix a sleep bug to decrease quiescent current

Changes

Running Apps

Our update loop setup for apps worked very well for games, and terribly for
everything else. We've heard your troubles with this approach, and had some
of our own.

launch has now been split into launch, which accepts a file path and loads
an app, and run which runs an update function, or any other function, in
either a continuous loop or one with a duration.

All apps now own their main loops, so you can simply while True your way to
app nirvana (at least on Blinky and Tufty, Badger is a whole different animal.)

To update your existing update based apps, you just need to add:

run(update)

To the bottom of your __init__.py. If you've already got a Thonny fixup:

from badgeware import run

...

if __name__ == "__main___":
    run(update)

Then delete it and swap it to the above.

run will run a function for you, handling the clear, update and poll calls
needed to keep Badgeware ticking. You don't have to use it, though, and can
call the new badge.update() function to update, clear and poll, or call
screen.update(), badge.clear() and badge.poll() separately. Up to you!

A typical main loop might look like this:

while True:
    # Do stuff
    badge.update()

You can now also access the buttons like they were ordinary MicroPython Pin
objects (because they are) so BUTTON_A.value() will give you the current
raw status of a button (0 when pressed) regardless of polling.

Badger

Since a display update is so costly on Badger, it works a little different
from the other boards. You can still run() your update function, but you
must call badge.update() to update the display when you want to.

You may also call wait_for_button_or_alarm(timeout=5000) which will sit in
a loop calling badge.poll() and waiting for a button press or an alarm
interrupt. After the timeout time (in ticks/milliseconds) your badge will go
into deep sleep to conserve battery. This timeout can be as long as you feel
is necessary for your app, or None to never automatically sleep.

Note: Badger will not sleep while connected to power!

To update an existing app, you would do:

def update():
    ...
    badge.update()
    wait_for_button_or_alarm(timeout=5000)

run(update)

Badger also gets a new NON_BLOCKING mode flag, letting you update the display
asynchronously. We've deployed this in the Menu, making it feel more responsive.

General

  • Output an error message if /system/main.py is missing
  • Output an error message if /system fails to mount

Drawing

  • color.black and color.white are now set to pure black/white on Badger and Blinky
  • The builtin pen() has been removed, use screen.pen = v

Advanced Drawing

  • vspan_tex is now blit_vspan and we have a new blit_hspan counterpart.
  • A new multi-ray raycast algorithm, and a single-ray dda

Builtin Functions

  • free() is now a builtin (call to show the memory delta)
  • run() is now a builtin, and can take a filename, eg: run("/system/apps/menu")

Badge (formerly known as io)

  • io is now badge and some straggler functions have been moved to badge:
    • io.ticks to badge.ticks
    • io.ticks_delta to badge.ticks_delta
    • badgeware.get_battery_level() is now badge.battery_level()
    • badgeware.is_charging() is now badge.is_charging()
    • badge.model (new) returns "tufty", "blinky" or "badger" depending on the board.
    • badge.uid returns the board UID (hex) as an ASCII string.
    • set_caselights() is now badge.caselights(), and caselights are gamma corrected.
    • badge.first_update (Badger only)
    • mode() is now badge.mode()
    • mode() now supports VSYNC† (Tufty only)
    • badgeware.get_disk_usage() moved to badge.disk_free() and now returns total, used and free in bytes
    • new badge.default_clear and badge.default_pen to control the update better

† - Avoiding VSYNC will normally give you a better framerate where your app logic takes over ~8ms, since you wont be missing entire VSYNC periods and having to wait up to 16.6ms. Using VSYNC will fix screen tearing at the cost of raw framerate. Currently display.update() on Tufty blocks for around 7.7ms leaving you only 8ms for logic/drawing. To enable VSYNC when changing mode use badge.mode(HIRES | VSYNC) or badge.mode(LORES | VSYNC)

Images

  • SpriteSheet and AnimatedSprite are now builtin, you need not
  • PNG now supports images up to 2048 pixels wide
  • JPEG image support
  • Loading images from RAM/buffer, eg: image.load(open("image.jpg", "rb").read())
  • Both JPEG and PNG images support downsizing on load my_image = image.load(filename, max_width, max_height)

RTC

  • rtc now has its own module.

Text

Both vector and pixel fonts now support UTF-8 codepoints, a fairly critical omission
from our shipping firmware- you can now use the degrees symbol and much, much more.

Advanced text layout functions have been moved to text, a builtin providing:

  • text.scroll() which has been modified heavily from scroll_text()
  • text.tokenise()
  • text.draw() which should now return the correct bounds

Two new ROM fonts have been added:

  • badgeware
  • badgewaremax

The font yolk has been updated with better language support.

Technical Stuff

  • Fix display update stability at some overclocks (Tufty only)
  • Switched MICROPY_OBJ_REPR to MICROPY_OBJ_REPR_C, which is slightly faster at the cost of RAM
  • Set the VID and PID of each board to match our assigned values.

Add a mode to return the framebuffer to the user without being cleared.

Allows for a slightly better framerate when the whole screen is overdrawn.
Add builtins for X2, X4 and OFF so we can:

image.antialias = X2
@Gadgetoid Gadgetoid mentioned this pull request Feb 2, 2026
MichaelBell and others added 2 commits February 2, 2026 14:30
Deprecate passing True/False into display.update() to pick pixel
doubled vs full resolution.

Move this to display.fullres(True / False)
MichaelBell and others added 7 commits February 3, 2026 22:54
This violated the cardinal rule: don't write to FAT from the board.

Highly probable source of our corrupt-from-just-resetting issue.

Remove any attempt to recover the FAT during boot and instead emit an
error. This leaves us with room to inspect or attempt to recover the
filesystem rather than the board simply resetting it.
Allow the user to attach Thonny or a serial terminal and try to
diagnose, recover or reformat the filesystem.

Note: _boot_fat.py occurs before USB is enabled.
badge.default_pen(pen) -> badge.default_pen = pen
badge.default_clear(pen) -> badge.default_clear = pen
w, h = badge.resolution() -> w, h = badge.resolution
badge.set_caselights(c1[, c2, c3, c4]) -> badge.caselights(c1[, c2, c3, c4])
c1, c2, c3, c4 = badge.get_caselights() -> c1, c2, c3, c4 = badge.caselights()
@Gadgetoid Gadgetoid force-pushed the dev-jan-2026 branch 2 times, most recently from 8d7973e to cd2ab9e Compare February 16, 2026 09:05
thirdr and others added 12 commits February 19, 2026 12:35
launch now accepts only a filename, since it is not doing the double
duty that run did to make apps tenable in Thonny.

run now takes only an update function and either loops indefinitely,
or runs for a given duration in ticks.

launch expects an app to block or call run, otherwise it will exit
back to the menu. The update function is no longer implicitly called
by the system, and must be called in the app (using run(update)) if
you decide to use one at all.

If run is given a duration it will provide a progress value, in all
cases the run function has its own internal ticks timed from the
start of the run. These are available inside a running function as
builtins: "loop.ticks" and "loop.progress".
Since __import__ would never return in blocking apps, app would never
get set. Retrieve on_exit from sys.modules instead.
@Gadgetoid Gadgetoid marked this pull request as ready for review February 26, 2026 13:08
@Gadgetoid Gadgetoid merged commit 02c4a8d into main Feb 26, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants