Tufty 2350 Documentation

Tuft 2340 Badge + STEM Kit box has a QR code labeled, “Start Here.” Unfortunately, this takes you to a 401 URL Not Found Error! Fortunately the links on the product page work for Docs and Micro-Python code :)

@BillyTPilgrim Thank you.

Now that some people have received their shiny new Tufty 2350s, I’m wondering what they think about the screen in bright daylight? How readable it is? Does the light sensor auto-adjust the screen brightness?

I’m debating between the Badger & Tufty for an outdoor project and while the e-ink display would be perfect in sunlight I’m concerned about the screen refresh rate impacting the UX.

Bright sunshine! Some hope in dismal Leicestershire at the moment. I’ve not noticed the screen brightness changing as ambient light goes up and down. Probably quite difficult to see in very bright sunshine.

Progress with the coding

I’ve managed to build my first TUFTY2350 app as specified in an earlier post. They only bit I’ve not found out how to do is use button_B to exit back to the menu.

# Tony Goodhew - A simple TUFTY2350 app - 31 January 2026

# Load some fonts
nope_font = rom_font.nope
compass_font = rom_font.compass
sins_font = rom_font.sins
ignore_font = rom_font.ignore
bacteria_font = rom_font.bacteria

count = 50
title_screen = True  # Flag for title screen

start_time = io.ticks

def update():                   # This is the main loop
    global title_screen
    global count
    global start_time
    
# Title screen     used for the first 2 seconds
    if title_screen:
        screen.pen = color.black
        screen.clear()
        screen.pen = color.white
        screen.font = sins_font
        screen.text("Tonygo2's First App",10,30)
        screen_font = compass_font
        screen.text("Title Screen",10,10)
        screen.pen = color.red
        screen.font = bacteria_font
        screen.text("Testing",30,60)               
        if io.ticks - start_time >= 2000:
            title_screen = False

# The main loop  - used after the first 2 seconds
    else:
        screen.pen = color.blue
        screen.clear()

        screen.pen = color.yellow
        screen.font = nope_font
        screen.text("MAIN LOOP", 5, 5)
        
        screen.font = ignore_font
        screen.text(str(count), 70,50)
        
        screen.pen = color.white
        screen.font = sins_font
        screen.text(str(count), 140,5)
        
        # Draw bar graph
        screen.pen = color.green
        rectangle = shape.rectangle(25, 90, count, 20)
        screen.shape(rectangle)
        
        # Check U and D buttons
        if io.BUTTON_UP in io.pressed:
            count = count + 10
            if count > 100:
                count = 100
        if io.BUTTON_DOWN in io.pressed:
            count = count - 10
            if count < 0:
                count = 0
        
            

It is not very elegant but it does work and does run from the menu.

I hope someone finds it useful.

The animated documentation is very impressive.

It would be great if other users joined in.

@snb Just a FYI post.
The Tufty 2040 also has a photo transistor light sensor. It’s accusable code wise. I use it to auto adjust the screen backlight brightness on my 2040. I’m thinking it should also be doable on the Tufty 2350.

@alphanumeric The front facing light sensor does not appear on the current hardware list but appears to be visible between the UP and DOWN buttons. I’ve not found it mentioned r in the current apps or documentation. The light sensor on the sensor stick is used in one of the example apps.

I’m feeling much more confident now that I’ve managed to get it working. I’m still using Thonny to edit the code directly on the TUFTY. You just have to press buttons on the back and reconnect Thonny to the COM port for each upload. You then reset TUFTY and use the menu to execute your changes. A bit of a pain but workable.

I’m going to try to get the mini I2C gamepad to control the SKULL sprite on the screen as my next project.

The product page mentions it, but that’s about it.

  • Phototransistor for light sensing

It’s likely wired to one of the ADC pins, need a schematic to see which one? On the Tufty2040 its GPIO28 / ADC 0.

If you look at the code the components appear to be pre-registered and named in the background MicroPython add-ons/ system - GPIO pins not necessary - just use the given name. I’m sure it will all come out eventually.

1 Like

Looking at the pin definition is the board definition (tufty2350/board/pins.csv at main · pimoroni/tufty2350 · GitHub), it seems to be GPIO43.

This is channel A3, since the Tufty2350 uses a RP2350B chip which has eight channels.

1 Like

That link is a really useful find. Thanks

1 Like

Thanks, it would make sense if the 2350 allows for this too. As the documentation is completed we’ll have a better idea of what’s possible. I did pre-order a Badger 2350 and I’m going to focus efforts there for now as the e-paper display would be best for outdoors and the ~8 hours battery life I need for the project.

I have to say I like what Pimoroni have done with these. They are a great advancement over the 2040 devices.

Regarding returning to the menu from the B button - we’ve sort of set things up with the intent that you return to the menu using the home button on the back of the machine. That said, it’s perfectly possible just by resetting the machine, assuming you’ve not changed anything so the machine still boots up to the menu:

if io.BUTTON_B in io.pressed:
    # (save whatever you need to save)
    on_exit()   # (if your program has an on_exit() method)
    machine.reset()

Having just chatted to the boss about it though, it looks like we’ll be putting in a command to do just that in a firmware revision at some point shortly, ‘cause it’s useful functionality to have.

About the animated documentation - those examples aren’t just gifs or something, they’re actually code running in what is essentially a Tufty simulator embedded in the web page.

What that means is that we’re planning to have this simulator available online to you good folks, so it’s possible to tinker and try things out live before taking your code onto the physical machine.

Still got to knock a few rough edges off it yet and get it presenting well for Badger and Blinky, but it’s well on the way.

1 Like

That is really great news - what a clever piece of coding.

I’ve just tried the EXIT code but it does not like it:

‘on_exit’ isn’t defined.

Update speed on Badger is controllable to an extent - fast, medium or full update, which I’ve just timed at around 1, 2 and 3 - 3.5 seconds respectively. Obviously there’s swings and roundabouts - Full takes longer but gives you nice dark blacks, whereas fast is nice and quick but does tend to leave ghosting on the screen sometimes. I find fast also gives nicer greys though, that’s why it’s set to fast for the dithered images in The Compendium.

You’ll only need to call on_exit() if you’ve defined one yourself, it’s the method that runs as the last thing after update() when you’re dropped back to the menu via the home button. If you’ve not set up an on_exit() method, you can just leave that line out.

Basically what you’re seeing here is what Tufty does behind the scenes when you press the home button - it breaks out of the update loop, calls on_exit() if there’s one defined in your program, then resets the machine.

Edited the above code to reflect that.

I am scratching my head and wondering, if you are on the right track. I have done programming all my life, including hard-core UI programming with callbacks all over the place. But on a microcontroller, that is only a niche. Most of your audience will not create highly interactive, menu-driven “applications”. In fact, I almost never use the buttons on most of my (many) badgers and tufties. And if so, I can handle the events from my own main-loop.

So while interactive programming does have it’s place, it is not the one solution that fits all needs. I would really appreciate if you also add docs and examples on how to use your hardware in the classical way, i.e. with some init-code and then a while True: loop. This will really help users that don’t have a professional background in programming. Otherwise, you will force your users not only to learn a new UI-api, but also to learn and adapt to a new programming paradigm.

1 Like

Yes one can decide when best to do a full refresh, partially update the screen etc. If the screen update code is non-blocking then that’s OK for me. The crucial thing is not missing a button press due to an in-progress screen refresh

I’m finding this very difficult, slow and more than a bit frustrating. I would really like a basic, normal MicroPython .uf2 to use as an alternative on this super hardware. I hate not being able to do a screen.update() when I want to. I can fly a PRESTO but I feel like a total idiot on this TUFTY2350.

I just cannot see why this code just skips over title screen and goes straight to the middle/ main section but does the exit section. It worked before I added the exit screen part.

# Tony Goodhew - A simple TUFTY2350 app - 2 January 2026
import machine

# Load some fonts
nope_font = rom_font.nope
compass_font = rom_font.compass
sins_font = rom_font.sins
ignore_font = rom_font.ignore
bacteria_font = rom_font.bacteria

count = 50
title_screen = True   # Flag for title screen
exit_screen = False   # Flag for exit screen
start_time = io.ticks

def update():         # This is the main loop
    global title_screen
    global exit_screen
    global count
    global start_time
        
    # Title screen   -  used for the first 2 seconds
    if title_screen:
        screen.pen = color.black
        screen.clear()
        screen.pen = color.red
        screen.font = bacteria_font
        screen.text("Testing",30,60)               
        if io.ticks - start_time >= 2000:
            title_screen = False

    # Exit screen - used for last 2 second after Button_B pressed
    if exit_screen:
        screen.pen = color.black
        screen.clear()               
        screen.pen = color.blue
        screen.font = bacteria_font
        screen.text("Good Bye",30,60)               
        if io.ticks - start_time >= 2000:
            exit_screen = False
            machine.reset()

    # The main loop  - used after the first 2 seconds
    else:
        screen.pen = color.blue
        screen.clear()

        screen.pen = color.yellow
        screen.font = nope_font
        screen.text("MAIN LOOP", 5, 5)
        
        screen.font = ignore_font
        screen.text(str(count), 70,50)
        
        screen.pen = color.white
        screen.font = sins_font
        screen.text(str(count), 140,5)
        
        # Draw bar graph
        screen.pen = color.green
        rectangle = shape.rectangle(25, 90, count, 20)
        screen.shape(rectangle)
        
        # Check U and D buttons
        if io.BUTTON_UP in io.pressed:
            count = count + 10
            if count > 100:
                count = 100
        if io.BUTTON_DOWN in io.pressed:
            count = count - 10
            if count < 0:
                count = 0
        if io.BUTTON_B in io.pressed:
            start_time = io.ticks
            exit_screen = True
            
            

        
    

I gave up with CircuitPython because it refused to support simple graphics but went ‘all layers and sprites’. I enjoy using PicoGraphics and can do quite complicated things like clocks.

I’m sure the majority of customers would appreciate a ‘normal’ .uf2 - perhaps add access to these rather nice and simple to use fonts.

So the issue comes with the if statements to check whether you’re in the title or exit screen. You’ve said:

if title_screen:
    (stuff)

if exit_screen:
    (stuff)

else:
    (stuff)

Basically, you’re telling it to draw the title screen because title_screen is set, then in the same loop you’re telling it to draw the main screen on top of it because exit_screen isn’t set. In other words, with it set up like this you’re telling it to draw the title screen as well as the main screen rather than instead of it, and because the title screen comes first in the code, the main screen is drawing over the top of it. But it’s a simple fix:

if title_screen:
    (stuff)

elif exit_screen:
    (stuff)

else:
    (stuff)

That “elif” makes all the difference. Now you only get to the bit about drawing the main screen if both title_screen and exit_screen are False.

Another way to do this might be instead of using two separate flags for title_screen and exit_screen, using a single variable that can have more than two values, like an integer. That way if it’s got a particular value, you know by definition it can’t be any of the others. And if things are getting more complex and you don’t want to memorise what number means what, you could always define it at the top of the file essentially as an enum, like:

class program_states:
    title_screen = 1
    main_screen = 2
    exit_screen = 3

current_state = program_states.title_screen

and then in your update() loop you can use

if current_state == program_states.title_screen:
    (do something)