Source: A1 — reTerminal Control Center
Stack Overview
- Python backend: FastAPI REST API controlling on-board devices
- TypeScript frontend: React/bun dashboard on reTerminal touchscreen
- HTTP communication between backend and frontend
uv Package Manager
uv replaces venv + pip for this assignment.
# Initialize project
uv init controller # creates main.py, pyproject.toml, README.md
# Manage dependencies
uv add fastapi['standard']
uv add smbus2
uv remove <package>
# Run the app
fastapi run # production (accessible on local network)
fastapi dev # development with auto-reloadpyproject.toml tracks all dependencies (like requirements.txt but modern).
AHT20 Sensor via smbus2
Seeed-grove.py is broken/deprecated — use this direct smbus2 implementation.
Critical: reTerminal uses I2C bus 4, not bus 1.
from gpiozero import Device
from time import sleep
from smbus2 import SMBus
class AHT20(Device):
def __init__(self, address=0x38, bus=4): # bus=4 on reTerminal!
self.address = address
self.bus = SMBus(bus)
# Initialize sensor
self.bus.write_i2c_block_data(self.address, 0xBE, [0x08, 0x00])
sleep(0.02)
def read(self):
# Trigger measurement
self.bus.write_i2c_block_data(self.address, 0xAC, [0x33, 0x00])
sleep(0.08)
data = self.bus.read_i2c_block_data(self.address, 0x00, 7)
humidity = ((data[1] << 16) | (data[2] << 8) | data[3]) >> 4
humidity = humidity * 100 / 1048576.0
temperature = ((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5]
temperature = temperature * 200 / 1048576.0 - 50
return temperature, humidity # (°C, %RH)
# Usage: only ONE AHT20 instance at a time (shared across sensors)
aht20_device = AHT20(bus=4)Sensor / Actuator Model (model.py)
from typing import Any
class Sensor():
device: Any
UNIT: str = ""
def __init__(self, device: Any):
self.device = device
def read_sensor(self) -> str:
"""Returns current reading as a string."""
pass
class Actuator():
device: Any
state: str = ""
def __init__(self, device: Any):
self.device = device
def control_actuator(self, state: str) -> str:
"""Changes state, returns new state as string."""
passConcrete implementations:
class TemperatureSensor(Sensor):
device: AHT20
UNIT = "°C"
def read_sensor(self):
temp, _ = self.device.read()
return f"{temp:.1f}{self.UNIT}"
class HumiditySensor(Sensor):
device: AHT20
UNIT = "%"
def read_sensor(self):
_, humi = self.device.read()
return f"{humi:.1f}{self.UNIT}"
class LED(Actuator):
pass # implement control_actuator
class Fan(Actuator):
pass # implement control_actuatorFastAPI Endpoints (app.py)
from fastapi import FastAPI
app = FastAPI()
# Register sensors and actuators with IDs
sensors = {"temperature": TemperatureSensor(aht20), "humidity": HumiditySensor(aht20)}
actuators = {"led": led_actuator, "fan": fan_actuator}
@app.get("/read/{sensor_id}")
def read_sensor(sensor_id: str):
return {"value": sensors[sensor_id].read_sensor()}
@app.put("/control/{actuator_id}")
def control_actuator(actuator_id: str, state: str):
return {"state": actuators[actuator_id].control_actuator(state)}Run: fastapi run (accessible via Tailscale IP)
Landscape Mode on reTerminal
# List connected displays
wlr-randr
# Rotate screen 270°
wlr-randr --output <display-name> --transform 270Or: Control Centre → Screens → right-click display → change orientation.
SSH Display Fix
If running GUI/dashboard code over SSH and getting “cannot open display”:
export DISPLAY=:0
# then run your dashboard commandVNC for Dashboard Preview
Enable VNC on reTerminal, then use TigerVNC from lab computer to connect via Tailscale IP.
Project Structure
a1/
├── controller/ # Python backend
│ ├── app.py # FastAPI app (rename from main.py)
│ ├── model.py # Sensor / Actuator base classes
│ ├── led.py # LED class
│ ├── fan.py # Fan class
│ ├── aht20.py # AHT20 + TemperatureSensor + HumiditySensor
│ └── pyproject.toml # uv dependencies
└── dashboard/ # Frontend
├── package.json
└── bun.lock
See Also
- fastapi-and-http-backend topic
- GPIO and reTerminal topic (AHT20, seeed devices)
- Python Essentials topic (uv, venv)
- I2C concept