Hardware Setup
Wiring, serial protocol, I2C bus, permissions, mounts and first-run verification for the ESP32 volume knob, BerryIMU v3 and PAJ7620U2 gesture sensor.
What Works Where
This page is the physical-side companion to Hardware Managers, which documents the driver code in src/managers/.
| Peripheral | Linux / Raspberry Pi / Orange Pi | Windows / macOS | Android |
|---|---|---|---|
| ESP32 volume knob (USB serial) | Yes | Yes | No (stub) |
| BerryIMU v3 (I2C) | Yes | No — reports "Disconnected" and never retries | Replaced by the phone's own sensors (QtSensors) |
| PAJ7620U2 gesture sensor (I2C) | Yes | No — reports "Disconnected" and never retries | No (stub, "Disabled on mobile") |
The I2C drivers use Linux /dev/i2c-* devices through ioctl(I2C_SLAVE)
inside #ifdef Q_OS_LINUX. The reference installation that the timing numbers in the code were
measured on is an Orange Pi 5 Plus.
ESP32 Wireless Volume Knob
Linux Windows macOSTwo ESP32 boards: a remote encoder unit (rotary encoder + push button) and an ESP32-S3 receiver dongle plugged into the computer over USB. The encoder talks to the dongle over ESP-NOW; the dongle re-emits the commands as text lines on its USB serial port and drives an LED ring from the replies. OCTAVE only sees the dongle's serial port.
Architecture
[Rotary Encoder] ESP-NOW (2.4 GHz) [USB Serial]
[ + Button ] ----> [ESP32 Dev Board] ~~~~> [ESP32-S3 Dongle] ----> Computer
"Encoder Unit" "Receiver" (OCTAVE)
Sends: +, -, M + LED ring 115200 8N1
Forwards to USB
Serial Protocol
115200 baud, 8 data bits, no parity, 1 stop bit, no flow control. Every command is one ASCII line
terminated by \n; OCTAVE trims and upper-cases incoming lines before parsing.
| Line | Direction | Meaning |
|---|---|---|
+ or +N | Dongle → OCTAVE | Volume up by 1 or N encoder ticks |
- or -N | Dongle → OCTAVE | Volume down by 1 or N ticks |
M | Dongle → OCTAVE | Mute toggle (button press) |
P | Dongle → OCTAVE | Reserved for play/pause (double-click); accepted and logged, no action |
V<0-100> | OCTAVE → Dongle | Current volume percent for the LED ring (debounced to at most 20/s; also used as the 2 s keepalive) |
M0 / M1 | OCTAVE → Dongle | Mute indicator off / on |
C<r>,<g>,<b> | OCTAVE → Dongle | LED colour, e.g. C255,128,0 |
Each tick is multiplied by the esp32VolumeStepSize setting (default 1 %, adjustable 0.25–10 %
in the Volume Knob settings); fractional results accumulate until a whole percent is available, then
volumeController.applyVolume() moves every audio output together.
Detection and port selection
The dongle is recognised by Espressif's USB vendor ID 0x303A (or "JTAG" / "ESP32-S3" in the port
description) and listed first, labelled "<port> - ESP32-S3 (Recommended)", in
Settings → Accessories → Volume Knob. The OBD serial port is never offered.
The list comes from QSerialPortInfo::availablePorts() on every desktop platform: every enumerated
port is listed (on Linux typically /dev/ttyACM* / /dev/ttyUSB*, on Windows
COM ports, on macOS /dev/cu.* / /dev/tty.* devices), with the ESP32-S3 matched by VID or by "JTAG" /
"ESP32-S3" in the description (Windows names it "USB JTAG/serial debug unit").
esp32VolumePort setting defaults to "" (empty), and an empty setting
means "auto-select": on the first start OCTAVE scans, saves the detected ESP32-S3 port and connects. Only an ESP32-S3 is picked automatically; for any other USB-serial dongle choose the port
in the Volume Knob settings (OCTAVE saves the choice and reconnects 500 ms later). Such a port shows "turn the knob to confirm"
and gets no LED commands until the first knob turn or press arrives.
A settingsConfigure.json written by an older build may still carry the former "COM7"
default; clear or change it there once. From then on it connects 3 s after every launch.
Connection behaviour
- Startup: a 3 s delay, a port scan, then
connect_device()ifesp32VolumeEnabledis true. - Unplug: detected on a serial error (
QSerialPort::ResourceError,DeviceNotFoundError,PermissionError,ReadError,WriteError) or a keepalive write the port rejects. Withesp32AutoReconnecton, up to 5 attempts at 3, 6, 9, 10, 10 s (min(10 s, 3 s × n)); the Accessories page shows "Reconnecting in Ns... (n/5)" and finally "Max retries reached. Check connection." Pressing Connect there resets the counter. - While disconnected, a background scan every 10 s keeps the port list fresh.
- Changing the port disconnects and schedules reconnect 500 ms later. The enable toggle starts or stops the feature.
LED ring modes
- Theme (
esp32LedColorMode = "theme", default): the accent colour extracted from the current album art is sent asCwhenever the theme changes. - Static (
"static"): a fixed colour from the picker (esp32LedStaticColor, default#00FFFF). - LED sleep (
esp32LedSleepEnabled, default on): the manager assumes firmware that turns the LEDs off after 5 s without traffic. OCTAVE implements "keep LEDs on" by sending aVkeepalive every 2 s only when sleep is off; leaving sleep on means the ring also goes dark when OCTAVE exits.
src/main.cpp sends the colour
whenever it changes, the volume on every change through volumeController, and an initial volume / mute /
colour sync 500 ms after the dongle connects, so the ring matches the app. Details on
Hardware Managers → LED Feedback.
Serial permissions (Linux)
sudo usermod -aG dialout $USER # then log out and back in
I2C Bus 2 & Permissions
Linux only
Both I2C drivers hard-code bus 2 (I2C_BUS = 2 in
src/managers/berryimumanager.cpp, src/managers/gesturemanager.cpp).
There is no setting or command-line flag to change it: the sensors must be reachable as /dev/i2c-2.
/dev/i2c-2 exists before launchingi2c-2 is a property of the board and its device-tree configuration, not of
OCTAVE. On boards where the default header bus is a different number, enable a second bus (or remap one) with a
device-tree overlay in your boot configuration, reboot, and confirm with ls /dev/i2c-2. Without it
OCTAVE logs "failed to open /dev/i2c-2", then retries
five times (5, 10, 15, 20, 25 s) and give up.
Both sensors share the bus. With everything wired and powered, the scan should show all four addresses:
sudo apt install i2c-tools # if needed
i2cdetect -y 2
# 0x1c LIS3MDL (BerryIMU magnetometer)
# 0x6a LSM6DSL (BerryIMU accelerometer + gyroscope)
# 0x73 PAJ7620U2 (gesture sensor)
# 0x77 BMP388 (BerryIMU barometer)
I2C permissions
sudo usermod -aG i2c $USER # then log out and back in
On a permission error the drivers emit "Disconnected" and deliberately do not retry (the fix
needs a re-login); they log
"permission denied on /dev/i2c-2 - add the user to the i2c group; not retrying". Toggle the sensor off
and on in Settings → Accessories (or restart) after fixing the group.
BerryIMU v3 IMU Sensor
Linux / Raspberry Pi / Orange Pi
The BerryIMU v3 carries an LSM6DSL accelerometer/gyroscope, a LIS3MDL magnetometer and a BMP388 barometer. OCTAVE
fuses accelerometer + gyroscope with a Madgwick filter into a quaternion that rotates the Jeep model on
CarMenu, and derives pitch, roll, compass heading, G-forces, altitude and the barometer's temperature for the
sensor page, dashboards and home cards.
Wiring
BerryIMU v3 Single-board computer
+-----------+ +---------------------------+
| 3V3 |------------------| 3.3 V |
| GND |------------------| GND |
| SDA |------------------| SDA of the bus that is | /dev/i2c-2
| SCL |------------------| SCL exposed as i2c-2 |
+-----------+ +---------------------------+
Use the BerryIMUv3's 3.3 V connection for this wiring; the board also has level shifting for 5 V hosts. Verify the connector pinout in the manufacturer's BerryIMUv3 guide. Keep the run short and away from ignition wiring.
I2C addresses and initialisation
| Chip | Address | Function | Configured as (register ← value) |
|---|---|---|---|
| LSM6DSL | 0x6A | Accelerometer + gyroscope | 0x10 ← 0x68 (accel 416 Hz, ±4 g), 0x11 ← 0x60 (gyro 416 Hz, 245 dps), 0x12 ← 0x44 (BDU + auto-increment) |
| LIS3MDL | 0x1C | Magnetometer (heading only) | 0x20 ← 0xFE (ultra-high performance, 155 Hz), 0x21 ← 0x20 (±8 gauss), 0x22 ← 0x00 (continuous), 0x23 ← 0x0C (ultra-high performance Z) |
| BMP388 | 0x77 | Pressure + temperature | 21 calibration bytes read from 0x31; 0x1C ← 0x03, 0x1D ← 0x02, 0x1B ← 0x33; 50 ms settle; read every 200 ms |
Full register map, scale factors and the compensation math: Hardware Managers → I2C Sensor Chips.
Filter and rates
- Sample loop: 5 ms period (200 Hz target). The documented Orange Pi 5 Plus measurement was about 4.3 ms for the three I2C reads and about 172 Hz overall; actual timing depends on the bus and device. The loop sleeps only when time remains in the target period.
- Madgwick beta: 0.03. Gyro deadband: 0.4 dps; the filter is only stepped while some axis is moving, so a parked vehicle's model does not creep.
- Fusion inputs: gyroscope + accelerometer (6DOF). The magnetometer is read but used only for the compass heading (
atan2(my, mx)); yaw is stripped from the quaternion before it reaches the model. - Emission: ~60 Hz by default (
sensorEmitRate, 10–120 Hz in Settings → Accessories → IMU Sensor); automatically 5 Hz while no page shows the values (car view, sensor pages, OBD dashboards, or a home card bound to an IMU parameter). - Barometer: 5 Hz. Altitude is computed against standard sea level (1013.25 hPa), so it is a relative figure, not a surveyed elevation.
Startup sequence
- 1 s after the app starts, the manager checks
imuEnabledand opens/dev/i2c-2. - Initialises LSM6DSL, LIS3MDL and BMP388 (reading the barometer calibration).
- Emits "Connected" and starts the read thread.
- The thread first samples the gyroscope 200 times over ~1 s to measure its bias — keep the vehicle still for the first second after the status turns green, or the model will drift until the next restart or re-enable.
- The paced read loop begins; the first barometer read happens on the first iteration so altitude is available immediately.
Mounting orientation, ZERO and RESET
CarMenu next to the LIVE badge, or at the bottom of SensorMenu). The current tilt is
captured as a tare quaternion and the current accelerometer reading as the G-force zero, so the model sits flat and
lateral / longitudinal G read 0. RESET clears it. The tare is held in memory only: it is not saved
to settings and starts from identity on every launch, so re-zero after each start.
The sensor's Z axis should point up (or down) for the yaw-stripping to behave as intended: OCTAVE removes rotation
about the sensor Z axis and treats the remaining tilt as pitch/roll. The on-screen remap to Qt Quick 3D is
Qt.quaternion(w, y, z, x) in frontend/CarMenu.qml.
If it does not connect
Retries: 5 attempts with linear backoff (5, 10, 15, 20, 25 s), then "max retries reached, giving up". Toggle IMU Sensor off and on in Settings → Accessories to start again. Permission errors are not retried (see above). A bus that dies mid-run (a loose wire) shows as "Error" after five consecutive failed reads, followed by the same retry cycle. The status text on that page mirrors the last emitted state: "Connected", "Disconnected", "Error" or "Disabled".
PAJ7620U2 Gesture Sensor
Linux / Raspberry Pi / Orange PiOCTAVE maps nine PAJ7620U2 gestures in front of its IR emitter. OCTAVE polls it at ~50 Hz on a worker thread and turns each gesture into a media / volume action.
Wiring
PAJ7620U2 Single-board computer
+-----------+ +---------------------------+
| VCC |------------------| 3.3 V |
| GND |------------------| GND |
| SDA |------------------| SDA of the bus that is | /dev/i2c-2
| SCL |------------------| SCL exposed as i2c-2 | (shared with BerryIMU)
+-----------+ +---------------------------+
I2C address: 0x73 Chip ID: 0x7620 (verified at start)
The diagram uses a breakout board that accepts 3.3 V supply and logic. Check the pinout for the exact module: DFRobot SEN0315 and Waveshare PAJ7620U2 both document 3.3 V operation, but their connectors differ.
Gestures and default actions
| Gesture | Flag | Default action |
|---|---|---|
| RIGHT | reg 0x43 bit 0x01 | next_track |
| LEFT | reg 0x43 bit 0x02 | previous_track |
| UP | reg 0x43 bit 0x04 | volume_up |
| DOWN | reg 0x43 bit 0x08 | volume_down |
| FORWARD | reg 0x43 bit 0x10 | play_pause |
| BACKWARD | reg 0x43 bit 0x20 | mute_toggle |
| CLOCKWISE | reg 0x43 bit 0x40 | volume_up |
| COUNTER-CLOCKWISE | reg 0x43 bit 0x80 | volume_down |
| WAVE | reg 0x44 bit 0x01 | play_pause |
Remap any gesture in Settings → Accessories → Gesture Sensor → Mapping (actions:
Next Track, Previous Track, Volume Up, Volume Down, Play / Pause, Mute Toggle, Disabled). volume_up /
volume_down move by gestureVolumeStep (default 5 %, 1–100).
Timing
- Poll interval: 20 ms (~50 Hz).
- Cooldown:
gestureCooldown, default 300 ms (100–2000 ms in 50 ms steps) — gestures inside the window are ignored. - Forward / backward settle: 300 ms / 400 ms. An approach or retreat that turns out to be a swipe is re-read after the wait and reported as the swipe; otherwise as FORWARD / BACKWARD. These two gestures therefore respond a little later than swipes.
- Post-flush: 100 ms of read-and-discard after every accepted gesture, to reduce repeated triggers from the same motion.
Initialisation
Wake-up write to the bank-select register (0xEF ← 0x00, retried once after 50 ms), chip-ID check
(0x00/0x01 must read 0x7620), then the DFRobot reference table of 219
register writes across banks 0 and 1, then back to bank 0. A wrong chip ID is logged as
"expected chip ID 0x7620, got …" — a reading of 0x0000 or 0xFFFF
means the sensor is not answering (wiring, power, or wrong bus).
Mounting
gestureCooldown if a single motion triggers twice.
Verifying the Install (Log Lines)
Run with --debug for detailed logging. Watch octave-cpp.log (see Troubleshooting
for log locations). Logging categories: octave.esp32volume, octave.berryimu,
octave.gesture. A healthy start looks like this:
| Peripheral | Expect | Meaning |
|---|---|---|
| ESP32 | detected ESP32-S3 on /dev/ttyACM0 | VID 0x303A found in the scan |
connected to /dev/ttyACM0 | Port open; the read loop / readyRead handler is live | |
keepalive timer started (sleep disabled) or …NOT started (sleep enabled) | Reflects esp32LedSleepEnabled | |
| BerryIMU | all sensors initialized successfully | All three chips answered their init writes |
gyro calibrated - bias x=… y=… z=… dps | ~1 s after connect; keep the vehicle stationary during this measurement; a large bias alone does not identify the cause | |
first emit - pitch=… roll=… heading=… g=… q=[…] | First values reached QML; g should be near 1.0 at rest | |
baro - alt=…m temp=…C | Logged once on the first barometer read | |
| Gesture | PAJ7620U2 initialized (chip ID: 0x7620) | Chip present and configured |
RIGHT -> next_track (#1) | A gesture was accepted and its action dispatched |
On shutdown each manager logs "cleaning up..." and the I2C loops "read loop exited after N
reads" / "… after N gestures".
3D Printed Mounts
The hardware/ directory holds vehicle-specific mounting parts and an Orange Pi case.
2003 Jeep TJ media console
hardware/2003-jeep-tj/media-console/:
| File | Format | Description |
|---|---|---|
bracket-for-screen.STL | STL (print-ready) | Screen mounting bracket |
bracket-for-screen.SLDPRT | SolidWorks part | Source for the screen bracket |
media-console-bracket-mk2.STL | STL (print-ready) | Mk2 media-console bracket |
media-console-bracket-mk2.SLDPRT | SolidWorks part | Source for the Mk2 bracket |
Other checked-in parts:
| Directory | Files |
|---|---|
hardware/orange-pi-5-plus-case/ | opi5plus-case-with-magnets.STL and its .SLDPRT source |
hardware/2003-jeep-tj/glove-box-mount/ | glove-box-mount-base.STL, glove-box-mount-opi5-magnetic.STL and its .scad source |
No slicer profile is checked in; choose a filament rated for cabin heat. The Jeep TJ itself is also the vehicle
rendered on CarMenu (see Pages → CarMenu).
Reference Install: Orange Pi 5 Plus
The author's Jeep TJ runs OCTAVE on an Orange Pi 5 Plus (RK3588, 16 GB, NVMe) driving a 10.1-inch 1280×800 HDMI
touchscreen, under Armbian with the mainline kernel and Hyprland. The system and desktop config from that box is
checked in at deployment/orange-pi-5-plus/, laid out the way it sits on the box
(home/, etc/, boot/). Its README.md lists the full hardware and every file.
- Session: GDM autologin into Hyprland.
exec-once = ~/.local/bin/octave-start.shstarts OCTAVE, routes audio to the 3.5 mm jack and restarts OCTAVE after a crash (it stops after 5 crashes in 3 minutes). - Touch: the hyprgrass plugin for multi-finger gestures, plus a small GTK on-screen keyboard toggled from Waybar.
- Phone mirroring: a local
OCTAVE-Jeep5 GHz hotspot on a virtualap0interface (hostapd + DHCP-only dnsmasq, no gateway or DNS so the phone keeps mobile data). See Phone Mirror. - Reliability: hardware watchdog, reboot on kernel panic or oops, a persistent journal, and suspend disabled.
Secrets and device identifiers (hotspot password, MAC addresses, ADB port, disk UUID) are replaced with placeholders. Change the user name, paths and audio sink names to match your install.
Power & Vehicle Installation
General guidance for a permanent in-vehicle install; none of this is enforced by the software.
- SBC power: use a supply that meets the board's voltage and current requirements and is designed for the vehicle electrical environment. Sudden power loss can corrupt the boot medium; a power-hold circuit or UPS with a graceful-shutdown trigger can allow the OS to finish writing before power is removed. OCTAVE runs its cleanup (
esp32VolumeManager.cleanup(),berryIMU.cleanup(),gestureSensor.cleanup()) on SIGTERM / SIGINT as well as on window close, so a shutdown script that signals the process closes the ports cleanly. - Sensors: both I2C boards run from the SBC's 3.3 V rail.
- ESP32 dongle: USB-powered by the host. The encoder unit needs its own supply.
- Display: size its supply according to the display's requirements and the host's available power budget.
- I2C cable length: keep it short; I2C is not designed for long runs and the loop targets 200 samples per second, so marginal wiring shows up as read errors (a failed read skips the sample; five in a row restart the driver).
- Vibration and heat: strain-relieve connectors, keep electronics out of direct sun.
- Grounding: follow each board's power and signal-ground requirements; verify the complete installation independently of OCTAVE.
- OBD-II: unrelated to these sensors but shares the port list: a Bluetooth Classic ELM327 can use a bound RFCOMM serial port or a direct RFCOMM socket, selected in OBD settings (
obdBluetoothPort, empty by default); that port is hidden from the ESP32 picker. On Linux/Pi the easiest route is "Connect Bluetooth OBD2" on the empty OBD page (or "Find Bluetooth Adapters" in OBD settings): it scans, pairs with the adapter's PIN (1234 / 0000 / 6789 / 1111, answered by OCTAVE), trusts it and connects, with no MAC to type and nobluetoothctl. See Adapter Finder. For a manual direct connection, use the "Direct" button: enter the adapter's MAC and it connects over a Bluetooth socket (saved asrfcomm://<MAC>) with norfcomm bind, retrying every 10 s while the ignition is off. Pairing requirements depend on the adapter; the finder can try a trusted Classic connection even if pairing fails. See Direct Bluetooth. On the Android build the same OBD settings page lists the phone's paired Bluetooth devices as "<name> · <MAC>" once the Bluetooth permission has been granted on the first Connect tap, so the adapter is picked from the list rather than typed in.
Why It Is Set Up This Way
- A USB dongle instead of a wired knob. The manager's docstring describes the two-board design: the encoder is wireless over ESP-NOW and the ESP32-S3 dongle turns that into plain USB serial, so the knob can sit anywhere in the cabin and the host needs nothing but a serial port — which is also why the same code runs on Windows and macOS.
- Text lines, not a binary frame. One printable command per line keeps the firmware trivial and lets you test with any serial terminal (
echo -e "V50\n" > /dev/ttyACM0, or type+to the port). - Bus 2 hard-coded. The constants were set for the reference board and never needed to move; the price is the overlay step above. If you need another bus, it is a one-line change in each of the two driver files.
- Gyro bias measured at every start, tare on demand. Bias drifts with temperature, so it is re-measured each run; the mounting slope is a user decision, so it is a button rather than a config value, and it is deliberately not persisted.
- Retry, but not forever. Five linear-backoff attempts cover a bus that appears late at boot without hammering a bus that will never appear; a permission error is not retried because permissions must be corrected before another attempt.
- The driver, not the UI, decides the emit rate. Whatever page is showing, the sensor loop keeps the attitude current; only the rate of signals into QML changes, so the first frame after opening the car view is already right.