Documentation / Hardware

Peripherals and I/O

Buses, devices, expanders, analog and digital pins - and the poll loop that reads them.

Beyond the panel and the touch controller, a board has sensors, expanders, analog inputs and plain digital pins. The Buses, Devices and I/O tabs describe them, and the generator writes hardware.h and hardware.cpp to bring them up and read them on a schedule. Nothing here ever draws: the readings go into caches that variables read.

Buses

The Buses tab declares I2C and SPI buses beyond the display's. An I2C bus has SDA, SCL, a clock in kHz and an internal pull-up option (weak - fit 4.7 kΩ externals for anything but the shortest run). An SPI bus has SCK, MOSI, an optional MISO, a clock in MHz and the SPI mode. The first bus of a kind becomes Wire or SPI in the generated code, the next Wire1 or SPI1, and so on up to what the MCU has - except that an I2C touch controller already has Wire: a bus on the touch controller's own pins shares it, and a bus on any other pins starts at Wire1. The tab's heading for each bus shows the object it became, which is the one to use from your own code. A device attached to a bus that shares pins with the display's bus is legitimate and not flagged.

Devices

The Devices tab adds peripherals. Each has a Type, a Bus, an I2C address or a CS pin depending on the bus, and a Poll interval - how often hw_poll() reads it, independently of everything else, so a slow sensor never holds up a fast one or the display. Below those comes the device's own configuration: every register the driver library exposes, applied once in hw_init() after the device reports present.

The Devices tab with a BME280 on the first I2C bus: bus, address, poll interval, then its sampling configuration.
The Devices tab with a BME280 on the first I2C bus: bus, address, poll interval, then its sampling configuration.
GroupDeviceBusChannelsLibraryConfiguration
EnvironmentBME280I2CTemperature (C), Humidity (%), Pressure (hPa), Altitude (m)adafruit/Adafruit BME280 Librarymode, osTemp, osPress, osHum, filter, standby, seaLevel
EnvironmentBMP280I2CTemperature (C), Pressure (hPa)adafruit/Adafruit BMP280 Librarymode, osTemp, osPress, filter, standby
EnvironmentSHT31 / SHT35I2CTemperature (C), Humidity (%)adafruit/Adafruit SHT31 Libraryheater
EnvironmentAHT10 / AHT20I2CTemperature (C), Humidity (%)adafruit/Adafruit AHTX0-
EnvironmentSCD40 / SCD41 (CO2)I2CCO2 (ppm), Temperature (C), Humidity (%)sparkfun/SparkFun SCD4x Arduino LibrarytempOffset, altitude, asc
EnvironmentSEN66 (air quality)I2CPM1.0 (ug/m3), PM2.5 (ug/m3), PM4.0 (ug/m3), PM10 (ug/m3), Humidity (%), Temperature (C), VOC index, NOx index, CO2 (ppm)sensirion/Sensirion I2C SEN66 1.3.1-
EnvironmentBH1750 (light)I2CIlluminance (lx)claws/BH1750mode, mtreg
EnvironmentMCP9808 (temperature)I2CTemperature (C)adafruit/Adafruit MCP9808 Libraryresolution, wake
ADCADS1115 (16-bit ADC)I2CChannel 0 (V), Channel 1 (V), Channel 2 (V), Channel 3 (V), Differential 0-1 (V)adafruit/Adafruit ADS1X15gain, rate
ADCINA219 (current)I2CBus voltage (V), Current (mA), Power (mW), Shunt voltage (mV)adafruit/Adafruit INA219calibration
ADCMAX17048 (battery gauge)I2CCell voltage (V), State of charge (%)adafruit/Adafruit MAX1704XalertMin, alertMax
ADCMCP3008 (8ch 10-bit ADC)SPIChannel 0 (counts), Channel 1 (counts), Channel 2 (counts), Channel 3 (counts), Channel 4 (counts), Channel 5 (counts), Channel 6 (counts), Channel 7 (counts)adafruit/Adafruit MCP3008vref
MotionMPU6050 (IMU)I2CAccel X (m/s2), Accel Y (m/s2), Accel Z (m/s2), Gyro X (rad/s), Gyro Y (rad/s), Gyro Z (rad/s), Temperature (C)adafruit/Adafruit MPU6050accelRange, gyroRange, bandwidth, cycleMode
MotionADXL345 (accelerometer)I2CAccel X (m/s2), Accel Y (m/s2), Accel Z (m/s2)adafruit/Adafruit ADXL345range, dataRate
MotionQMI8658 (IMU)I2CAccel X (g), Accel Y (g), Accel Z (g), Gyro X (deg/s), Gyro Y (deg/s), Gyro Z (deg/s), Temperature (C); Steps with Count steps onlewisxhe/SensorLibpedometer (Count steps), stepSwing, stepPeak, stepFastest, stepSlowest, stepEntry, stepEvery
MotionVL53L0X (distance)I2CDistance (mm)adafruit/Adafruit_VL53L0Xsense
MotionHC-SR04 (ultrasonic distance)GPIODistance (cm), Something in rangebuilt in (no library)maxCm, tempC
TemperatureMAX6675 (thermocouple)SPITemperature (C)adafruit/MAX6675 librarysettleMs
TemperatureMAX31855 (thermocouple)SPITemperature (C), Cold junction (C)adafruit/Adafruit MAX31855 librarysettleMs
TemperatureMLX90614 (IR thermometer)I2CObject temperature (C), Ambient temperature (C)adafruit/Adafruit MLX90614 Library 2.1.7-
TimeDS3231 (RTC)I2CHour, Minute, Second, Day, Month, Year, Unix time (s), Die temperature (C)adafruit/RTClibsqw, enable32k, setOnBoot
TimeDS1302 (RTC)GPIOHour, Minute, Second, Day, Month, Year, Day of week, Unix time (s), Clock runningRtc by Makunatrickle, setOnBoot
StorageSD cardSPICard size (MB), Used (MB)SD (bundled with the core)freqMhz
OutputPCA9685 (16ch PWM)I2C-adafruit/Adafruit PWM Servo Driver LibrarypwmFreq, oscFreq, totemPole
OutputNeoPixel string (WS2812 / SK6812)GPIOPixel countadafruit/Adafruit NeoPixelpixels, order, speed, brightness

A device that fails to answer at boot is marked absent, its variables keep their initial values, and the sketch says so on the serial monitor rather than hanging. Each device's library is listed in LIBRARIES.md with the Library Manager search string.

CAN bus and OBD-II

CAN bus / OBD-II reads a car through its diagnostic socket. Every ESP32 has the CAN controller built in and none has the transceiver, so the one extra part is an SN65HVD230 or a TJA1051 between two pins of your choice and the socket: CAN-H is pin 6, CAN-L pin 14, ground pin 5. Pin 16 is the battery - 12 V and more - and must never reach the board directly. It is an Arduino export feature, and a board has one.

A car gives up its values in two ways, and the device reads both. Some the engine computer only gives when asked: a request names a PID and the reply carries the value. Others are simply on the bus, sent over and over in a frame with an identifier of its own. The standard PIDs below are built in, each shown on the device with its code and the arithmetic that makes a number of the reply:

ValuePIDWorked out asUnit
Coolant temperature05A - 40C
Fuel level2FA * 100 / 255%
Engine speed0C(A * 256 + B) / 4rpm
Road speed0DAkm/h
Engine load04A * 100 / 255%
Throttle position11A * 100 / 255%
Intake air temperature0FA - 40C
System voltage42(A * 256 + B) / 1000V

Beside those, ECU answering is 1 while an engine computer has replied in the last three seconds and Bus active is 1 while anything at all is on the wire. A value the car does not report stays at 0: the firmware asks the car which PIDs it has before asking for any, so a missing one costs nothing. Fuel level is one many cars leave out.

Signals of your own

Anything else the bus carries is a signal you add on the device, with Add a PID or Add a frame. A PID signal takes the code in hexadecimal, as PID tables give it, and a formula over A, B, C and D - the bytes of the reply, named the way those tables name them. A frame signal takes the frame's identifier (tick 29-bit for a long one), the first byte, how many bytes, their order and whether the number is signed; its formula works on raw, the number those bytes make, and can also name the frame's bytes B0 to B7. The row shows what the formula gives for a sample value and says straight away if the export would refuse it.

Every value, built in or added, has a Variable button: it makes a variable that reads it, and that variable is what you pick under Data bindings on a widget - a label's text, an arc's value, a scale's needle. The variable's own formula, smoothing and automations then apply as to any reading.

Two settings matter on a car. Bus speed is 500 kbit/s on nearly all of them; the wrong one takes the board off the bus rather than reading slowly, and the firmware brings it back and keeps trying. Listen only never transmits and never acknowledges, so nothing on the bus can tell the board is there: no PID is asked for, and only frame signals are read. Use it on a bus you must not disturb. Cars older than about 2008 often use K-line rather than CAN, which this does not speak.

RS485 and Modbus

RS485 / Modbus RTU reads and writes the devices on an RS485 line: a drive, an energy meter, a temperature controller, a relay board. The board is the master - it asks, they answer. It needs an RS485 transceiver on a serial port of its own: TX to the transceiver’s DI, RX from its RO, and one more pin to DE and RE together, which the chip’s serial port then switches by itself to the bit. A board whose transceiver turns itself round, as Waveshare’s do, leaves that pin unset. It is an Arduino export feature for the ESP32 family, and a board has one.

Nothing is asked for until you say what to read. Add a register on the device takes:

SettingWhat it is
Device addressThe address set on the device itself, 1 to 247. Its manual may call it the slave ID or unit ID.
TableHolding register or input register (numbers), coil or discrete input (one bit). Holding registers and coils can be written; the other two are read only.
Register numberAs the protocol counts it, from 0. Where a manual writes 40101 or 30101 it means register 100: drop the leading 4 or 3 and take one off.
Holds16-bit unsigned or signed, 32-bit unsigned or signed, or a 32-bit float. A 32-bit value is two registers, and Word order says which half comes first - devices disagree, and a reading that is nonsense is usually this.
FormulaOver raw, the number the device sent. A register in tenths of a degree is raw / 10.

Each register then has a Variable button like any other value, and the row says straight away what the export would refuse. Registers are asked for one at a time, in turn, with a pause between requests that you set; nothing waits for an answer, so a device that is switched off costs the screen nothing. A register that stops answering keeps its last reading. Two values say how the line is doing: A device answering is 1 while anything has answered in the last few seconds, and Failed requests counts those that got no answer, a damaged one, or a refusal.

To write a holding register or a coil, use the action Write to peripheral: it offers the registers that can be written. What is sent is the number the device holds, before your formula - a formula runs one way - rounded to a whole number for the integer kinds and held to their range. A write goes out before the next read, is tried three times if nothing answers, and the register is read back straight after, so what the design shows is what the device now holds.

Every device on the line needs the same Speed and Format as the board. The Modbus standard asks for even parity (8E1); most low-cost devices ship set to 9600 baud, 8N1. If nothing answers, check those two, the device address, and then swap the A and B wires: makers disagree about which is which.

Modbus TCP

Modbus TCP (client) is the same thing over a network: a meter, a drive or a PLC that is on the network itself, or a gateway in front of an RS485 line. It has no pins - give it the device’s Address (an IP address, or a name the network’s DNS knows) and Port (502, Modbus TCP’s own) - and its registers are added exactly as on RS485, with one difference: a register’s device is a Unit id, 0 to 255. A device that is itself on the network usually answers to 1 or 255; behind a gateway, the unit id is the device’s address on its serial line.

It needs a network: a WiFi Setup or WiFi by phone widget, or an Ethernet port. In a design with both, Reached through chooses which, and starts as Ethernet. It keeps one connection open, asks for one register at a time, writes before it reads and reads a register straight after writing it, as the RS485 driver does, and it connects again whenever the connection is lost. All of it runs in a task of its own on core 0, so the screen never waits for the network. Besides each register, it says Connected to it, A device answering and Failed requests. A design may have several, one for each device or gateway. Arduino export, any ESP32; nothing here has spoken to a real device yet.

A camera

A camera with a DVP connector (an OV2640 or OV5640 module) is a device here too: its control lines on an I2C bus, its clock, sync and data lines on pins of its own. A Camera view widget plays it on a screen, the action Take a picture saves a JPEG, and it can be watched over Wi-Fi. It has a page of its own. ESP32-S3 with PSRAM, Arduino export.

On an ESP32-P4 the camera is the one on its MIPI connector (an OV5647, an SC2336 and the like): only its control bus is set here, since the chip's camera lanes are fixed. It does everything the S3's camera does, and can also be an RTSP camera on the network (Pro): the P4's camera.

Wired Ethernet

A wired network is a device as well: Ethernet over SPI (a W5500 module, on any ESP32) or an Ethernet PHY on RMII (a LAN8720 or the like, on the original ESP32 and the ESP32-P4). Everything that uses the network then works over the cable, with or without Wi-Fi. Which pins each needs, and what it says of itself, is on the network page. Arduino export.

A device’s line on an I/O extender

Two lines of a device may be a pin of an I/O extender rather than a GPIO, because some boards are wired that way. An SD card’s CS: the SD library cannot switch a pin it does not have, so the card is selected once and stays selected - which means it must be the only thing on its SPI bus, and the Errors tab says so if it is not. And a CAN bus’s enable / select line: a line the transceiver needs held one way to be on the bus, held that way while the device is on and the other way while it is switched off. Waveshare’s ESP32-S3-Touch-LCD-4.3 and ESP32-S3-Touch-LCD-7 have both: the card slot’s CS and the switch that hands GPIO 19 and 20 from the USB port to the CAN transceiver are pins of the CH422G. Their CAN bus therefore arrives switched off - switching it on takes the USB port away, and flashing is then through the UART port; those two boards have a page of their own. Only the Arduino export drives a device’s line through an extender; on the other two that device is left out of the board’s kit.

Which export reads which. The Arduino export reads every device here. The MicroPython export reads most of them through a driver module of its own for each – all but the BME280, BMP280, SCD4x, VL53L0X, the CAN bus, RS485, Modbus TCP and Ethernet – and ESPHome reads the ones it has a component for. A device the project’s export cannot read is greyed in the list with the reason. The I/O extenders below are driven by all three: every one on Arduino and MicroPython, seven on ESPHome.

Air quality and an infrared thermometer (SEN66, MLX90614)

The SEN66 is Sensirion’s air-quality module: particles (PM1.0, PM2.5, PM4.0 and PM10, in micrograms per cubic metre), humidity, temperature, a VOC index, a NOx index and CO2 in ppm, nine values from one part on I2C at 0x6B, its only address. It measures once a second by itself; the export starts it at power-up (a reset, which takes 1.2 s, then a start) and before each read asks whether a new set is ready, so the poll interval, 2 s to begin with, never reads one twice. The sensor says “not known yet” for a value while it warms up - CO2 for its first seconds, the NOx index for about ten - and such a value is not shown as a number: each one stays at 0 until the sensor has given it once, and after that keeps its last value. The VOC and NOx indices are relative to the air the sensor has known: 100 (VOC) and 1 (NOx) are “as usual”, and they take hours of running to settle.

The MLX90614 is Melexis’s infrared thermometer: Object temperature is whatever it points at, Ambient temperature the sensor itself, both in °C. It answers at 0x5A unless its address has been changed (the address is kept in the sensor’s own memory; 0x5B to 0x5D are offered too). It is an SMBus part, made for a 100 kHz bus at most: on a faster bus the Check tab says so, and the bus’s Clock is under Buses. Its emissivity is left as the sensor has it.

Two addresses that are often taken. 0x6B is also the QMI8658 motion sensor’s on many boards, and 0x5A the CST9217 / CST9220 touch panel’s; the Check tab refuses either clash on one bus. On the Waveshare ESP32-C6-Touch-AMOLED-2.16 both are soldered to the board’s one I2C bus (GPIO 8 / 7, the SCL and SDA pads): an MLX90614 there needs its address changed first, and a SEN66 cannot share those wires with the board’s QMI8658 - taking the motion sensor out of the design does not take the chip off the bus.

Both are read on Arduino (Sensirion’s and Adafruit’s libraries), ESP-IDF and MicroPython (drivers of this export’s own, checked on a PC against those libraries), and ESPHome (its sen6x and mlx90614 components). On ESPHome the SEN66 publishes nothing for its first minute, and the MLX90614 component writes an emissivity of 100 % into the sensor at every start. Compiled; not yet seen on a sensor.

Counting steps (QMI8658)

The QMI8658 has a step counter of its own - the one on Waveshare’s watch-shaped boards, the ESP32-S3-Touch-AMOLED-2.06 among them. Tick Count steps in its device configuration and it gives one more value, Steps, for a variable to read and a label or an arc to show, and one write, Reset steps, for a button’s Write to peripheral action. With Count steps off neither is there, and the device is written exactly as it was before.

The counter works from the accelerometer, so with it on the accelerometer and the gyroscope both run at 56 samples a second (rather than about 900); acceleration, rotation and temperature are read as before. The chip counts a walk only once several steps come in a row - ten, unless Steps before counting starts says otherwise - and then counts them all, so the number goes from 0 to 10 at once and then by ones. The other settings are the chip’s own: how big a swing is a step, how far above the average a peak has to rise, the shortest and the longest step. Their defaults are the example in QST’s datasheet; a wrist swings more than a belt, so a watch that counts too many is given a bigger swing.

The count is kept through a read that fails, and goes back to 0 only with Reset steps or a power cut. In Preview, Steps is a simulated reading in the drawer, and a Reset steps button puts it to 0. The Arduino, MicroPython and ESP-IDF exports all count; ESPHome’s own QMI8658 component has no step counter, and this export does not write the part on ESPHome.

Not yet seen working on a watch. The register writes are SensorLib’s, checked on a PC against the library itself, but nothing here has counted a real walk, and people have reported the counter reading 0 on some of Waveshare’s boards. If yours stays at 0 while you walk, say so through Report a problem.

QMP6988 barometer (M5Stack ENV III)

The QMP6988 (pressure) device, under Environment, is the barometer in M5Stack’s ENV III unit (its SHT30 is an SHT31 / SHT35 at 0x44). It sits on I2C at 0x70, or 0x56 with its address pad changed, and gives Pressure (hPa) and Temperature (C). Its configuration is Temp oversampling, Pressure oversampling (more is steadier and slower) and IIR filter. It needs no library, and it is read on every export.

A joystick

The Joystick (analogue thumbstick) device, under Input, is the common KY-023 kind: two potentiometers and a press switch to ground. Give it the pins VRx (X axis), VRy (Y axis) and SW (press), or under Read X and Y from choose An ADC device in this design and pick an ADS1115 or MCP3008 and a channel for each axis, which reads more steadily and frees pins. It gives X and Y (-100 to 100, 0 at rest), Pressed, and Direction (0 centre, 1 up, 2 right, 3 down, 4 left).

The resting position is taken as the centre from the first reads at start-up, so leave the stick alone while the board starts. Each side is scaled to its own end, so a stick whose centre is not half way still reaches 100 both ways. The settings are Dead zone (8 % by default: how far from the centre before X or Y leaves 0), Invert X, Invert Y and Swap X and Y (for a stick mounted on its side). Compiled, not run on a board.

Power chips (AXP192 and AXP2101)

The AXP192 (power and battery) and AXP2101 are X-Powers power chips on I2C. They give Battery voltage, Battery charge, Charging, Battery connected, USB power present, USB voltage and System voltage. On a board whose screen is powered through one, the preset switches the screen’s supply, reset and backlight through the chip before the panel starts: that is how the M5Stack Core2 presets (v1.0 with an AXP192, v1.1 with an AXP2101) light the screen. The AXP192 works on Arduino, ESP-IDF and MicroPython; ESPHome has no component for it and refuses it. Compiled, not run on a board.

Stream to card: a CSV of your variables

Hardware › Devices › Stream to card writes the design’s variables to a CSV file that a spreadsheet opens: a row at a time, each variable a column headed by its name. It is free, on the Arduino export, on the ESP32 family. With no SD card in the design a log is kept in the board’s own flash. Add log adds one; a design can have up to eight.

SettingWhat it does
NameWhat the actions call this log.
Kept onThe SD card (take it out and read it on a computer) or The board’s own flash (works with no card, in files of the size set below).
File nameLetters, figures, - _ . and / for a folder; .csv is added when the name has no ending. Put {n} in it (log_{n}.csv) for a new file each time the log starts: log_1.csv, then log_2.csv. Without {n}, rows are added to the end of the same file under the header it already has, so after changing the columns give it a new name.
ColumnsTick the variables to write, in the order ticked. Whole numbers as they are, numbers with the variable’s own decimals, 0 or 1 for on and off, text in double quotes. A list is not a column.
First columnNone, Seconds since the board started, or Date and time from the design’s clock (YYYY-MM-DD HH:MM:SS, empty until the clock knows the time; offered when the design has a Clock widget).
Every (s)The time between rows. 0: a row only when an action asks.
StartWhen the board starts. Off, the log waits for an action.
New file at (KB), Old files keptWhen a row would take the file past this size it is renamed (log.1.csv, the one before that log.2.csv) and a new file starts with the header. Beyond Old files kept, the oldest is deleted. On the card the default is never; in flash a size is required (64 KB by default).

Actions (group Stream to card): Stream to card: start or stop (Do: Start, Stop, or start if stopped and stop if running; a start picks the next free {n}) and Stream to card: write a row (one row now, running or not). Values on the Data tab, in a group Log: <name>: Running (1) or stopped (0) and Rows written since start-up.

Every row opens the file, appends and closes it, so a pulled card or a power cut loses one row at most. A log that cannot write stops itself. The card is found by the card watcher if it is swapped while the board runs. Not done: noticing that the columns changed, reading a log back on the board, and the ESP-IDF, MicroPython and ESPHome exports (each refuses it). In Preview the values read 0. Compiled, not run on a board.

Configuration worth knowing about

I/O extenders

Port expanders that add digital pins over I2C or SPI. Add one on the I/O tab with its bus and address (or CS pin), and digital I/O entries can then be placed on its pins instead of the MCU's. Each pin's direction is set by the entry that uses it - a digital input or output placed on the pin, a line of the board's that runs through it, or the extender's own pin table below, in that order where two name one pin - and the generated ext_<name>_write() and _read() helpers are guarded by the expander's presence.

ExtenderBusPinsLibrary
PCF8574I2C8xreef/PCF8574 library
PCF8575I2C16xreef/PCF8575 library
MCP23017I2C16adafruit/Adafruit MCP23017 Arduino Library
MCP23S17 (SPI)SPI16adafruit/Adafruit MCP23017 Arduino Library
CH422GI2C12built in (no library)
TCA9554 / PCA9554I2C8built in (no library)
PCA9555I2C16built in (no library)

The validator errors on an extender pin outside the part's width, on two entries claiming the same pin, and on an entry pointing at an extender that has been deleted. The same rules cover a display reset, touch reset or backlight routed through an extender - a backlight there switches on and off but cannot dim, and a touch controller whose reset is there is reset through it at boot and whenever it stops answering, with the bus left running (see Boards and displays).

The CH422G, TCA9554 and PCA9555 need no library: each is a few I2C register writes, and the generator writes a small driver class into hardware.cpp for them. The PCF and MCP parts use their Arduino libraries, listed in LIBRARIES.md as usual. A PCF8574 or PCF8575 has no direction register: its library sends the port when it starts, from the modes it has been given, so the generated code gives every pin its mode first and starts it after.

Analog inputs

An Analog input is an MCU ADC pin read and scaled. Fields:

FieldMeaning
PinAn ADC-capable GPIO on the chosen MCU.
AttenuationESP32 only: 11 dB (0-3.3 V), 6 dB (0-2.2 V), 2.5 dB (0-1.5 V) or 0 dB (0-1.1 V).
SamplesHow many conversions are averaged per read.
Poll (ms)How often it is read.
SmoothingAn exponential filter: 0 is raw, 0.2 is a good default, closer to 1 reacts faster.
ScalingA linear map from a raw range (e.g. 0-4095) to a value range (e.g. 0-100).
FormulaOptional; overrides the scaling. raw is the averaged ADC count.
UnitA label carried through to bindings.

The ADC pins per MCU come from the catalogue - the ESP32 has 16, the S3 20, the C3 6, the RP2040 4 - and a pin that is not one of them is an error. The STM32 and ESP32-P4 entries currently have no ADC pins listed, so analog inputs are not offered on them.

Digital I/O

A Digital input or output on an MCU pin or an extender pin:

FieldMeaning
SourceMCU pin or I/O extender.
Pin / Extender pinThe GPIO, or the extender and its pin number.
Active lowActive when the pin reads LOW. Everything downstream uses "active", not the raw level.
Pull-upEnable the internal pull-up on an input.
Debounce (ms)How long a level must hold before it counts.
Initial stateFor an output: start active or not.

Inputs are debounced in hw_poll() and exposed to variables as a Boolean; the Digital input changes and Digital input held automation triggers watch them. Outputs are driven by the Set digital output action. "Active" respects the active-low setting throughout, so a relay module that switches on LOW is described once and reads correctly everywhere.

What the generated code does

hardware.cpp declares one driver object per device and the bus objects they sit on. hw_init() begins each bus, probes each device, applies its configuration and records whether it answered. hw_poll(), called from app_poll() every loop, checks each device's own timer and reads it when due into a cache struct. Reads are cheap to ask for and never block on a slow part - a BME280 polled every two seconds costs nothing to a label refreshed ten times a second. The Refresh inputs now action forces every timer due at once.

Code of your own reaches all of this - the buses, the widgets and the variables - from a file beside the sketch, through the setup and loop hooks: see Your own code.