SemiBlock ESP32 (VS Code extension)
"Visual Basic for ESP32". High-school students drag controls onto a form, double-click a control to jump into its
Python event handler, write their own MicroPython, and press Run to see the form on the board's touch screen.
Supported board: Waveshare ESP32-S3-Touch-LCD-3.5B (AXS15231B, 320×480 portrait or 480×320 landscape) only.
More boards will be added after this one is tested. Plan: skill/1. plan.md in the repository.
API key (required)
SemiBlock ESP32 needs a free API key, so we know who is using it and so each user has agreed to the consent form.
- On build.semiblock.ai, open the VSCode ESP32 module, fill in the registration form (name, gender, age range,
country, company/school), read and agree to the consent form, and press Agree and get my API key.
- Press Copy. The full key (
sbk_ + 40 characters) is shown once; the website keeps only a hash.
- In VS Code run SemiBlock ESP32: Enter API Key and paste it. (If the key is on your clipboard it is pre-filled.)
The extension checks it with the server and keeps it in VS Code's secret storage.
Until a valid key is in place the form designer shows a lock screen, and New Project, Run, Install on board,
Serial monitor and Flash firmware ask for the key first. The key is checked at most once a day; if the school's internet is
down, a key that was verified within the last 7 days keeps working. A revoked key stops working at the next check
(at the latest a few hours after it was revoked, or right away on the next Run after a day of caching).
Manage keys (generate a new one, revoke) on the same website page. Other commands: Get an API Key (opens the page),
Remove API Key. Setting semiblockEsp32.serverUrl points at another SemiBlock server (must be https://; http:// only for localhost).
This is registration and consent, not copy protection: someone who edits the extension's code can skip the check.
Quick start (student)
- Firmware (once per board): run
SemiBlock ESP32: Flash MicroPython + LVGL Firmware and pick
lvgl_micropy_ESP32_GENERIC_S3-SPIRAM_OCT-16.bin (in waveshare-3.5-touch/burn_firmware_new_hardware/).
SemiBlock ESP32: New Project → choose a folder and a name. The Hello form opens in the designer.
- Drag controls from the toolbox. Select one to edit its properties on the right.
- Double-click a Button →
form1.py opens with def btn1_click(e): ready for your code.
- Press ▶ Run (or
Cmd/Ctrl+Alt+R). The files are uploaded and the form appears on the board. print() output shows in
the SemiBlock ESP32 output panel.
# form1.py
def btn1_click(e):
ui.lbl1.set_text("Hello!") # every control is ui.<name>; a Button's text is ui.<name>_label
Portrait or landscape
Pick Orientation in the designer toolbar (or in the form's properties: click the form background). Portrait is
320×480, landscape is 480×320. Switching moves any control that would fall off the screen back inside it (Undo restores them).
The choice is stored in Form1.sbform and applied on the board when the form starts.
How landscape works: the panel only draws correctly in its native portrait mode (changing the display rotation garbles
it), so the panel is always driven in portrait. For a landscape or flipped form, LVGL renders a normal 480×320 frame and the
runtime rotates it into the panel's 320×480 frame as it flushes (lv.draw_sw_rotate, fast C code); touches are mapped back by
the pointer driver. Measured on a 3.5B: a full repaint takes about 51 ms in portrait and about 90 ms rotated (an LVGL
transform version took 460 ms and made taps sluggish). Rotated forms use one LVGL draw buffer instead of two.
The panel garbles on any partial flush, so the runtime never lets LVGL refresh by itself (its refresh timer has an endless
period) and repaints the whole screen each loop. If your own code calls lv.refr_now(), the screen may flash once.
Direction and touch mapping are checked numerically on the board, but not yet by eye — report it if it looks wrong.
Flip 180° (toolbar checkbox, or the form's properties) turns the screen upside down on the board, for either orientation:
use it when the board is mounted the other way up, or when landscape comes out upside down. The designer itself always
stays upright; only the board output is flipped. It uses the same rotation mechanism as landscape.
Pictures (Image control)
Drag an Image onto the form, then in its properties press Choose… and pick a png, jpg or bmp. The picture is
copied into the project's images/ folder, the control takes the picture's size (scaled down to fit the screen the first
time), and it is stretched to the control's size when you resize it. Run uploads the pictures along with your code.
- Keep pictures about the size they have on screen: they are decoded and scaled once, when the form starts (about
0.3–0.9 s each), then drawn from RAM. A picture that is far bigger than needed uploads and loads slowly (the Run output
warns above 500 KB).
png keeps transparency; jpg and bmp use half the memory.
- Why not point
lv.image at the file? Without an image cache that decodes the picture on every frame, and this runtime
repaints the whole screen every frame: measured 225 ms (small PNG) to 1.5 s (full-screen JPG) per frame instead of ~10 ms.
- Change a picture from your code: put the other picture in the project's
images/ folder (everything there is uploaded
on Run) and call sb.set_image(ui.img1, "other.png"). Pictures are stored flat on the board, so two different files
with the same file name are not allowed.
- Double-click an Image to write its
img1_click(e) handler (tap a picture like a button).
Font sizes
The Font size list offers 12 to 24. The firmware from the Flash MicroPython + LVGL Firmware step only contains Montserrat 12, 14 and 16,
so on the board 18 and up are shown as 16 until you build the firmware with the larger fonts: in lvgl_micropython's lib/lv_conf.h set
LV_FONT_MONTSERRAT_18, _20, _22, _24 (each size you want) to 1, rebuild and flash again. Sizes that the firmware lacks use the nearest smaller one that exists.
A project can have as many forms as you like (a home screen, a settings screen, ...). Press the + next to the form list
under the canvas (or run SemiBlock ESP32: Add Form, or the + in the Project view) and type a name such as Settings.
That creates Settings.sbform (opens in its own designer tab) and settings.py for its code. The list under the canvas
switches between the forms. The first form (entry in sbproject.json, Form1 by default) is the one that Run shows first.
Switch forms from your code with sb.show_form("<form name>"):
import sb_runtime as sb # already at the top of new form files
def btnSettings_click(e): # in form1.py
sb.show_form("Settings")
def btnBack_click(e): # in settings.py
sb.show_form("Form1")
- A form is built the first time it is shown. Until then its
ui is None, so show a form once before reading its
controls from another form's code (import settings; settings.ui.lbl1.set_text("hi")).
form_load() runs once, when the form is first shown. form_show() (write it yourself, no designer entry) runs every time
the form is shown, which is the place to refresh its texts.
- Forms keep their state (text, slider position, ...) while another form is on screen. Timers keep ticking too; stop a
form's timers in your code if they should only run while it is visible.
- Every form has its own orientation, so a portrait home screen can open a landscape form.
sb.current_form() returns the name of the form on screen.
- Rename a form with the pencil next to the form list, SemiBlock ESP32: Rename Form, or right-click a
.sbform in the Project view. It renames <Name>.sbform, <name>.py and <name>_ui.py, the form's name inside the file, the start form in sbproject.json, main.py, and updates sb.show_form("..."), from <name>_ui import <Name>UI, import <name> and <name>.ui... in your Python files. Check the result with Undo-friendly tools such as git: other uses of the old name (for example in strings you build yourself) are not changed.
- Form names are letters, digits and
_, must start with a letter, must not end with _ui and must not be a Python module the board needs (main, time, machine, ...).
main.py is written by SemiBlock ESP32 and lists all forms on every Run. If you edit it yourself, add new forms by hand
(sb_runtime.add_form(...)), or delete it to have it written again.
Code completion
Type ui. and VS Code lists your controls; type ui.lbl1. and it lists what a label can do (set_text, set_style_text_color, ...).
This works because the extension keeps editor-only stubs in the project's typings/ folder (lvgl.pyi, sb_runtime.pyi).
They are created with the project, refreshed whenever VS Code opens it, and never uploaded to the board. It needs the
Python extension with Pylance and the project folder opened as the VS Code workspace (what New Project does).
To read which option a Dropdown has chosen as text, use sb.selected_text(...)
(LVGL's own get_selected_str() needs a buffer and returns None):
import sb_runtime as sb # already at the top of new form files
def dd1_value_changed(e):
ui.lbl1.set_text(sb.selected_text(ui.dd1))
The designer
Left: Components (drag a tile onto the form, or click it; press / to search) and Hierarchy (every control, click to select).
Centre: the form on the board's screen, with the Run/Stop bar above and zoom, grid and Fit to window. Right: Properties
(General, Layout, Appearance, Behavior, Events), Events (one card per event, press </> to open its handler in Python) and
Code (the generated Python for the selected control, read-only). Arrow keys nudge a control (Shift = 10 px), Delete removes it,
Ctrl/Cmd+D duplicates it, and double-clicking a control opens its default event.
Files in a project
| File |
Who edits it |
Purpose |
Form1.sbform |
designer |
layout (JSON, source of truth) |
form1.py |
student |
event handlers + own functions |
form1_ui.py |
generated, read-only |
builds the LVGL widgets; rewritten on save/Run |
Settings.sbform, settings.py, ... |
designer / student |
one pair (plus a generated _ui.py) per extra form |
main.py |
generated |
registers the forms and starts the first one |
sbproject.json |
extension |
board and entry form |
typings/*.pyi |
extension, read-only |
editor completions only (see above) |
Handler names are <control name>_<event>, e.g. btn1_click, sld1_value_changed, tmr1_tick() (no argument).
Renaming a control in the designer renames its handlers. Extra *.py files in the project folder are uploaded too.
Requirements
- VS Code ≥ 1.90, Python 3 with mpremote (
pip install mpremote) and, for flashing, esptool (pip install esptool).
- Settings:
semiblockEsp32.port (empty = auto-detect), mpremotePath, pythonPath, firmwarePath, resetBeforeRun, serverUrl.
How it talks to the board
Same recipe as the Makefiles in waveshare-3.5-touch: mpremote reset, wait 2 s, mpremote fs cp … + run main.py.
runtime/waveshare-3.5b/sb_runtime.py is uploaded on every Run; it contains the display/touch bring-up taken from the working
flippybird_new_hardware / clock_swap_new_hardware examples. Panel quirks it respects: full-width strips only, LVGL refresh
timer paused, whole screen repainted every loop, builtins.lv = lv for the touch driver.
Develop
npm install
npm test # compile + unit tests (generated Python is checked with python3)
# F5 in VS Code → "Run Extension"
npm run package # → semiblock-esp32-<version>.vsix
code --install-extension semiblock-esp32-0.3.1.vsix
stubs/lvgl.pyi is generated from the lvgl_micropython build: python3 scripts/update-lvgl-stub.py after rebuilding the firmware.
stubs/sb_runtime.pyi is written by hand; the tests fail if it falls out of sync with sb_runtime.py.
Layout: src/codegen (pure, tested), src/designer (custom editor host), media/designer.{js,css} (webview),
src/device (mpremote), src/project, src/views, runtime/<board>/ (Python copied to the board).
Adding a board = a profile in src/codegen/board.ts + a runtime/<board>/sb_runtime.py.