What Works Where

This page is the physical-side companion to Hardware Managers, which documents the driver code in src/managers/.

PeripheralLinux / Raspberry Pi / Orange PiWindows / macOSAndroid
ESP32 volume knob (USB serial)YesYesNo (stub)
BerryIMU v3 (I2C)YesNo — reports "Disconnected" and never retriesReplaced by the phone's own sensors (QtSensors)
PAJ7620U2 gesture sensor (I2C)YesNo — reports "Disconnected" and never retriesNo (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 macOS

Two 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
        
Firmware is not in this repository
There are no ESP32 sketches under the OCTAVE tree. The table below is the protocol OCTAVE implements (src/managers/esp32volumemanager.cpp); the wireless link and LED behaviour depend on compatible firmware.

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.

LineDirectionMeaning
+ or +NDongle → OCTAVEVolume up by 1 or N encoder ticks
- or -NDongle → OCTAVEVolume down by 1 or N ticks
MDongle → OCTAVEMute toggle (button press)
PDongle → OCTAVEReserved for play/pause (double-click); accepted and logged, no action
V<0-100>OCTAVE → DongleCurrent volume percent for the LED ring (debounced to at most 20/s; also used as the 2 s keepalive)
M0 / M1OCTAVE → DongleMute indicator off / on
C<r>,<g>,<b>OCTAVE → DongleLED 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").

First run: the port is picked for you
The 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() if esp32VolumeEnabled is true.
  • Unplug: detected on a serial error (QSerialPort::ResourceError, DeviceNotFoundError, PermissionError, ReadError, WriteError) or a keepalive write the port rejects. With esp32AutoReconnect on, 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 as C whenever 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 a V keepalive 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.

Make sure /dev/i2c-2 exists before launching
Which header pins carry i2c-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

ChipAddressFunctionConfigured as (register ← value)
LSM6DSL0x6AAccelerometer + gyroscope0x10 ← 0x68 (accel 416 Hz, ±4 g), 0x11 ← 0x60 (gyro 416 Hz, 245 dps), 0x12 ← 0x44 (BDU + auto-increment)
LIS3MDL0x1CMagnetometer (heading only)0x20 ← 0xFE (ultra-high performance, 155 Hz), 0x21 ← 0x20 (±8 gauss), 0x22 ← 0x00 (continuous), 0x23 ← 0x0C (ultra-high performance Z)
BMP3880x77Pressure + temperature21 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. 1 s after the app starts, the manager checks imuEnabled and opens /dev/i2c-2.
  2. Initialises LSM6DSL, LIS3MDL and BMP388 (reading the barometer calibration).
  3. Emits "Connected" and starts the read thread.
  4. 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.
  5. The paced read loop begins; the first barometer read happens on the first iteration so altitude is available immediately.

Mounting orientation, ZERO and RESET

Level the vehicle with ZERO
Mount the board rigidly with its Z axis aligned vertically, park on level ground, then press ZERO (on 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 Pi

OCTAVE 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

GestureFlagDefault action
RIGHTreg 0x43 bit 0x01next_track
LEFTreg 0x43 bit 0x02previous_track
UPreg 0x43 bit 0x04volume_up
DOWNreg 0x43 bit 0x08volume_down
FORWARDreg 0x43 bit 0x10play_pause
BACKWARDreg 0x43 bit 0x20mute_toggle
CLOCKWISEreg 0x43 bit 0x40volume_up
COUNTER-CLOCKWISEreg 0x43 bit 0x80volume_down
WAVEreg 0x44 bit 0x01play_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

Face the user, keep the view clear
Mount the sensor pointing at where the hand will be, with nothing between them, out of direct sunlight on the lens (it is an IR sensor). OCTAVE does not tune the sensor's range or sensitivity beyond the reference init table; if swipes register as FORWARD/BACKWARD or vice-versa, adjust the mounting distance rather than the software, and raise 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:

PeripheralExpectMeaning
ESP32detected ESP32-S3 on /dev/ttyACM0VID 0x303A found in the scan
connected to /dev/ttyACM0Port open; the read loop / readyRead handler is live
keepalive timer started (sleep disabled) or …NOT started (sleep enabled)Reflects esp32LedSleepEnabled
BerryIMUall sensors initialized successfullyAll 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=…CLogged once on the first barometer read
GesturePAJ7620U2 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/:

FileFormatDescription
bracket-for-screen.STLSTL (print-ready)Screen mounting bracket
bracket-for-screen.SLDPRTSolidWorks partSource for the screen bracket
media-console-bracket-mk2.STLSTL (print-ready)Mk2 media-console bracket
media-console-bracket-mk2.SLDPRTSolidWorks partSource for the Mk2 bracket

Other checked-in parts:

DirectoryFiles
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.sh starts 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-Jeep 5 GHz hotspot on a virtual ap0 interface (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 no bluetoothctl. 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 as rfcomm://<MAC>) with no rfcomm 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.