Go back

Raspberry Pi 4 and 5 in the browser: basic mode and Linux mode, step by step

A Raspberry Pi in Velxio is a board on the canvas that runs a Python script against the circuit you wire around it. There is nothing to compile. You write the script, press Run, and one of two engines executes it:

This article builds two projects, one per mode, and shows every click along the way. The first is an alarm station made of Grove modules on a Raspberry Pi 5: a motion sensor, a vibration switch and a DHT11 on the input side, a buzzer, two LEDs and a relay with a fan on the output side. The second is a sensor rig on a Raspberry Pi 4 with three I2C devices. Both scripts use the libraries that ship with Raspberry Pi OS, so they run unchanged on a real board.

Before you start, two things about access. Anyone can place a Raspberry Pi on the canvas and wire a circuit around it. Running it needs a signed-in account: every free account gets three trial sessions of 15 minutes on the Raspberry Pi family, in either mode, and any paid plan removes the limit. The details are in the What it costs section near the end.

Two ways to run the same script

Basic modeLinux mode
Where the script runsA Python interpreter in your browser tabAn Alpine Linux guest under QEMU on Velxio’s servers
Start timeA few secondsAbout a minute
GPIORPi.GPIO, gpiozero, Adafruit BlinkaThe same, plus /dev/gpiochip0 and the libgpiod tools
I2C, SPI, UART, 1-WireThe real smbus2, spidev, pyserial and w1thermsensor over emulated device filesThe same libraries over the kernel’s real device nodes
A shell and a file systemNoYes, in the Serial Monitor
Third-party packagesFrom a requirements.txtFrom a requirements.txt, resolved offline before the guest boots
Best forAlmost everythingShell tools, subprocesses, files, and anything a tutorial does from a terminal

The engine is chosen for you. A script that asks for something only Linux can do, such as subprocess, is sent to Linux mode and a chip in the toolbar tells you why. You can also force Linux mode yourself, which is what the second project does.

Project 1: a Grove alarm station on a Raspberry Pi 5

Grove modules are the fastest way to put sensors on a Pi: every module has the same four-pin socket, and in Velxio each one connects to the header with three wires, SIG, VCC and GND. This station watches a room with a PIR motion sensor and a vibration switch, reads the air with a DHT11, and answers through a buzzer, a green LED for “all quiet”, a blue LED for “alarm”, and a relay that runs a fan when the room gets warm.

Step 1: start a project on the Raspberry Pi 5

Open velxio.dev/editor. The Start a new project dialog opens on its own. Scroll to the Raspberry Pi section and click the Raspberry Pi 5 card. The Raspberry Pi 4B card works the same way: the two boards share the 40-pin header and the same libraries, so everything below applies to both.

The Start a new project dialog with the Raspberry Pi section; the Raspberry Pi 5 card is highlighted

Step 2: add the parts

Click the blue Add button at the right end of the toolbar. It opens the parts catalog.

The toolbar with the Add button highlighted

Type a name in the search field and click the card to drop the part on the canvas. The Grove modules are all listed under their Seeed names, so “Grove PIR” or “Grove relay” finds them. Add these eight parts, one at a time:

  1. Grove - PIR Motion Sensor
  2. Grove - Vibration Sensor (SW-420)
  3. Grove - Temperature & Humidity Sensor (DHT11)
  4. Grove - Piezo Buzzer
  5. Grove - Green LED
  6. Grove - Blue LED
  7. Grove - Relay
  8. DC Motor / Fan

The Add Component dialog with Grove PIR typed in the search field and the PIR motion sensor card highlighted

Parts can be dragged anywhere on the canvas. Leaving the Pi at the bottom and the modules above its header keeps the wires short.

Step 3: wire it

To draw a wire, click a pin, then click the pin at the other end. The pin names appear when you hover a part. Every Grove module takes the same three wires: SIG to a GPIO, VCC to 3V3, GND to GND. The relay is the exception on the supply side, its coil wants 5 V.

ModuleSIGVCCGND
PIR motion sensorGPIO173V3GND
Vibration sensor (SW-420)GPIO273V3GND
DHT11GPIO43V3GND
BuzzerGPIO243V3GND
Green LEDGPIO53V3GND
Blue LEDGPIO63V3GND
RelayGPIO235VGND

The fan has no signal wire of its own. It sits on the relay’s switched contacts, the way a load does on a bench: the supply goes into COM and comes out of NO only while the coil is energised.

FromTo
Raspberry Pi 5VRelay COM
Relay NOFan +
Fan -Raspberry Pi GND

The wired station: the Raspberry Pi 5 at the bottom, the PIR, vibration sensor, DHT11 and buzzer in a row above its header, the two LEDs and the relay with its fan below them, with call-outs on each module

Step 4: the script

The file explorer on the left already holds a file called script.py with a short blink script. Keep that name. Basic mode runs the first .py file it finds, but Linux mode runs script.py specifically, so a project whose main file keeps that name works in both.

The code editor with script.py selected in the file explorer

Replace its contents with this script:

"""Grove alarm station on a Raspberry Pi 5.

A PIR sensor and a vibration switch watch the room, a DHT11 reads the air,
and the actuators answer: a buzzer and a blue LED for the alarm, a green LED
while all is quiet, and a relay that runs a fan when the room gets warm.
Every module hangs off the header with a Grove cable, one signal pin each.

Wiring, BCM numbers (VCC and GND of each module to 3V3 and GND):
    PIR motion sensor   SIG GPIO17   HIGH while motion is seen
    vibration SW-420    SIG GPIO27   LOW while shaking, HIGH at rest
    DHT11               SIG GPIO4
    buzzer              SIG GPIO24
    green LED GPIO5, blue LED GPIO6
    relay               SIG GPIO23, VCC on 5V; the contacts switch the fan:
                        5V to COM, NO to the fan's +, the fan's - to GND
"""
import time

import adafruit_dht
import board
from gpiozero import LED, Button, Buzzer, MotionSensor

FAN_ON_AT = 28   # degrees C
HOLD = 2.5       # seconds the alarm light stays on after the beeps

pir = MotionSensor(17)
shaker = Button(27, pull_up=True)   # the SW-420 pulls SIG low while it vibrates
dht = adafruit_dht.DHT11(board.D4, use_pulseio=False)
buzzer = Buzzer(24)
quiet_led = LED(5)
alarm_led = LED(6)
fan = LED(23)                        # the relay coil: on() closes COM to NO


def alarm(reason, beeps, beep_time):
    print(f"ALARM: {reason}")
    quiet_led.off()
    alarm_led.on()
    buzzer.beep(on_time=beep_time, off_time=0.1, n=beeps, background=False)
    time.sleep(HOLD)
    alarm_led.off()
    quiet_led.on()


quiet_led.on()
motion_seen = False
shaking = False
next_reading = 0
print("Station armed")

while True:
    if pir.motion_detected and not motion_seen:
        alarm("motion in the room", beeps=3, beep_time=0.15)
    motion_seen = pir.motion_detected

    if shaker.is_pressed and not shaking:
        alarm("vibration on the door", beeps=1, beep_time=0.6)
    shaking = shaker.is_pressed

    if time.monotonic() >= next_reading:
        next_reading = time.monotonic() + 2
        try:
            temperature, humidity = dht.temperature, dht.humidity
        except RuntimeError as err:   # a DHT11 misses a reading now and then
            print(f"DHT11: {err}")
        else:
            fan.value = temperature >= FAN_ON_AT
            print(f"{temperature:.0f} C  {humidity:.0f} %RH  fan {'on' if fan.value else 'off'}")

    time.sleep(0.1)

Three things in it are worth a look, because they are the same on real hardware:

Step 5: run it

On the toolbar, click the green triangle to run the project. The Serial Monitor opens at the bottom and reports what the engine is doing: it loads the Python runtime, installs the GPIO library, and prints a line like [Velxio] ready in 6068 ms followed by the name of the script it is running. From then on, every print() in the script lands there.

The toolbar with the Run button highlighted

The script prints Station armed, then a temperature and humidity line every two seconds. The DHT11 on the canvas starts at 31 C, so the relay closes at once and the fan spins up: the motor shows a direction arrow and its speed in rpm, and the relay’s own indicator lights. Now interact with the sensors:

The station running in basic mode: the fan spins behind the relay, the blue LED is on after a click on the PIR dome, and the Serial Monitor shows the readings and the ALARM line

The red square next to the Run button stops the script. That is basic mode in full: the script runs in your tab, the pins reach the canvas directly, and the whole loop from Run to a reacting circuit takes seconds.

Project 2: an I2C sensor rig on a Raspberry Pi 4, then Linux mode

The second project puts three I2C devices on the header bus and uses the real smbus2 to talk to them: a Grove ADS1115 analog-to-digital converter with two potentiometers, a Grove SHT31 temperature and humidity sensor, and an SSD1306 OLED. A button and an LED on plain GPIO complete it. The same circuit works on a Raspberry Pi 5 without changing a wire or a line: the header is the same.

The ADS1115 is there for a reason that catches many people: a Raspberry Pi has no analog input. A potentiometer wired straight to a GPIO only ever reads high or low, on the real board and in Velxio, so the knobs go through the ADC.

The circuit

Start a new project on the Raspberry Pi 4B card and add these parts from the catalog:

Then wire them. All three I2C devices share GPIO2 (SDA) and GPIO3 (SCL):

From (Raspberry Pi 4)To
GPIO2 (SDA)SDA of the ADS1115, the SHT31 and the OLED
GPIO3 (SCL)SCL of the ADS1115, the SHT31 and the OLED
3V3VCC of the ADS1115, the SHT31, the OLED and both potentiometers
GNDGND of the ADS1115, the SHT31, the OLED and both potentiometers
GPIO27LED anode; LED cathode to 220 Ω, then GND
GPIO22Pushbutton; the other side of the button to GND
FromTo
Potentiometer 1 SIGADS1115 A0
Potentiometer 2 SIGADS1115 A1

The wired sensor rig: the Raspberry Pi 4 at the bottom, the ADS1115 with two potentiometers, the OLED and the SHT31 above it, and the LED and button to the right, with call-outs on the I2C pins and on each device

The script

Save this as script.py. It first asks each of the three addresses whether anyone answers, then loops: two ADC channels, temperature and humidity, the button state on the LED, and the readings on the OLED through luma.oled.

"""Sensor rig check for a Raspberry Pi 4.

Probes the three I2C devices, then loops: two knobs through the ADS1115,
temperature and humidity from the SHT31, the readings on the OLED, and a
button that lights the LED. Everything here is what Raspberry Pi OS ships:
smbus2 for the bus, gpiozero for the LED and the button, luma.oled for
the display.

Wiring, on the 40-pin header:
    I2C   : ADS1115, SHT31 and OLED on GPIO2 (SDA) and GPIO3 (SCL), 3V3, GND
    ADC   : one potentiometer on ADS1115 A0, another on A1
    GPIO  : LED through 220 ohm on GPIO27, button between GPIO22 and GND
"""
import time

import smbus2
from gpiozero import LED, Button
from luma.core.interface.serial import i2c
from luma.core.render import canvas
from luma.oled.device import ssd1306

ADS1115, SHT31, OLED = 0x48, 0x44, 0x3C
NAMES = {ADS1115: "ADS1115", SHT31: "SHT31", OLED: "SSD1306 OLED"}

# ADS1115 config register, field by field (datasheet, table 8)
ADS_START = 0x8000       # OS: begin one conversion
ADS_MUX_SINGLE = 0x4000  # AINx against GND; the channel goes in bits 12 and 13
ADS_PGA_4V = 0x0200      # +/-4.096 V full scale
ADS_ONE_SHOT = 0x0100    # power down between conversions
ADS_128_SPS = 0x0080     # 128 samples per second
ADS_NO_COMP = 0x0003     # comparator off
ADS_LSB_VOLTS = 4.096 / 32768

bus = smbus2.SMBus(1)
led = LED(27)
button = Button(22, pull_up=True)


def present(address):
    """True when a device acknowledges its address on bus 1."""
    try:
        bus.write_quick(address)
        return True
    except OSError:
        return False


def read_ads1115(channel):
    """One single-shot conversion on AIN0..AIN3, returned in volts."""
    config = (ADS_START | ADS_MUX_SINGLE | (channel << 12)
              | ADS_PGA_4V | ADS_ONE_SHOT | ADS_128_SPS | ADS_NO_COMP)
    bus.write_i2c_block_data(ADS1115, 0x01, [config >> 8, config & 0xFF])
    time.sleep(0.01)
    hi, lo = bus.read_i2c_block_data(ADS1115, 0x00, 2)
    raw = (hi << 8) | lo
    if raw & 0x8000:
        raw -= 0x10000
    return raw * ADS_LSB_VOLTS


def read_sht31():
    """Single shot, high repeatability, no clock stretching (command 0x2400)."""
    bus.i2c_rdwr(smbus2.i2c_msg.write(SHT31, [0x24, 0x00]))
    time.sleep(0.02)
    reply = smbus2.i2c_msg.read(SHT31, 6)
    bus.i2c_rdwr(reply)
    d = list(reply)
    temperature = -45 + 175 * ((d[0] << 8) | d[1]) / 65535
    humidity = 100 * ((d[3] << 8) | d[4]) / 65535
    return temperature, humidity


print("I2C bus 1:")
for address, name in NAMES.items():
    print(f"  0x{address:02X}  {name}: {'found' if present(address) else 'missing'}")
print()

oled = ssd1306(i2c(port=1, address=OLED))

while True:
    knob_a = read_ads1115(0)
    knob_b = read_ads1115(1)
    temperature, humidity = read_sht31()
    pressed = button.is_pressed
    led.value = pressed
    print(f"A0 {knob_a:5.3f} V   A1 {knob_b:5.3f} V   {temperature:4.1f} C  {humidity:3.0f} %RH"
          + ("   button pressed" if pressed else ""))
    with canvas(oled) as draw:
        draw.text((0, 0), "Pi 4 sensor rig", fill="white")
        draw.text((0, 16), f"A0 {knob_a:.2f}V  A1 {knob_b:.2f}V", fill="white")
        draw.text((0, 32), f"{temperature:.1f} C   {humidity:.0f} %RH", fill="white")
        draw.text((0, 48), "button " + ("down" if pressed else "up"), fill="white")
    time.sleep(2)

Three details are worth a look. The ADS1115 is configured field by field, the way its datasheet lays out the config register, so each constant can be checked against table 8 of that document. The SHT31 is read with i2c_rdwr because its measurement command is two bytes with no register address, which plain SMBus calls cannot express. And luma.oled is the same display library that Raspberry Pi tutorials install with pip; here it is preinstalled, like smbus2 and gpiozero.

Run it in basic mode first

Press Run. The probe finds the three chips, the loop prints the readings, and the OLED shows them. Turn a potentiometer and the A0 or A1 voltage follows; click the SHT31 and you can set the temperature and humidity it reports; hold the pushbutton and the LED lights.

The sensor rig running in basic mode: the OLED shows the two voltages, the temperature and humidity and the button state, and the Serial Monitor shows the three devices found and the readings

Everything so far ran in the browser. The next steps run the very same project on a Linux machine.

Switch to Linux mode

  1. If the Serial Monitor is not open, click Serial on the toolbar.
  2. In the Serial Monitor’s title bar, on the right, click Linux terminal.

The Serial Monitor title bar with the Linux terminal button highlighted

  1. A dialog called Advanced Linux mode explains what is about to happen: the project restarts, booting takes about a minute, and everything runs slower than in the browser. Click Start Linux mode.

The Advanced Linux mode dialog with the Start Linux mode button highlighted

The guest boots in the Serial Monitor, which has become a real terminal. While it boots, the chip next to the board selector in the toolbar reads Linux, and its tooltip says that Linux mode is on for this project.

The toolbar with the engine chip reading Linux highlighted

When the prompt appears, Velxio copies script.py into the guest’s home directory and starts it. On a quiet server the prompt shows up after about 20 seconds and the first readings about a minute after you clicked the button. The output is the same as before, and the OLED is driven by the same luma.oled call, now from inside the guest through the kernel’s /dev/i2c-1.

The sensor rig in Linux mode: the terminal shows the three devices found and the readings, and the OLED shows the same values

Use the shell

This is what Linux mode is for. Click inside the terminal, press Ctrl+C to stop the script, and you are at a root prompt on an Alpine Linux system. A few lines to try:

cat /etc/os-release | head -2
ls /dev/i2c* /dev/spidev* /dev/gpiochip*
gpioset gpiochip0 27=1
ls -la /home/pi

The second line lists the header buses as device nodes: /dev/i2c-0, /dev/i2c-1, /dev/spidev0.0, /dev/spidev0.1 and /dev/gpiochip0. They answer the same system calls as on a Raspberry Pi, so a library that opens them itself, a C program, or your own ioctl code talks to the parts on the canvas.

The third line is the modern way to drive a pin from the shell, with the libgpiod tools that Raspberry Pi OS Bookworm ships. The moment you press Enter, the LED on GPIO27 lights on the canvas. The guest has libgpiod version 1, so the chip is a positional argument, as written above, and the version 2 spelling with --chip is not understood.

The Linux shell after the script was interrupted: the gpioset command is highlighted and the LED on the canvas is lit

To run the script again by hand, type python3 script.py. To go back to the browser engine, click Exit Linux terminal in the same title bar. The choice is not saved with the project: reopen it tomorrow and Run goes back to basic mode.

The Serial Monitor title bar in Linux mode with the Exit Linux terminal button highlighted

What works in each mode

The table below is the short version of the Raspberry Pi page in the documentation, which was measured part by part against the live product.

ModuleBasic modeLinux mode
RPi.GPIO, gpiozeroPreinstalledPreinstalled
smbus2, spidevPreinstalled, the real librariesPreinstalled, the real libraries over the kernel nodes
serial (pyserial)The Pi UART paths onlyThe real pyserial on a real tty, /dev/serial0
adafruit_dht, Adafruit_DHTPreinstalledPreinstalled
w1thermsensor, the 1-Wire sysfs treePreinstalledThrough requirements.txt; the sysfs tree is there
luma.oled, RPLCD, ST7789, Adafruit BlinkaPreinstalledPreinstalled
gpiod and the gpio* toolsNo character device in the browserPreinstalled
cv2 (OpenCV)YesNo build for the guest
picamera2Yes, over your webcamNo; use rpicam-jpeg and its siblings
requests, urllibThrough an egress proxy with an allowlistNo network inside the guest
Anything else on PyPIThrough requirements.txtThrough requirements.txt, when PyPI has a musl aarch64 wheel

What neither mode does, so you do not spend a trial session finding out:

Two habits save time. Name the main file script.py, so that both modes run the same file. And put third-party packages in a requirements.txt next to it: when a script imports a package that is missing, the run console offers the line and a button writes it for you.

What it costs

Anyone can wire a Raspberry Pi circuit without an account. Running it is gated:

The toolbar as a signed-out visitor sees it, with the Free trial, sign in chip highlighted

Trial sessions are the same on both engines, so a script that only needs basic mode should stay there: it starts in seconds instead of a minute, and the Linux guest is for what only Linux can do.

Where to go next

Summary

Two projects, two engines. The Grove alarm station ran in basic mode on a Raspberry Pi 5, from an empty canvas to a reacting circuit in a few minutes, with the gpiozero and Adafruit DHT code a Raspberry Pi tutorial would use. The sensor rig ran first in basic mode on a Raspberry Pi 4, then in Linux mode, where the same script talked to the ADS1115, the SHT31 and the OLED through the kernel’s /dev/i2c-1, and a gpioset command typed at the prompt lit an LED on the canvas. Basic mode covers most of what a Raspberry Pi tutorial asks for; Linux mode is there for the terminal, the files and the system tools, at the price of a minute’s boot.


Share this post on:
David Montero

Written by

David Montero

Creator of Velxio, the open-source circuit and Arduino simulator.

GitHub velxio.dev

Related posts


Next Post
Castlevania: Symphony of the Night running natively on the ESP32-S3