Getting started

This is a short guide to starting work with M5Stack CoreS3 and Tab5 (and Paper) devices. These are chosen from the wide range of M5Stack devices because they are recently released (as I am writing in mid-2026) with updated features including integrated touchscreens and expandability. Touchscreens greatly simplify the implementation of user interfaces. Some details are shown in the table below. Other devices can be used with few modifications to the advice here - check the M5Stack website.

The CoreS3 and Tab5 ESP32-S3 and ESP32-P4 boards present a USB device directly from the chip, so there is no serial bridge and no driver to install.

What is UiFlow2?

UiFlow2 is M5Stack's browser-based Blockly IDE at uiflow2.m5stack.com. You drag blocks; it generates MicroPython in a pane beside them; the code is pushed straight onto the device over USB or Wi-Fi. Nothing is compiled and no toolchain is installed on your machine.

Three pieces have to line up, and most first-run problems are one of them missing:

  • The UiFlow2 MicroPython firmware on the device — this is the runtime, and it is not the same firmware as the earlier UiFlow1.

  • M5Burner, the desktop tool that writes that firmware and bakes in your Wi-Fi credentials.

  • The web IDE, which talks to the running device either through a serial port in the browser or through M5Stack's cloud relay.

A supported core, a data-capable USB-C cable (charge-only cables are the single most common cause of "device not found"), and a Chromium-based browser.

A web browser from the "Chrome" family must be used.
The IDE's WebTerminal and WebBurner use the Web Serial API, which only exists in Chromium browsers — Chrome, Edge, Brave, Opera, Arc. Firefox and Safari cannot open a serial port, so on those you are limited to the Wi-Fi access-code route.

Set up the device

UiFlow2 is the browser IDE at uiflow2.m5stack.com. It needs matching UiFlow2 MicroPython firmware on the device, written by the desktop M5Burner tool. Neither board needs a USB driver — the ESP32-S3 in CoreS3 and the ESP32-P4 in Tab5 both present USB directly.

  1. Install M5Burner v3 from the M5Stack downloads page and sign in.

  2. Put the board in download mode. For the CoreS3, long-press the power/G0 button until the indicator turns from red to green, then release. For the Tab5, hold reset about 2 seconds until the internal green LED flashes rapidly, then release.

  3. Pick the board in M5Burner's list, choose the UIFlow2 firmware (not UIFlow1, not a factory demo), Download then Burn.

  4. In the configuration screen set a 2.4 GHz Wi-Fi SSID and password, your timezone and SNTP server, and boot option Show startup menu and network setup.

  5. Reset. The startup screen shows Wi-Fi status and an access code.

  6. Open the IDE in a Chromium browser (Chrome, Edge, Brave — Web Serial doesn't exist in Firefox or Safari). The IDE's WebTerminal and WebBurner use the Web Serial API, which only exists in Chromium browsers. Click Controller, pick your board, then either connect over Wi-Fi with the access code, or open WebTerminal, choose the serial port and Connect.


The M5Stack device can be connected directly via USB-C, or though Wi-Fi. The Wi-Fi route is convenient but silent. Over USB the WebTerminal gives you the REPL (Read–Eval–Print Loop), a simple interactive programming environment) and full tracebacks, which is seen in the difference between error messages such as "it stopped" and "line 42, AttributeError".

Ensure that you select the correct microcontroller device in the UiFlow2 interface before you start dragging blocks — the palette, the UI-editor canvas size and the pin assignments in generated code all depend on this. For more information, see: https://docs.m5stack.com/en/uiflow2/uiflow_web

Development environment: 1. Menu Bar, 2. UI Editor, 3. Functional Extension Module, 4. Blockly List, 5. Workspace, 6. Run and Debug

Select the correct device.

Menu bar: 1. Login, 2. Project management, 3. Save project, 4. Project name, 5. Code preview: switch between the graphical programming mode and the code editing, 6. Project community zone, 7. File operations, 8. Help, 9. Settings.

Switch to the editor for user interface. For devices with display.

UI Editor: 1. Layout Tools: Align or center text, images, etc. 2. Screen Display Area: You can drag the UI to layout the details of the UI on the screen 1:1. 3. Interface Style: Adjust visual parameters such as UI font, rounded corners, and shadows.

Load software modules. Choose the software modules that you wish to load for your particular combination of hardware and application needs. This can be updated if new functions are added.

Blocks: The software blocks are accessed from a series of drop-down menu items. The blocks are categorised and colour-socked according to function. (More info below).

Run and Debug: 1. WebTerminal: A browser-based serial debugging tool. 2. WebBurner: A wireless firmware burning tool. 3. Device File Manager: A device storage space file manager for import of accessory files. 4. Run Once: to run the program once for testing. 5. Run Always: download the edited program directly to the device and configure it to run automatically on startup.

Note: these screen shot images have been adapted from tutorial material at: https://docs.m5stack.com/en/uiflow2/uiflow2_layout. This is recommended as a reference source.

UiFlow2 programming interface
UiFlow2 Block palette

Blocks are grouped into categories down the left side of the workspace. Blockly is a typed language expressed as geometry. A block's silhouette — where it has notches, tabs, plugs and mouths — is its type signature, and it is the reason the editor refuses some combinations. The Blockly elements can be categorised to two ways.

By shape — seven silhouettes, each drawn on the page, because in Blockly the shape is the type signature:

  • Hat (no upper connector) — Setup, Loop, event handlers → becomes a def

  • Statement (notch above, tab below) → one line

  • C-block (holds a substack) — if/repeat/while → a compound statement plus an indent

  • Cap (flat bottom) — break, return, restart → nothing follows

  • Value (left plug, no vertical connectors) → an expression, never a line

  • Boolean (hexagonal) → a value restricted to true/false, which is how if refuses a number

  • Field — dropdowns, pin numbers, colours; not a block at all, an argument edited in place

(See diagram, right)

By source - seven drawers: Core language (Variables, Logic, Loops, Math, Text, Lists, Functions, Timer, Json/Map/Bytearray), System, User interface (28 M5UI classes plus the legacy Widgets layer), Hardware, Advanced, Attached hardware (Unit/Module/Base/Hat/Stamp/Cap/Chain - this is the biggest, but elements are invisible until you declare them), and Cloud & media.

The one practical point: only the first five drawers are always there. The other two exist because of choices made outside the workspace - e.g. controller selection, widgets placed in the UI editor, units added in the extension panel.

Conversion of Blockly into MicroPython

Blockly encoded functions are connected in the workspace to form a tree-like assembly. The tree of block functions map predictably onto the generated MicroPython. Once you can read the mapping, the MicroPython pane can become a useful debugging tool. In the example below: A hat block becomes a function; blocks stacked in its body become the lines of that function; a C-block adds an indent level; and a value block never becomes a line of its own - it is substituted into whatever socket holds it.

Open the UiFlow2 at uiflow2.m5stack.com in a Chrome compatible browser. Set up a new project, and select the correct hardware that you are using. This will ensure that any project will have the correct I/O pin definitions and make hardware-dependent software modules available. This example has been demonstrated on an M5Stack Core S3 device.

Start in the UI editor, not the block workspace. Use the M5UI mode, which is based on LVGL widgets and code. Drag a Label and two Buttons widgets onto the canvas; they become label0, button0, button1. Then add a second page for a settings screen, add another Button (button2) to allow page navigation back from page1. Existing labels can be renamed, and additional explanatory labels can be added for clarity.

Switch back to the block workspace, and wire the events. In this simple example, we have several event handlers that manage the behaviour and actions of the button widgets displayed on the touchscreen.

  1. The When button was pressed block can be dragged onto the workspace from the M5UI>Button list. Define this as a default (white text/blue coloured) Button0, and add a Set background color block for when pressed. A Set label text block is used to change the text of label0 to "pressed".

  2. Similarly, a When button was released block is used to reset the label0 text to "released" after lift off from the touchscreen.

  3. Page navigation can be handled by attaching a Page load block to a When button was pressed block for button1. This can be set to load page1. Pressing the button on the touchscreen will switch the screen view to the new display.

  4. In order to return to the first screen, another Page load block is attached to a When button was pressed block for button2 on page1. This is set to load page0.

  5. The minimal Setup simply initialises the built-in hardware and loads page0. These will be added by default as the project is set up. The Loop section maintains checking of the event handlers.

What to try next:

  • Add a Slider and use its VALUE_CHANGED event to drive label0.set_text(str(slider0.get_value())).

  • Try using feeding Slider values to an onscreen Arc or Bar to generate a graphical display.

  • Use a Chart widget to display data values over time. The values can be generated interactively or by a mathematical function.

Example 1: Building an interactive touchscreen
Example 2: Displaying sensor values

The Core S3 device has a built-in light sensor and the UiFlow2 interface provides a simple way to access the values for incident light in Lux units. It is used as a source of real-time data in this example. Most M5Stack devices have in-built sensors which can substitute. For example, the CoreS3 and Tab5 contain an IMU (inertial measurement unit) where 3 dimensional values can be read from accelerometer, gyroscope or magnetic compass. Alternatively, M5Stack can provide many external devices (Units) or stackable modules with different functions, which should be simply accessible from inside UiFlow2.

In this demonstration, we want to (i) grab quantitative data from a sensor, (ii) display its value on some of the graphical widgets that are provided in UiFlow2, and (iii) plot changing values over time. UiFlow2 allows one to graphically compose suitable screens and widgets for displaying the data, and form navigation between screens.

User interface

The first steps are to open a new project, select the correct hardware that you want to use, and open the user interface editor.

Page0 has several display elements dragged into place. Label0 is a text element that can contain an integer corresponding to the measurement of light intensity falling on the sensor (positioned on the front, lower-right face of the Core S3). Bar0 is a dynamic sliding indicator bar that can be scaled to accept and display an artitrary value. A scale was placed under bar0. Similarly, arc0 is a widget that provides a simple gauge-like display of values fed to it. A smaller text label1 is positioned inside the arc0. Page0 also has a button positioned on the lower right past of the screen to allow navigation to a second page1, which contains a plotting display.

Page1 contains a chart0 widget. The chart widgets are powerful elements that can be configured in different ways for plotting time dependent data. It is highly recommended that you read the guide pages for this very flexible widget. Many parameters for chart0 can be set up by appropriate menu choices in the UI editor, but the parameters are mostly accessible programmatically also. Chart0 was set up to display up to 50 data points, with a range of 0 to 1000, and X and Y axes and dividers set up accordingly. The editor was then closed, and Blockly elements used to tie the elements together. The UiFlow2 environment allows the programmer to switch back and forth between these modes - so adjustments can be made interactively, while testing the program and interface on a test device attached to the computer by USB cable.

Blockly code

The Blockly code includes default init blocks to initialise the hardware and load page0 onto the screen. We want to collect sensor values over time and display these in real time. There are some sampling issues to consider. (i) some sensors require some internal processing to produce a reading, and are not polled continuously. While this is not the case with light sensor, we want to plot values over time on a chart with discrete time points, so a sampling rate needs to be chosen. One can sample a sensor, and sleep for a suitable time between samplings, however this will also affect other microcontroller activities - such as checking for button presses and LVGL animation. Instead, a special microsecond/millisecond timer is used to measure time gaps in "ticks" used by the processor. A get ticks in milliseconds block is used to save the current internal time to a variable start at the beginning of each sensor read loop. The tick diff block is used to measure the difference in milliseconds between the current time and the saved start time. When this value exceeds an arbitrary value (1000 milliseconds in this example), the next read/white events are triggered. The ticks_diff costs one comparison per pass and keeps the UI responsive. Sleep should only be used in a program that doesn't require user interaction, or where the delays are much longer. Again the UiFlow2 guide has useful information on the use of these and other time related blocks.

Each read cycle is triggered about 1 sec apart and this series of events follows. (i) the get lux block is connected to set arc0. (ii) the get lux block is connected to set bar0. (iii) The current lux value is also connected to set label0 and set label1 to update the text displays on page0. (iv) The lux value is also fed to chart0 (see below). Finally, the start variable is updated with the current machine time with get ticks in milliseconds, to start the next count.

The chart0 widget is defined in the setup with the series_lux. The data points are defined as red coloured, with zero dimension data points, i.e. displayed as lines. Data is displayed as a shifting display - i.e. feeding from the display in scroll-like fashion, rather than as a circular display, overwriting old screen info. There a lots of opportunities for customising the chart display, and the reference material is helpful for this.

When the program is downloaded onto the device, it will immediately display the ambient light levels on the page0 widgets, and on chart0 on page1. A light source like torch can be used to vary the sensor values. The software demo has set maximum values of 1000 lux for the various displays, and bright lights or sunlight will exceed this, clipping the data displays and sending a notice about exceeding maximum values via the program monitor. Of course the maximum lux value can be simply changed in software. There are many other modifications that can be explored. Some ideas are: (i) Adding audible alerts for over or under value conditions. (ii) Autoranging displays, as the widgets can be adjusted programatically according in incoming sensor values. (iii) Plotting multiple variables on the same chart.

Example 3: data logging

The one that makes the touchscreen worth having: a live strip chart plus a CSV on the SD card. This follows M5Stack's own charting example, using an ENV unit for temperature and humidity, with logging added.

  • Where the file goes. /sd/… needs a card that the firmware has mounted; if it isn't there, writes raise OSError. Internal flash (/flash/…) always works but is small and wears out — fine for a few thousand rows, not for continuous logging. Add the SD card block in Setup and check the mount before you trust it.

  • Open and close per write. Slower, but a card pulled mid-session loses one row instead of the file. For fast logging, keep the handle open and flush() every N rows instead.

  • Timestamps. time.time() gives epoch seconds, correct only once SNTP has synced — which is why the timezone and NTP fields at flash time matter. Log the raw value and convert on the desktop.

  • The chart is a display, not the record. UPDATE_MODE.SHIFT with point_num=10keeps ten points visible and discards the rest. The CSV is the data.

  • That time.sleep(1). It's in M5Stack's own example and it's fine here because nothing is interactive. Add a button and it has to become the ticks_diff pattern from Project 2.

Example 4: Wireless communications

Wi-Fi credentials were baked in by M5Burner, so the network is usually up by the time setup() runs. Everything network-related lives under Advanced.

HTTP request

The requests2 module is UiFlow2's HTTP client. get() and post() return a response with .text and .json().

AN HTTP CALL BLOCKS

A request in loop() stalls everything until the server answers or the socket times out — on a bad network that's seconds of frozen screen. Call it from a button event, or gate it behind a ticks_diff check so it happens once a minute rather than every pass, and wrap it in try/except so one failed request doesn't kill the program.

MQTT is the better fit for a device that should keep reporting. The client is umqtt.MQTTClient; the blocks generate exactly this.

  • check_msg() returns immediately and must be called every pass, or subscriptions look dead.

  • Client IDs must be unique per broker — two devices sharing one will kick each other off in a loop.

  • set_last_will() has to be called before connect(); it's how a dashboard notices the board dropped off.

  • keepalive of 0 means no keepalive at all. Set something like 60 for anything left running.

Program design for different screens

A Tab5 has roughly twelve times the pixel area of a CoreS3.

  • Don't port coordinates. A layout built at 320 × 240 lands in the top-left sixth of a Tab5. Rebuild the layout in the UI editor with the Tab5 controller selected, or anchor everything with align_to() instead of absolute x/y.

  • Fonts scale badly. lv.font_montserrat_14 is readable on a CoreS3 and tiny on a Tab5. Step up to a larger montserrat size in the widget's font property for the Tab5 build.

  • Touch targets. A 40 px button is a comfortable finger target on CoreS3 and a fingernail on Tab5 — size to physical millimetres, not pixels.

  • Rotation. Widgets.setRotation(1) in setup() sets landscape; set it before building the UI so widget coordinates match what you see.

  • Redraw cost. Full-screen animation at 1280 × 720 is real work even for a P4. Update the region that changed — set a label's text rather than repainting a page.

KEEP ONE PROJECT PER BOARD

It's tempting to keep a single UiFlow2 project and switch controllers. In practice the UI editor coordinates, the font sizes and the unit pin assignments all differ, so you end up re-editing every time. Save two projects with the same logic and different layouts, or move the logic into a module you upload through the file manager and import from both.

UiFlow2 tips

Reference

  • UIFlow2 Programming Guide — the API reference. Every M5UI widget (28 of them: M5Page, M5Label, M5Button, M5Switch, M5Slider, M5Chart, M5List, M5TabView, M5Keyboard, M5Msgbox and the rest) has a page with a complete runnable example. The hardware section covers Imu, Touch, Mic, Speaker, ADC, Pin, I2C, UART, IR and more; the software section covers requests2, umqtt and JSON.

  • uiflow-micropython on GitHub — firmware source and releases. Useful for reading what changed before updating a working device.

  • IDE layout reference — what each region of the workspace does.

  • M5Stack community forum — where board-specific quirks get answered, often faster than the docs get updated.

Sources

Micropython framework

Every UiFlow2 program, however you assemble it, comes out in the same shape. Knowing it makes the generated Micropython readable. There are four most common sources of problems:

  • M5.update() is what reads the touch panel. If it stops being called — you blocked the loop with a long sleep, or wrote your own while inside it — touch dies even though the screen still looks fine.

  • Widgets are LVGL objects. M5UI wraps LVGL, so anything in the LVGL API is available on a widget: set_text(), align_to(), set_style_size(), states like lv.STATE.CHECKED.

  • A page is a screen. Widgets are parented to an M5Page; calling page.screen_load() makes that page the visible screen. Multiple pages plus screen_load() is the whole of navigation.

  • Events are dispatched, not bound one-to-one. Blocks register a single handler for lv.EVENT.ALL and branch on event_struct.code. Adding a third event to a widget adds a branch, not a second registration.

biomaker.org

Home

Hardware

Software

Projects

Contact Us

Jim Haseloff

Cambridge, England

New York, USA

@jimhaseloff.bsky.social