Skip to content

Lopaka Basics 09: Beyond Arduino ​

Five devices showing the same plant monitor screen with a pot, BASIL, MOISTURE, a bar and WATER ME!, an Arduino Uno, an ESP32 with ESPHome, a Pico with MicroPython, a Pico with CircuitPython and a Flipper Zero

C++. YAML. Python. More Python. And C for a Flipper Zero. Five different programs draw the same screen, and Lopaka wrote the drawing code for every one of them.

In this episode you will find out why boards run code in such different languages, and what separates compiled firmware from a Python interpreter. Then you will draw a small plant monitor in Lopaka for MicroPython, build the same screen for CircuitPython, ESPHome and Flipper Zero, and run three of them on simulated boards.

What you will build: a 128×64 plant monitor screen: a pot with three leaves, the plant's name, a moisture bar and a reminder to water it, on four platforms. What you need: a browser and the online simulator from episode 1. Boards are optional: a Raspberry Pi Pico or an ESP32 with a 128×64 SSD1306 OLED, or a Flipper Zero.

Two ways to run code ​

Compiled: the whole firmware, every time ​

Arduino code is compiled. A compiler turns your sketch into firmware, raw machine instructions for the chip, and you flash the whole firmware onto the board. Change one line, and you compile and flash everything again.

ESPHome works the same way: your YAML file becomes C++, and the C++ becomes firmware. A Flipper Zero app is compiled too.

Interpreted: a text file on the board ​

MicroPython and CircuitPython flip this around. You flash an interpreter onto the board once. After that, your program is a plain text file on the board, such as main.py, and the interpreter reads it line by line. Change a line, save the file, and it runs again, with no compiler in between.

The price is speed and memory: the interpreter itself runs on the chip and takes its share. On small boards, keep the drawing simple.

Compiled versus interpreted: on top, a sketch goes through a compiler into firmware that is flashed again for every change, for Arduino, ESPHome and Flipper apps; below, an interpreter is flashed once and runs main.py on the display, saved and run again, at the cost of speed and memory

Step 1: pick a platform ​

In Lopaka, the platform is the first thing you choose in a new project. Click + New Project and open the Platform list. Next to each platform's name is a badge with the language of its code: C++, YAML, Python or C.

The Platform list in the Create New Project dialog: ESPHome with the YAML badge, Micropython and CircuitPython with the Python badge, and Flipper Zero, One with the C badge

Start with Micropython. Keep the screen size at 128×64, give the project a title, "Plant Monitor", and click Save.

Step 2: draw the screen ​

The design is a tiny plant monitor. It uses only rectangles, circles, lines and text, so every platform in this episode can draw it.

Draw it with the Rectangle, ellipse, line and text (Aa) tools, and type the exact values into the inspector:

  • The pot: two filled rectangles, the rim at X 8, Y 42, W 26, H 5, and the pot below it at X 11, Y 47, W 20, H 15. Turn on Filled for both.
  • The stem: a line from 21, 24 to 21, 41.
  • The leaves: three ellipses with Filled on and both radii equal: left at X 9, Y 26 with radius 5, right at X 23, Y 22 with radius 5, and top at X 15, Y 11 with radius 6.
  • The name: the text BASIL at X 44, Y 11, and a line under it from 44, 13 to 123, 13.
  • The moisture bar: the text MOISTURE at X 44, Y 27, a rectangle at X 44, Y 30, W 80, H 10, and a filled rectangle inside it at X 46, Y 32, W 21, H 6: the water level.
  • The reminder: the text WATER ME! at X 44, Y 57.

MicroPython has one font, 8×8 pixels, so the text layers have no font list.

Finally, give everything names. Double-click Screen 1 in the layers panel and rename it to Plant, then name the layers rim, pot, stem, leaf_left, leaf_right, leaf_top, title, rule, caption, bar, level and hint. The screen name becomes the name of the function, and the layer names become comments in the code.

The plant monitor in Lopaka on a MicroPython project: the bar layer is selected with X 44, Y 30, W 80 and H 10 in the inspector, and the code panel highlights display.rect(44, 30, 80, 10, 1)

Step 3: your code ​

One call per layer ​

The code is plain Python. Every layer becomes one call on the display object: fill_rect, rect, line, ellipse and text. At the end, display.show() sends the finished picture to the screen.

Lopaka's generated code sits between the BEGIN and END markers. A few lines of your own go around it: four above the markers set up the display, and one below calls the function. Type them straight into Lopaka's code panel. Lopaka keeps your lines outside the markers, and Copy takes the whole file:

python
from machine import Pin, I2C
from ssd1306 import SSD1306_I2C

i2c = I2C(0, scl=Pin(5), sda=Pin(4))
display = SSD1306_I2C(128, 64, i2c)

# [BEGIN lopaka generated]
# SSD1306 OLED display library: https://github.com/PerfecXX/MicroPython-SSD1306/blob/main/ssd1306.py

def drawPlant(display):
    # rim
    display.fill_rect(8, 42, 26, 5, 1)


    # pot
    display.fill_rect(11, 47, 20, 15, 1)


    # stem
    display.line(21, 24, 21, 41, 1)


    # leaf_left
    display.ellipse(14, 31, 5, 5, 1, True)


    # leaf_right
    display.ellipse(28, 27, 5, 5, 1, True)


    # leaf_top
    display.ellipse(21, 17, 6, 6, 1, True)


    # title
    display.text("BASIL", 44, 2, 1)


    # rule
    display.line(44, 13, 123, 13, 1)


    # caption
    display.text("MOISTURE", 44, 18, 1)


    # bar
    display.rect(44, 30, 80, 10, 1)


    # level
    display.fill_rect(46, 32, 21, 6, 1)


    # hint
    display.text("WATER ME!", 44, 48, 1)


    display.show()
# [END lopaka generated]
drawPlant(display)

Your four lines create an I2C bus on the Pico's pins GP5 (SCL) and GP4 (SDA) and a 128×64 display object on it. The last argument of most calls, 1, is the colour: pixel on.

The numbers that changed ​

Two kinds of numbers in this code differ from the inspector:

  • The leaves. In the inspector, the left leaf has X 9, Y 26 and radius 5: the top-left corner of the box around it. ellipse() takes the centre, so the code says ellipse(14, 31, 5, 5, ...). The True at the end fills it.
  • The text. You placed BASIL at Y 11, and the code says text("BASIL", 44, 2, ...). MicroPython's text() takes the top-left corner of the text, so Lopaka converts the position for you.

Step 4: run it in the simulator ​

The simulator runs real MicroPython on a simulated Raspberry Pi Pico:

  1. Create a new Raspberry Pi Pico project with MicroPython.
  2. Add an SSD1306 OLED display and connect four wires: GND to GND, VCC to 3V3, SCL to GP5 and SDA to GP4.
  3. Add a second file, ssd1306.py, with the display driver from the link in the comment of the generated code. Your first line imports SSD1306_I2C from it. Without this file the program stops at import.
  4. Paste the copied code into main.py and press Play.

The plant appears on the display. There is no compile step: the interpreter runs the file as it is.

On a real Pico, copy both files, main.py and ssd1306.py, onto the board. The board runs main.py every time it starts.

Step 5: a CircuitPython project ​

Now the same screen in CircuitPython. A project keeps the platform you chose, so the new platform needs a new project: + New Project, platform CircuitPython, size 128×64.

Place the same layers with the same numbers, and give the screen and the layers the same names. Two things look different in the inspector:

  • A circle has one radius, R, instead of two.
  • Text uses Terminus, the font built into CircuitPython. Its Size field multiplies the font: 1 is the normal size.

The plant monitor in Lopaka on a CircuitPython project: the title layer BASIL is selected at X 44, Y 11 with Size 1, and the code shows title = label.Label(terminalio.FONT, text="BASIL") with title.y = 6

Step 6: objects in a group ​

The code ​

The code looks different. Every shape is an object: a Rect, a Circle, a Line or a Label for text. Each object is added to a group, and the display shows that group:

python
import board, busio, displayio, terminalio
import adafruit_displayio_ssd1306
from adafruit_display_text import label
from adafruit_display_shapes.rect import Rect
from adafruit_display_shapes.circle import Circle
from adafruit_display_shapes.line import Line

displayio.release_displays()
i2c = busio.I2C(board.GP5, board.GP4)
bus = displayio.I2CDisplay(i2c, device_address=0x3C)
display = adafruit_displayio_ssd1306.SSD1306(bus, width=128, height=64)

# [BEGIN lopaka generated]
# Init display
Plant = displayio.Group()
display.root_group = Plant

def drawPlant():
    # rim
    rim = Rect(8, 42, 26, 5, fill=0xFFFFFF, outline=0xFFFFFF)
    Plant.append(rim)
    # pot
    pot = Rect(11, 47, 20, 15, fill=0xFFFFFF, outline=0xFFFFFF)
    Plant.append(pot)
    # stem
    stem = Line(21, 24, 21, 41, color=0xFFFFFF)
    Plant.append(stem)
    # leaf_left
    leaf_left = Circle(14, 31, 5, fill=0xFFFFFF, outline=0xFFFFFF)
    Plant.append(leaf_left)
    # leaf_right
    leaf_right = Circle(28, 27, 5, fill=0xFFFFFF, outline=0xFFFFFF)
    Plant.append(leaf_right)
    # leaf_top
    leaf_top = Circle(21, 17, 6, fill=0xFFFFFF, outline=0xFFFFFF)
    Plant.append(leaf_top)
    # title
    title = label.Label(terminalio.FONT, text="BASIL", color=0xFFFFFF)
    title.x = 44
    title.y = 6
    Plant.append(title)
    # rule
    rule = Line(44, 13, 123, 13, color=0xFFFFFF)
    Plant.append(rule)
    # caption
    caption = label.Label(terminalio.FONT, text="MOISTURE", color=0xFFFFFF)
    caption.x = 44
    caption.y = 22
    Plant.append(caption)
    # bar
    bar = Rect(44, 30, 80, 10, outline=0xFFFFFF)
    Plant.append(bar)
    # level
    level = Rect(46, 32, 21, 6, fill=0xFFFFFF, outline=0xFFFFFF)
    Plant.append(level)
    # hint
    hint = label.Label(terminalio.FONT, text="WATER ME!", color=0xFFFFFF)
    hint.x = 44
    hint.y = 52
    Plant.append(hint)
# [END lopaka generated]
drawPlant()
while True:
    pass

The eleven lines above the markers are yours: the imports for the shapes and the text label, and the display setup on the same pins, GP5 and GP4, at the usual I2C address of these modules, 0x3C. displayio.release_displays() frees the display in case an earlier run still holds it. Below the markers, drawPlant() builds the group, and while True: pass keeps the program running. When a CircuitPython program ends, the board takes the display back for its own text output, and the plant would disappear.

The display keeps showing the group ​

display.root_group = Plant tells the display which group to show. From then on the display redraws that group by itself. Your code never sends a frame: if you move an object or change the text of a label, the screen follows.

CircuitPython objects in a group: Plant = Group() holds Rect objects for the rim, pot and bar, Circle objects for the leaves and Label objects for BASIL and the other texts, and display.root_group shows the group on the screen

Colours are 24-bit hex values: 0xFFFFFF is white, a lit pixel on this display. And title.y = 6 is the middle of the text line: a Label is placed by its left edge and its vertical centre, so Lopaka converts the Y 11 from the inspector.

Run it in the simulator ​

Create a new Raspberry Pi Pico project with CircuitPython and wire the display as in step 4. Add a file requirements.txt with the three Adafruit display libraries the code imports:

adafruit_displayio_ssd1306
adafruit_display_text
adafruit_display_shapes

Paste the code into code.py and press Play. The libraries load when the simulation starts, and the same plant appears. On a real board, copy these three libraries into the lib folder of the CIRCUITPY drive.

Step 7: an ESPHome project ​

ESPHome is made for smart home devices: you describe the device in a YAML file, and it appears in Home Assistant. Create a new project with the platform ESPHome, size 128×64, and place the same layers with the same numbers.

The text works differently here. ESPHome uses TrueType fonts in any size, and the Font list offers Courier Prime, Fira Code, Merriweather, Montserrat, Roboto, Roboto Mono and Ubuntu. Select BASIL, pick Roboto and set Size to 12. The next text you add keeps Roboto but starts at size 14, so set 12 on MOISTURE and WATER ME! too. Every font and size you use becomes a separate entry in the code, and the device stores each one.

In ESPHome, the Y of a text is the bottom of its text box, and Roboto 12 makes a box 14 pixels tall. At Y 11, the top of BASIL would stick out above the screen. Move the three texts a little lower: Y 14, 30 and 59.

The plant monitor in Lopaka on an ESPHome project: the title layer BASIL in Roboto, Size 12, at X 44 and Y 14; the code panel starts with a font section, file gfonts://Roboto, id Roboto, size 12

Step 8: a font and a lambda ​

What Lopaka writes ​

In ESPHome, the design becomes two pieces of YAML: a font: section that pulls the font from Google Fonts, and a lambda, a few lines of C++ that live inside the YAML file:

yaml
# [BEGIN lopaka generated]
font:
    - file: 'gfonts://Roboto'
      id: Roboto
      size: 12

display:
    lambda: |-
        // rim
        it.filled_rectangle(8, 42, 26, 5);


        // pot
        it.filled_rectangle(11, 47, 20, 15);


        // stem
        it.line(21, 24, 21, 41);


        // leaf_left
        it.filled_circle(14, 31, 5);


        // leaf_right
        it.filled_circle(28, 27, 5);


        // leaf_top
        it.filled_circle(21, 17, 6);


        // title
        it.print(44, 0, id(Roboto), "BASIL");


        // rule
        it.line(44, 13, 123, 13);


        // caption
        it.print(44, 16, id(Roboto), "MOISTURE");


        // bar
        it.rectangle(44, 30, 80, 10);


        // level
        it.filled_rectangle(46, 32, 21, 6);


        // hint
        it.print(44, 45, id(Roboto), "WATER ME!");

# [END lopaka generated]

it is the display. filled_circle takes the centre and the radius, like MicroPython's ellipse. print(44, 0, ...) takes the top-left corner of the text box: the box of BASIL runs from row 0 to row 14, and the inspector shows its bottom, 14.

Put it in your device's config ​

The rest of the YAML file describes your device, and you write it yourself: the board, the I2C pins and the display. Put Lopaka's font: section at the top level and its lambda: under your display, with the indentation as it is:

yaml
esphome:
    name: plant-monitor

esp32:
    board: esp32dev
    framework:
        type: esp-idf

i2c:
    sda: 21
    scl: 22

font:
    - file: 'gfonts://Roboto'
      id: Roboto
      size: 12

display:
    - platform: ssd1306_i2c
      model: 'SSD1306 128x64'
      address: 0x3C
      lambda: |-
          // rim
          it.filled_rectangle(8, 42, 26, 5);
          // ...the rest of the lambda from Lopaka

Copy the whole lambda, from // rim to the last it.print. The only line Lopaka does not give you under display: is - platform: ssd1306_i2c with its model and address: that part tells ESPHome which display you have.

Step 9: build the firmware and run it ​

ESPHome is compiled. Save the file as plant.yaml and run esphome compile plant.yaml. ESPHome turns the whole file into C++, with your lambda inside it line for line, and compiles firmware for the ESP32.

The device YAML with the font section and the lambda highlighted, and an arrow to the src/main.cpp that ESPHome generates, where the same filled_rectangle, line, filled_circle and print lines sit inside set_writer

In the simulator, create an ESP32 project and connect the display: SDA to pin 21, SCL to pin 22, GND to GND and VCC to 3V3. Load the compiled firmware.factory.bin with the simulator's Upload Firmware and Start Simulation command. The plant is on the screen.

On a real ESP32, esphome run plant.yaml builds the same firmware and flashes it over USB. Because the drawing lives in a lambda, the same lines can print live sensor values later, for example the real moisture instead of a fixed bar.

Step 10: a Flipper Zero project ​

The Flipper Zero has its own screen, also 128×64 pixels, black on orange. Create a new project with the platform Flipper Zero, One. This platform has no default size: open Screen Size and pick Zero Horizontal (128×64).

Place the same layers with the same numbers. Lopaka knows the Flipper's four built-in fonts: FontPrimary, FontSecondary, FontKeyboard and FontBigNumbers. New text starts in FontPrimary, which is the font for all three texts here.

The plant monitor in Lopaka on a Flipper Zero project, black on orange: the title layer BASIL is selected and the font list shows FontPrimary, FontSecondary, FontKeyboard and FontBigNumbers

Step 11: C for the canvas ​

Lopaka writes C for the Flipper's canvas:

c

// [BEGIN lopaka generated]

void drawPlant(Canvas* canvas) {
    canvas_set_bitmap_mode(canvas, true);

    // rim
    canvas_draw_box(canvas, 8, 42, 26, 5);

    // pot
    canvas_draw_box(canvas, 11, 47, 20, 15);

    // stem
    canvas_draw_line(canvas, 21, 24, 21, 41);

    // leaf_left
    canvas_draw_disc(canvas, 14, 31, 5);

    // leaf_right
    canvas_draw_disc(canvas, 28, 27, 5);

    // leaf_top
    canvas_draw_disc(canvas, 21, 17, 6);

    // title
    canvas_set_font(canvas, FontPrimary);
    canvas_draw_str(canvas, 44, 11, "BASIL");
    // rule
    canvas_draw_line(canvas, 44, 13, 123, 13);

    // caption
    canvas_draw_str(canvas, 44, 27, "MOISTURE");
    // bar
    canvas_draw_frame(canvas, 44, 30, 80, 10);

    // level
    canvas_draw_box(canvas, 46, 32, 21, 6);

    // hint
    canvas_draw_str(canvas, 44, 57, "WATER ME!");
}
// [END lopaka generated]

canvas_draw_frame draws an outline, canvas_draw_box a filled rectangle and canvas_draw_disc a filled circle around its centre. The y of canvas_draw_str is the baseline, the same number as in the inspector, so the texts keep 11, 27 and 57. All three texts use FontPrimary, so canvas_set_font appears once.

This function goes into the draw callback of a Flipper app: the function the Flipper calls whenever it redraws your app's screen. You build the app with Flipper's own tools.

The draw callback of a Flipper app calling drawPlant(canvas), the function from Lopaka, next to a Flipper Zero showing the plant monitor black on orange; build it with the Flipper app SDK

Five platforms side by side ​

Side by side: an Arduino Uno, an ESP32 running ESPHome, a Pico with MicroPython, a Pico with CircuitPython, and the Flipper Zero version, ready for its app. The Arduino Uno runs the same design in a U8g2 project, set up as in episode 1, with the 6x13B font.

What changes between platforms ​

Same numbers, different words ​

Look at the moisture bar on all five platforms. Forty-four, thirty, eighty, ten: the numbers are the same everywhere. Only the words around them change: the name of the call, the 1 for the colour in MicroPython, the outline= in CircuitPython, the canvas in front on the Flipper.

The moisture bar in five languages: u8g2.drawFrame(44, 30, 80, 10) for Arduino with U8g2, it.rectangle(44, 30, 80, 10) for ESPHome, display.rect(44, 30, 80, 10, 1) for MicroPython, bar = Rect(44, 30, 80, 10, outline=0xFFFFFF) for CircuitPython and canvas_draw_frame(canvas, 44, 30, 80, 10) for Flipper Zero

Each platform has its own fonts ​

The fonts change too, because every platform has its own set: Petme 8×8 in MicroPython, Terminus in CircuitPython, Roboto 12 in ESPHome, FontPrimary on the Flipper Zero and 6x13B in U8g2. That is why the texts needed a different Y in ESPHome. Lopaka draws each project with the platform's own fonts, so the canvas shows the text you will get on the device.

The same plant monitor with five fonts: Petme 8×8 for MicroPython, Terminus for CircuitPython, Roboto 12 for ESPHome, FontPrimary for Flipper Zero and 6x13B for U8g2: same numbers, different words, each platform's own fonts

Recap ​

  1. Compiled or interpreted, the pixels stay the same: Arduino, ESPHome and Flipper apps are compiled into firmware; MicroPython and CircuitPython run a text file through an interpreter.
  2. Pick the platform when you create the project: the platform decides the language of the code, and each platform needs its own project.
  3. Same layers, new language: place the same layers with the same numbers, and Lopaka writes the code in that platform's language.

Next episode: E-paper & Shipping. Screens that keep their picture without power. Read episode 10.

Ready to try? Open Lopaka and take your screen to a new platform.