From 7a79e48378e5af8f742e4301303d8331e9da2199 Mon Sep 17 00:00:00 2001 From: Daniel Dybing Date: Wed, 14 Jan 2026 14:44:01 +0100 Subject: [PATCH] feat: Visual upgrades, Dallas sensor backend, and docs --- .gitignore | 17 + README.md | 154 ++++++++ TemperatureGauge.qml | 721 ++++++++++++++++++++++++++++++++++ build.sh | 35 ++ config.json | 4 + main.py | 172 ++++++++ main.qml | 63 +++ requirements.txt | 4 + run_pi.sh | 63 +++ verify_sensors.py | 58 +++ volvodisplay_splashscreen.kra | Bin 0 -> 51978 bytes 11 files changed, 1291 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 TemperatureGauge.qml create mode 100755 build.sh create mode 100644 config.json create mode 100644 main.py create mode 100644 main.qml create mode 100644 requirements.txt create mode 100644 run_pi.sh create mode 100644 verify_sensors.py create mode 100644 volvodisplay_splashscreen.kra diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..09bb192 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class + +# Virtual Environment +.venv/ +venv/ +env/ + +# Distribution / Build +build/ +dist/ +*.spec + +# Logs +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..fd7c579 --- /dev/null +++ b/README.md @@ -0,0 +1,154 @@ +# Volvo Display App + +## Setup + +1. Install Python dependencies: + ```bash + pip install -r requirements.txt + ``` + Or if using system packages: + ```bash + sudo apt install python3-pyside6 + ``` + +2. **System Requirements (Linux/Raspberry Pi):** + PySide6 (Qt 6.5+) requires `libxcb-cursor0` to be installed on Linux systems. + ```bash + sudo apt install libxcb-cursor0 + ``` + If you encounter other missing library errors, you may need additional Qt dependencies: + ```bash + sudo apt install libxcb-xinerama0 + ``` + +## Hardware Setup (Dallas DS18B20 Sensors) + +To use real temperature sensors, wire them to the Raspberry Pi as follows: + +| Sensor Wire | Raspberry Pi Pin | Note | +| :--- | :--- | :--- | +| **VCC (Red)** | **Pin 1 (3.3V)** | | +| **GND (Black)** | **Pin 6 (GND)** | | +| **DATA (Yellow)** | **Pin 7 (GPIO 4)** | **Requires 4.7kΩ Resistor between VCC and DATA** | + +**Enable 1-Wire**: +1. Run `sudo raspi-config` +2. Select **Interface Options** -> **1-Wire** -> **Yes**. +3. Reboot. + +### Option B: Separate Pins (Advanced) +If you prefer to connect sensors to separate pins (e.g., to distinguish them physically or avoid soldering), you can enable multiple 1-Wire buses. + +1. Open configuration: + ```bash + sudo nano /boot/config.txt + ``` +2. Add multiple overlay lines with defined pins (e.g., GPIO 4 and GPIO 17): + ```ini + dtoverlay=w1-gpio,gpiopin=4 + dtoverlay=w1-gpio,gpiopin=17 + ``` +3. Reboot. + +*Note: Sensors from all pins will still appear in the same list. Identification is done via their unique ID.* + +## Running the App + +Run the application with: +```bash +python3 main.py +``` + +### Headless / Framebuffer +If running on a Raspberry Pi without a desktop environment, you might need to specify the platform: +```bash +python3 main.py -platform linuxfb +``` +or +```bash +python3 main.py -platform eglfs +``` + +## Running on Headless Raspberry Pi + +If the application hangs or fails to open on the display, ensure you are using the correct platform plugin. + +Use the provided helper script: +```bash +chmod +x run_pi.sh +./run_pi.sh +``` + +Or manually: +```bash +export QT_QPA_PLATFORM=eglfs +python3 main.py +# or if using the compiled binary: +./dist/volvodisplay +``` + +### Troubleshooting "failed to load egl device" + +This error means the application cannot access the Direct Rendering Manager (DRM) device required for hardware acceleration (`eglfs`). + +1. **Check Permissions**: Ensure your user is part of the `render` and `video` groups. + ```bash + sudo usermod -aG render $USER + sudo usermod -aG video $USER + ``` + **You must reboot** after running these commands. + +2. **Use Software Rendering**: If you cannot get hardware acceleration working, use `linuxfb` (Linux Framebuffer). This uses the CPU. + ./run_pi.sh linuxfb + ``` + +3. **Check Boot Configuration**: + If you are running as root and still get EGL errors, ensure your Pi is using the KMS driver. + Check `/boot/config.txt` (or `/boot/firmware/config.txt`) and ensure this line is active: + ```ini + dtoverlay=vc4-kms-v3d + ``` + (or `dtoverlay=vc4-fkms-v3d` for legacy fake-kms) + +4. **Runtime Directory**: + Qt requires `XDG_RUNTIME_DIR` to be set. The `run_pi.sh` script now handles this automatically for root users. +```bash +export QT_QPA_PLATFORM=linuxfb +python3 main.py +``` + +## Compiling for Raspberry Pi + +### System Requirements for Build +PyInstaller needs to locate system libraries to bundle them. On a fresh Raspberry Pi OS (or Debian container), you likely need to install these: + +```bash +sudo apt update +sudo apt install -y \ + libxcb-cursor0 \ + libxcb-icccm4 \ + libxcb-keysyms1 \ + libxcb-render-util0 \ + libxcb-xkb1 \ + libxcb-image0 \ + libxkbcommon-x11-0 \ + libxkbcommon0 +``` + +### Build Instructions + +To create a single-file binary that runs on the Raspberry Pi: + +1. Ensure you are **on the Raspberry Pi** (or a system with the same architecture, e.g., ARM64). +2. Make the build script executable: + ```bash + chmod +x build.sh + ``` +3. Run the build script: + ```bash + ./build.sh + ``` +4. The executable will be located in the `dist` folder: + ```bash + ./dist/volvodisplay + ``` diff --git a/TemperatureGauge.qml b/TemperatureGauge.qml new file mode 100644 index 0000000..4b6b9d4 --- /dev/null +++ b/TemperatureGauge.qml @@ -0,0 +1,721 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Shapes + +Item { + id: root + width: 480 + height: 480 + + property real currentTemp: 0 // Bound in main.qml + onCurrentTempChanged: console.log("QML Left Temp Updated: " + currentTemp) + + property real currentRightTemp: 0 // Bound in main.qml + property bool startupComplete: true + + // Sweep Property for Startup - REMOVED + // property real sweepTemp: -15 + + // Startup Sweep Animation - REMOVED + // SequentialAnimation { ... } + + Component.onCompleted: { + forceActiveFocus() + root.startupComplete = true + } + + Rectangle { + id: background + anchors.fill: parent + // color: "#050505" // Old solid color + + // Gradient for depth (Vertical Linear) + gradient: Gradient { + GradientStop { position: 0.0; color: "black" } + GradientStop { position: 0.5; color: "#1a1a1a" } // Lighter center strip + GradientStop { position: 1.0; color: "black" } + } + } + + + // --- Left Gauge (Temperature) --- + Item { + id: leftGauge + anchors.fill: parent + + // Track Outline + Shape { + anchors.fill: parent + // Anti-aliasing for smoother lines + // layer.enabled: true + // layer.samples: 4 + // layer.smooth: true + + ShapePath { + strokeColor: "white" + strokeWidth: 2 + fillColor: "transparent" + capStyle: ShapePath.FlatCap + + // Outer Arc (145 to 215 degrees) + // 145 deg = 0.8055 PI + // 215 deg = 1.1944 PI + startX: 240 + 210 * Math.cos(Math.PI * (145/180)) + startY: 240 + 210 * Math.sin(Math.PI * (145/180)) + + PathArc { + x: 240 + 210 * Math.cos(Math.PI * (215/180)) + y: 240 + 210 * Math.sin(Math.PI * (215/180)) + radiusX: 210; radiusY: 210 + useLargeArc: false + } + + // Bottom Line connecting to inner + PathLine { + x: 240 + 160 * Math.cos(Math.PI * (215/180)) + y: 240 + 160 * Math.sin(Math.PI * (215/180)) + } + + // Inner Arc + PathArc { + x: 240 + 160 * Math.cos(Math.PI * (145/180)) + y: 240 + 160 * Math.sin(Math.PI * (145/180)) + radiusX: 160; radiusY: 160 + useLargeArc: false + direction: PathArc.Counterclockwise + } + + // Top Line connecting back to start + PathLine { + x: 240 + 210 * Math.cos(Math.PI * (145/180)) + y: 240 + 210 * Math.sin(Math.PI * (145/180)) + } + } + + + } + + // Indicator (Blue Bar) + Shape { + anchors.fill: parent + // layer.enabled: true + // layer.samples: 4 + + ShapePath { + strokeColor: "transparent" + fillColor: "#2196F3" // Blue + + // Range -15 to 0. + // -15 = 145 deg + // 0 = 171.25 deg + startX: 240 + 205 * Math.cos(Math.PI * (145/180)) + startY: 240 + 205 * Math.sin(Math.PI * (145/180)) + + PathArc { + x: 240 + 205 * Math.cos(Math.PI * (171.25/180)) + y: 240 + 205 * Math.sin(Math.PI * (171.25/180)) + radiusX: 205; radiusY: 205 + } + PathLine { + x: 240 + 165 * Math.cos(Math.PI * (171.25/180)) + y: 240 + 165 * Math.sin(Math.PI * (171.25/180)) + } + PathArc { + x: 240 + 165 * Math.cos(Math.PI * (145/180)) + y: 240 + 165 * Math.sin(Math.PI * (145/180)) + radiusX: 165; radiusY: 165 + direction: PathArc.Counterclockwise + } + PathLine { + x: 240 + 205 * Math.cos(Math.PI * (145/180)) + y: 240 + 205 * Math.sin(Math.PI * (145/180)) + } + } + } + + // Temperature Markers & Ticks + Shape { + anchors.fill: parent + // layer.enabled: true + // layer.samples: 4 + ShapePath { + strokeColor: "white" + strokeWidth: 2 + fillColor: "transparent" + + // -15 @ 145 deg + startX: 240 + 210 * Math.cos(Math.PI * (145/180)) + startY: 240 + 210 * Math.sin(Math.PI * (145/180)) + PathLine { x: 240 + 215 * Math.cos(Math.PI * (145/180)); y: 240 + 215 * Math.sin(Math.PI * (145/180)) } + + // -10 @ 153.75 deg + PathMove { x: 240 + 210 * Math.cos(Math.PI * (153.75/180)); y: 240 + 210 * Math.sin(Math.PI * (153.75/180)) } + PathLine { x: 240 + 215 * Math.cos(Math.PI * (153.75/180)); y: 240 + 215 * Math.sin(Math.PI * (153.75/180)) } + + // -5 @ 162.5 deg + PathMove { x: 240 + 210 * Math.cos(Math.PI * (162.5/180)); y: 240 + 210 * Math.sin(Math.PI * (162.5/180)) } + PathLine { x: 240 + 215 * Math.cos(Math.PI * (162.5/180)); y: 240 + 215 * Math.sin(Math.PI * (162.5/180)) } + + // 0 @ 171.25 deg + PathMove { x: 240 + 210 * Math.cos(Math.PI * (171.25/180)); y: 240 + 210 * Math.sin(Math.PI * (171.25/180)) } + PathLine { x: 240 + 215 * Math.cos(Math.PI * (171.25/180)); y: 240 + 215 * Math.sin(Math.PI * (171.25/180)) } + + // 5 @ 180 deg + PathMove { x: 240 + 210 * Math.cos(Math.PI * 1.0); y: 240 + 210 * Math.sin(Math.PI * 1.0) } + PathLine { x: 240 + 215 * Math.cos(Math.PI * 1.0); y: 240 + 215 * Math.sin(Math.PI * 1.0) } + + // 10 @ 188.75 deg + PathMove { x: 240 + 210 * Math.cos(Math.PI * (188.75/180)); y: 240 + 210 * Math.sin(Math.PI * (188.75/180)) } + PathLine { x: 240 + 215 * Math.cos(Math.PI * (188.75/180)); y: 240 + 215 * Math.sin(Math.PI * (188.75/180)) } + + // 15 @ 197.5 deg + PathMove { x: 240 + 210 * Math.cos(Math.PI * (197.5/180)); y: 240 + 210 * Math.sin(Math.PI * (197.5/180)) } + PathLine { x: 240 + 215 * Math.cos(Math.PI * (197.5/180)); y: 240 + 215 * Math.sin(Math.PI * (197.5/180)) } + + // 20 @ 206.25 deg + PathMove { x: 240 + 210 * Math.cos(Math.PI * (206.25/180)); y: 240 + 210 * Math.sin(Math.PI * (206.25/180)) } + PathLine { x: 240 + 215 * Math.cos(Math.PI * (206.25/180)); y: 240 + 215 * Math.sin(Math.PI * (206.25/180)) } + + // 25 @ 215 deg + PathMove { x: 240 + 210 * Math.cos(Math.PI * (215/180)); y: 240 + 210 * Math.sin(Math.PI * (215/180)) } + PathLine { x: 240 + 215 * Math.cos(Math.PI * (215/180)); y: 240 + 215 * Math.sin(Math.PI * (215/180)) } + } + } + + // -15 @ 145 deg + Text { + text: "-15" + color: "white" + font.pixelSize: 20 + font.family: "Arial" + font.bold: true + x: 240 + 230 * Math.cos(Math.PI * (145/180)) - width/2 + y: 240 + 230 * Math.sin(Math.PI * (145/180)) - height/2 + } + // -10 @ 153.75 deg + Text { + text: "-10" + color: "white" + font.pixelSize: 20 + font.family: "Arial" + font.bold: true + x: 240 + 230 * Math.cos(Math.PI * (153.75/180)) - width/2 + y: 240 + 230 * Math.sin(Math.PI * (153.75/180)) - height/2 + } + // -5 @ 162.5 deg + Text { + text: "-5" + color: "white" + font.pixelSize: 20 + font.family: "Arial" + font.bold: true + x: 240 + 230 * Math.cos(Math.PI * (162.5/180)) - width/2 + y: 240 + 230 * Math.sin(Math.PI * (162.5/180)) - height/2 + } + // 0 @ 171.25 deg + Text { + text: "0" + color: "white" + font.pixelSize: 20 + font.family: "Arial" + font.bold: true + x: 240 + 230 * Math.cos(Math.PI * (171.25/180)) - width/2 + y: 240 + 230 * Math.sin(Math.PI * (171.25/180)) - height/2 + } + // 5 @ 180 deg + Text { + text: "5" + color: "white" + font.pixelSize: 20 + font.family: "Arial" + font.bold: true + x: 240 + 230 * Math.cos(Math.PI * 1.0) - width/2 + y: 240 + 230 * Math.sin(Math.PI * 1.0) - height/2 + } + // 10 @ 188.75 deg + Text { + text: "10" + color: "white" + font.pixelSize: 20 + font.family: "Arial" + font.bold: true + x: 240 + 230 * Math.cos(Math.PI * (188.75/180)) - width/2 + y: 240 + 230 * Math.sin(Math.PI * (188.75/180)) - height/2 + } + // 15 @ 197.5 deg + Text { + text: "15" + color: "white" + font.pixelSize: 20 + font.family: "Arial" + font.bold: true + x: 240 + 230 * Math.cos(Math.PI * (197.5/180)) - width/2 + y: 240 + 230 * Math.sin(Math.PI * (197.5/180)) - height/2 + } + // 20 @ 206.25 deg + Text { + text: "20" + color: "white" + font.pixelSize: 20 + font.family: "Arial" + font.bold: true + x: 240 + 230 * Math.cos(Math.PI * (206.25/180)) - width/2 + y: 240 + 230 * Math.sin(Math.PI * (206.25/180)) - height/2 + } + // 25 @ 215 deg + Text { + text: "25" + color: "white" + font.pixelSize: 20 + font.family: "Arial" + font.bold: true + x: 240 + 230 * Math.cos(Math.PI * (215/180)) - width/2 + y: 240 + 230 * Math.sin(Math.PI * (215/180)) - height/2 + } + + + + + + + } + + // --- Right Gauge (Mirrored) --- + Item { + id: rightGauge + anchors.fill: parent + + // Track Outline + Shape { + anchors.fill: parent + // layer.enabled: true + // layer.samples: 4 + // layer.smooth: true + + ShapePath { + strokeColor: "white" + strokeWidth: 2 + fillColor: "transparent" + capStyle: ShapePath.FlatCap + + // Outer Arc (35 to -35 degrees) + // 35 deg = 0.1944 PI + // -35 deg = 325 deg = 1.8055 PI + + startX: 240 + 210 * Math.cos(Math.PI * (325/180)) + startY: 240 + 210 * Math.sin(Math.PI * (325/180)) + + PathArc { + x: 240 + 210 * Math.cos(Math.PI * (35/180)) + y: 240 + 210 * Math.sin(Math.PI * (35/180)) + radiusX: 210; radiusY: 210 + useLargeArc: false + } + + // Bottom Line connecting to inner + PathLine { + x: 240 + 160 * Math.cos(Math.PI * (35/180)) + y: 240 + 160 * Math.sin(Math.PI * (35/180)) + } + + // Inner Arc + PathArc { + x: 240 + 160 * Math.cos(Math.PI * (325/180)) + y: 240 + 160 * Math.sin(Math.PI * (325/180)) + radiusX: 160; radiusY: 160 + useLargeArc: false + direction: PathArc.Counterclockwise + } + + // Top Line connecting back to start + PathLine { + x: 240 + 210 * Math.cos(Math.PI * (325/180)) + y: 240 + 210 * Math.sin(Math.PI * (325/180)) + } + } + + + } + + // Indicator (Blue Bar) + Shape { + anchors.fill: parent + // layer.enabled: true + // layer.samples: 4 + + ShapePath { + strokeColor: "transparent" + fillColor: "#2196F3" // Blue + + // Range -15 to 0. + // -15 = 35 deg + // 0 = 8.75 deg + startX: 240 + 205 * Math.cos(Math.PI * (35/180)) + startY: 240 + 205 * Math.sin(Math.PI * (35/180)) + + PathArc { + x: 240 + 205 * Math.cos(Math.PI * (8.75/180)) + y: 240 + 205 * Math.sin(Math.PI * (8.75/180)) + radiusX: 205; radiusY: 205 + direction: PathArc.Counterclockwise + } + PathLine { + x: 240 + 165 * Math.cos(Math.PI * (8.75/180)) + y: 240 + 165 * Math.sin(Math.PI * (8.75/180)) + } + PathArc { + x: 240 + 165 * Math.cos(Math.PI * (35/180)) + y: 240 + 165 * Math.sin(Math.PI * (35/180)) + radiusX: 165; radiusY: 165 + } + PathLine { + x: 240 + 205 * Math.cos(Math.PI * (35/180)) + y: 240 + 205 * Math.sin(Math.PI * (35/180)) + } + } + } + + // Markers & Ticks + Shape { + anchors.fill: parent + // layer.enabled: true + // layer.samples: 4 + ShapePath { + strokeColor: "white" + strokeWidth: 2 + fillColor: "transparent" + + // -15 @ 35 deg + startX: 240 + 210 * Math.cos(Math.PI * (35/180)) + startY: 240 + 210 * Math.sin(Math.PI * (35/180)) + PathLine { x: 240 + 215 * Math.cos(Math.PI * (35/180)); y: 240 + 215 * Math.sin(Math.PI * (35/180)) } + + // -10 @ 26.25 deg + PathMove { x: 240 + 210 * Math.cos(Math.PI * (26.25/180)); y: 240 + 210 * Math.sin(Math.PI * (26.25/180)) } + PathLine { x: 240 + 215 * Math.cos(Math.PI * (26.25/180)); y: 240 + 215 * Math.sin(Math.PI * (26.25/180)) } + + // -5 @ 17.5 deg + PathMove { x: 240 + 210 * Math.cos(Math.PI * (17.5/180)); y: 240 + 210 * Math.sin(Math.PI * (17.5/180)) } + PathLine { x: 240 + 215 * Math.cos(Math.PI * (17.5/180)); y: 240 + 215 * Math.sin(Math.PI * (17.5/180)) } + + // 0 @ 8.75 deg + PathMove { x: 240 + 210 * Math.cos(Math.PI * (8.75/180)); y: 240 + 210 * Math.sin(Math.PI * (8.75/180)) } + PathLine { x: 240 + 215 * Math.cos(Math.PI * (8.75/180)); y: 240 + 215 * Math.sin(Math.PI * (8.75/180)) } + + // 5 @ 0 deg + PathMove { x: 240 + 210 * Math.cos(0); y: 240 + 210 * Math.sin(0) } + PathLine { x: 240 + 215 * Math.cos(0); y: 240 + 215 * Math.sin(0) } + + // 10 @ -8.75 deg + PathMove { x: 240 + 210 * Math.cos(Math.PI * (-8.75/180)); y: 240 + 210 * Math.sin(Math.PI * (-8.75/180)) } + PathLine { x: 240 + 215 * Math.cos(Math.PI * (-8.75/180)); y: 240 + 215 * Math.sin(Math.PI * (-8.75/180)) } + + // 15 @ -17.5 deg + PathMove { x: 240 + 210 * Math.cos(Math.PI * (-17.5/180)); y: 240 + 210 * Math.sin(Math.PI * (-17.5/180)) } + PathLine { x: 240 + 215 * Math.cos(Math.PI * (-17.5/180)); y: 240 + 215 * Math.sin(Math.PI * (-17.5/180)) } + + // 20 @ -26.25 deg + PathMove { x: 240 + 210 * Math.cos(Math.PI * (-26.25/180)); y: 240 + 210 * Math.sin(Math.PI * (-26.25/180)) } + PathLine { x: 240 + 215 * Math.cos(Math.PI * (-26.25/180)); y: 240 + 215 * Math.sin(Math.PI * (-26.25/180)) } + + // 25 @ -35 deg + PathMove { x: 240 + 210 * Math.cos(Math.PI * (-35/180)); y: 240 + 210 * Math.sin(Math.PI * (-35/180)) } + PathLine { x: 240 + 215 * Math.cos(Math.PI * (-35/180)); y: 240 + 215 * Math.sin(Math.PI * (-35/180)) } + } + } + + // -15 @ 35 deg + Text { + text: "-15" + color: "white" + font.pixelSize: 20 + font.family: "Arial" + font.bold: true + x: 240 + 230 * Math.cos(Math.PI * (35/180)) - width/2 + y: 240 + 230 * Math.sin(Math.PI * (35/180)) - height/2 + } + // -10 @ 26.25 deg + Text { + text: "-10" + color: "white" + font.pixelSize: 20 + font.family: "Arial" + font.bold: true + x: 240 + 230 * Math.cos(Math.PI * (26.25/180)) - width/2 + y: 240 + 230 * Math.sin(Math.PI * (26.25/180)) - height/2 + } + // -5 @ 17.5 deg + Text { + text: "-5" + color: "white" + font.pixelSize: 20 + font.family: "Arial" + font.bold: true + x: 240 + 230 * Math.cos(Math.PI * (17.5/180)) - width/2 + y: 240 + 230 * Math.sin(Math.PI * (17.5/180)) - height/2 + } + // 0 @ 8.75 deg + Text { + text: "0" + color: "white" + font.pixelSize: 20 + font.family: "Arial" + font.bold: true + x: 240 + 230 * Math.cos(Math.PI * (8.75/180)) - width/2 + y: 240 + 230 * Math.sin(Math.PI * (8.75/180)) - height/2 + } + // 5 @ 0 deg + Text { + text: "5" + color: "white" + font.pixelSize: 20 + font.family: "Arial" + font.bold: true + x: 240 + 230 * Math.cos(0) - width/2 + y: 240 + 230 * Math.sin(0) - height/2 + } + // 10 @ -8.75 deg + Text { + text: "10" + color: "white" + font.pixelSize: 20 + font.family: "Arial" + font.bold: true + x: 240 + 230 * Math.cos(Math.PI * (-8.75/180)) - width/2 + y: 240 + 230 * Math.sin(Math.PI * (-8.75/180)) - height/2 + } + // 15 @ -17.5 deg + Text { + text: "15" + color: "white" + font.pixelSize: 20 + font.family: "Arial" + font.bold: true + x: 240 + 230 * Math.cos(Math.PI * (-17.5/180)) - width/2 + y: 240 + 230 * Math.sin(Math.PI * (-17.5/180)) - height/2 + } + // 20 @ -26.25 deg + Text { + text: "20" + color: "white" + font.pixelSize: 20 + font.family: "Arial" + font.bold: true + x: 240 + 230 * Math.cos(Math.PI * (-26.25/180)) - width/2 + y: 240 + 230 * Math.sin(Math.PI * (-26.25/180)) - height/2 + } + // 25 @ -35 deg + Text { + text: "25" + color: "white" + font.pixelSize: 20 + font.family: "Arial" + font.bold: true + x: 240 + 230 * Math.cos(Math.PI * (-35/180)) - width/2 + y: 240 + 230 * Math.sin(Math.PI * (-35/180)) - height/2 + } + + } + + // --- Center Cover Shape --- + Shape { + anchors.fill: parent + // layer.enabled: true + // layer.samples: 4 + z: 1 // Above background, below text/needles + + ShapePath { + strokeColor: "#333333" // Subtle outline + strokeWidth: 1 + fillColor: "#111111" // Slightly lighter than black + + // Top Arc (225 to 315 degrees) + startX: 240 + 210 * Math.cos(Math.PI * 1.25) // 225 deg + startY: 240 + 210 * Math.sin(Math.PI * 1.25) + + PathArc { + x: 240 + 210 * Math.cos(Math.PI * 1.75) // 315 deg + y: 240 + 210 * Math.sin(Math.PI * 1.75) + radiusX: 210; radiusY: 210 + } + + // Right Side (Straight lines) + // Line to Top-Right Waist + PathLine { + x: 240 + 90 + y: 240 - 80 + } + // Vertical Line down + PathLine { + x: 240 + 90 + y: 240 + 80 + } + // Line to Bottom Arc Start (45 deg) + PathLine { + x: 240 + 210 * Math.cos(Math.PI * 0.25) + y: 240 + 210 * Math.sin(Math.PI * 0.25) + } + + // Bottom Arc (45 to 135 degrees) + PathArc { + x: 240 + 210 * Math.cos(Math.PI * 0.75) // 135 deg + y: 240 + 210 * Math.sin(Math.PI * 0.75) + radiusX: 210; radiusY: 210 + } + + // Left Side (Straight lines) + // Line to Bottom-Left Waist + PathLine { + x: 240 - 90 + y: 240 + 80 + } + // Vertical Line up + PathLine { + x: 240 - 90 + y: 240 - 80 + } + // Line back to Top Arc Start (225 deg) + PathLine { + x: 240 + 210 * Math.cos(Math.PI * 1.25) + y: 240 + 210 * Math.sin(Math.PI * 1.25) + } + } + + // Inner Glow / Highlight on the cover + ShapePath { + strokeColor: "transparent" + fillColor: "transparent" // Placeholder for an effect if needed + } + } + + // --- Center Details --- + Text { + text: "VOLVO" + font.pixelSize: 28 + font.bold: true + font.family: "Serif" + color: "white" + anchors.bottom: parent.bottom + anchors.bottomMargin: 100 + anchors.horizontalCenter: parent.horizontalCenter + z: 2 // Ensure on top of cover + } + + // Unit Symbol (Moved to root to be above center cover) + Text { + text: "°C" + color: "orange" + font.pixelSize: 28 + font.family: "Arial" + font.bold: true + z: 10 // Above center cover + anchors.centerIn: parent + anchors.verticalCenterOffset: -40 // Above center + } + + // --- Needles --- + + component GaugeNeedle : Item { + id: needleComponent + property real angle: 0 + property color needleColor: "#FF6600" + + x: 240 + y: 240 + width: 1 + height: 1 + + // Glow/Shadow effect behind needle + Rectangle { + width: 180 + height: 12 + color: needleComponent.needleColor + opacity: 0.3 + radius: 6 + y: -height / 2 + x: 0 + transform: Rotation { + origin.x: 0 + origin.y: 6 + angle: needleComponent.angle + } + } + + Rectangle { + id: needleRect + width: 180 + height: 4 // Thinner for sharper look + color: needleComponent.needleColor + radius: 2 + antialiasing: true + + y: -height / 2 + x: 0 // Starts at center and extends outwards + + transform: Rotation { + origin.x: 0 + origin.y: needleRect.height / 2 + angle: needleComponent.angle + } + } + } + + function getAngleFromTemp(temp) { + // Range -15 to 25 (40 deg range) + // Span 70 degrees (145 to 215) + // 70 / 40 = 1.75 degrees per unit + return 145 + (temp + 15) * 1.75 + } + + function getRightAngleFromTemp(temp) { + // Range -15 to 25 + // Span 70 degrees (35 to -35) + return 35 - (temp + 15) * 1.75 + } + + // Keyboard Control + focus: true + Keys.onUpPressed: { + if (root.startupComplete) { + root.currentTemp = Math.min(root.currentTemp + 2, 25) + } + } + Keys.onDownPressed: { + if (root.startupComplete) { + root.currentTemp = Math.max(root.currentTemp - 2, -15) + } + } + Keys.onPressed: (event) => { + if (!root.startupComplete) return; + + if (event.key === Qt.Key_PageUp) { + root.currentRightTemp = Math.min(root.currentRightTemp + 2, 25) + event.accepted = true + } else if (event.key === Qt.Key_PageDown) { + root.currentRightTemp = Math.max(root.currentRightTemp - 2, -15) + event.accepted = true + } + } + + + + GaugeNeedle { + id: leftNeedle + // If startup complete, use real temp. Else use sweep temp. + angle: root.getAngleFromTemp(root.startupComplete ? root.currentTemp : -15) + + Behavior on angle { + // Slower, smoother damping for a "heavy" gauge feel + SpringAnimation { spring: 2; damping: 0.2; epsilon: 0.1 } + } + } + + GaugeNeedle { + id: rightNeedle + angle: root.getRightAngleFromTemp(root.startupComplete ? root.currentRightTemp : -15) + + Behavior on angle { + SpringAnimation { spring: 2; damping: 0.2; epsilon: 0.1 } + } + } +} diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..aa1e255 --- /dev/null +++ b/build.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# Source virtual environment if it exists +if [ -d ".venv" ]; then + source .venv/bin/activate +elif [ -d "venv" ]; then + source venv/bin/activate +fi + +# Install dependencies if not already installed +pip install -r requirements.txt + +# Run PyInstaller +# --name: Name of the binary +# --onefile: Create a single executable file +# --windowed: No console window (useful for GUI apps) +# --add-data: Include main.qml in the binary +# --clean: Clean cache +# --noconfirm: overwrite existing build directory + +echo "Building volvo_display binary..." + +pyinstaller --name "volvodisplay" \ + --onefile \ + --windowed \ + --clean \ + --noconfirm \ + --add-data "main.qml:." \ + --add-data "TemperatureGauge.qml:." \ + --exclude-module PySide6.QtWebEngineQuick \ + --exclude-module PySide6.QtWebEngineCore \ + --exclude-module PySide6.QtQuick3DSpatialAudio \ + main.py + +echo "Build complete. Binary is in 'dist/volvodisplay'" diff --git a/config.json b/config.json new file mode 100644 index 0000000..e834325 --- /dev/null +++ b/config.json @@ -0,0 +1,4 @@ +{ + "left_sensor_id": "020391774d86", + "right_sensor_id": "0213138491aa" +} \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..47b4b03 --- /dev/null +++ b/main.py @@ -0,0 +1,172 @@ +import sys +import os +import json +import math +from PySide6.QtGui import QGuiApplication +from PySide6.QtQml import QQmlApplicationEngine +from PySide6.QtCore import QObject, Property, Signal, Slot, QTimer + +# Try importing w1thermsensor, handle failure for dev environments +try: + from w1thermsensor import W1ThermSensor, Sensor + HAS_W1 = True +except ImportError: + HAS_W1 = False + print("Warning: w1thermsensor not found. Running in simulation mode.") + +class Backend(QObject): + leftTempChanged = Signal() + rightTempChanged = Signal() + + def __init__(self, demo_mode=False): + super().__init__() + self.demo_mode = demo_mode + self._left_temp = 0.0 + self._right_temp = 0.0 + + self.left_sensor = None + self.right_sensor = None + + # Load Config + self.config = self.load_config() + + # Initialize Sensors + self.init_sensors() + + # Timer for polling + self.timer = QTimer() + self.timer.timeout.connect(self.update_temps) + self.timer.start(2000) # Poll every 2 seconds + + def load_config(self): + config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.json") + if os.path.exists(config_path): + try: + with open(config_path, 'r') as f: + return json.load(f) + except Exception as e: + print(f"Error loading config: {e}") + return {} + + def init_sensors(self): + if not HAS_W1: + print("W1ThermSensor not installed.") + return + + print(f"Loading sensors using config: {self.config}") + try: + available_sensors = W1ThermSensor.get_available_sensors() + print(f"Available W1 sensors: {[s.id for s in available_sensors]}") + + # Map Left Sensor + left_id = self.config.get("left_sensor_id") + if left_id: + try: + # Check if ID exists in available to be sure + found = next((s for s in available_sensors if s.id == left_id), None) + if found: + self.left_sensor = found + print(f"✅ Successfully bound Left Sensor to {left_id}") + else: + print(f"❌ Configured Left Sensor {left_id} not found in available list!") + # Try direct init anyway just in case + self.left_sensor = W1ThermSensor(sensor_id=left_id) + except Exception as e: + print(f"❌ Failed to bind Left Sensor {left_id}: {e}") + + # Auto-assign fallback + if not self.left_sensor and len(available_sensors) > 0: + self.left_sensor = available_sensors[0] + print(f"⚠️ Auto-assigned Left Sensor to {self.left_sensor.id}") + + # Map Right Sensor + right_id = self.config.get("right_sensor_id") + if right_id: + try: + self.right_sensor = W1ThermSensor(sensor_id=right_id) + print(f"Bound Right Sensor to {right_id}") + except Exception as e: + print(f"Failed to bind Right Sensor {right_id}: {e}") + + except Exception as e: + print(f"Error initializing sensors: {e}") + + def update_temps(self): + # Simulation counter + if not hasattr(self, '_sim_step'): + self._sim_step = 0 + self._sim_step += 0.1 + + # Left + if self.left_sensor: + try: + t = self.left_sensor.get_temperature() + print(f"Left Sensor ({self.left_sensor.id}) Read: {t}°C") + self._set_left_temp(t) + except Exception as e: + print(f"Error reading left sensor: {e}") + elif self.demo_mode: + # Simulation + print("Left Sensor Missing - Simulating...") + sim_val = 5 + 10 * math.sin(self._sim_step) + self._set_left_temp(sim_val) + + # Right + if self.right_sensor: + try: + t = self.right_sensor.get_temperature() + self._set_right_temp(t) + except Exception as e: + print(f"Error reading right sensor: {e}") + elif self.demo_mode: + # Simulation + sim_val = 10 + 10 * math.cos(self._sim_step) + self._set_right_temp(sim_val) + + @Property(float, notify=leftTempChanged) + def leftTemp(self): + return self._left_temp + + def _set_left_temp(self, val): + if abs(self._left_temp - val) > 0.1: + self._left_temp = val + self.leftTempChanged.emit() + + @Property(float, notify=rightTempChanged) + def rightTemp(self): + return self._right_temp + + def _set_right_temp(self, val): + if abs(self._right_temp - val) > 0.1: + self._right_temp = val + self.rightTempChanged.emit() + +def main(): + # Redirect output to log.txt + sys.stdout = open("log.txt", "w", buffering=1) + sys.stderr = sys.stdout + print("--- Volvo Display Log ---") + + # Check for demo mode + demo_mode = "--demo" in sys.argv + if demo_mode: + print("Starting in DEMO MODE (Simulation Enabled)") + + app = QGuiApplication(sys.argv) + engine = QQmlApplicationEngine() + + backend = Backend(demo_mode=demo_mode) + engine.rootContext().setContextProperty("backend", backend) + + # Get absolute path to main.qml + current_dir = os.path.dirname(os.path.abspath(__file__)) + qml_file = os.path.join(current_dir, "main.qml") + + engine.load(qml_file) + if not engine.rootObjects(): + sys.exit(-1) + + sys.exit(app.exec()) + +if __name__ == "__main__": + main() diff --git a/main.qml b/main.qml new file mode 100644 index 0000000..ddd8f18 --- /dev/null +++ b/main.qml @@ -0,0 +1,63 @@ +import QtQuick +import QtQuick.Controls + +Window { + width: 480 + height: 480 + visible: true + title: "Volvo Display" + + // For a circular display, we might want to hide the window frame + // if running full screen, but for now we keep it standard. + // flags: Qt.FramelessWindowHint + + + + Loader { + id: mainLoader + anchors.fill: parent + sourceComponent: introComponent + onStatusChanged: { + if (status === Loader.Ready) console.log("Loader: Loaded " + source) + if (status === Loader.Error) console.log("Loader: Error loading " + source) + } + + Binding { + target: mainLoader.item + property: "currentTemp" + value: backend.leftTemp + when: mainLoader.status === Loader.Ready && mainLoader.source.toString().includes("TemperatureGauge.qml") + } + Binding { + target: mainLoader.item + property: "currentRightTemp" + value: backend.rightTemp + when: mainLoader.status === Loader.Ready && mainLoader.source.toString().includes("TemperatureGauge.qml") + } + } + + Component { + id: introComponent + Rectangle { + id: bg + anchors.fill: parent + color: "white" + + Text { + anchors.centerIn: parent + text: "Hello Pi" + color: "black" + } + } + } + + Timer { + interval: 5000 + running: true + repeat: false + onTriggered: { + console.log("Timer triggered. Switching to TemperatureGauge.qml") + mainLoader.source = "TemperatureGauge.qml" + } + } +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f02e31e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +PySide6 +pyinstaller +w1thermsensor + diff --git a/run_pi.sh b/run_pi.sh new file mode 100644 index 0000000..0f02856 --- /dev/null +++ b/run_pi.sh @@ -0,0 +1,63 @@ +#!/bin/bash + + +# Ensure we are in the script's directory +cd "$(dirname "$0")" + +# Activate venv if it exists +if [ -f "venv/bin/activate" ]; then + source venv/bin/activate +fi + +# Configuration for Raspberry Pi Headless Display (EGLFS/KMS) + +# Default to eglfs, but allow override +# Usage: ./run_pi.sh [platform] +# Example: ./run_pi.sh linuxfb + +# Initialize from argument or default +# Check if running under X11 (DISPLAY is set) +if [ -n "$DISPLAY" ]; then + echo "X11 environment detected (DISPLAY=$DISPLAY). Using xcb." + TARGET_PLATFORM="xcb" +else + # If DISPLAY is not set, use the provided argument or default to linuxfb + TARGET_PLATFORM=${1:-linuxfb} +fi + +# Diagnostic: Check for DRI devices +if [ -z "$(ls /dev/dri/card* 2>/dev/null)" ] && [ "$TARGET_PLATFORM" == "eglfs" ]; then + echo "WARNING: /dev/dri/card0 not found. Hardware acceleration (EGLFS) will likely fail." + echo "Check /boot/config.txt for 'dtoverlay=vc4-kms-v3d'." + echo "Switching to linuxfb..." + TARGET_PLATFORM=linuxfb +fi + +if [ "$TARGET_PLATFORM" == "linuxfb" ]; then + export QSG_RENDER_LOOP=basic +fi + +export QT_QPA_PLATFORM=$TARGET_PLATFORM + + +# Fix for "EGLFS: Failed to open /dev/dri/card0" or similar when running as root/headless +# Qt needs a runtime directory +if [ -z "$XDG_RUNTIME_DIR" ]; then + export XDG_RUNTIME_DIR=/tmp/runtime-root + mkdir -p $XDG_RUNTIME_DIR + chmod 700 $XDG_RUNTIME_DIR +fi + +# For Raspberry Pi 4 with KMS (vc4-kms-v3d) +export QT_QPA_EGLFS_INTEGRATION=eglfs_kms +export QT_QPA_EGLFS_ALWAYS_SET_MODE=1 +# export QT_LOGGING_RULES=qt.qpa.*=true +# export QSGRENDERER_DEBUG=1 + +echo "Starting Volvo Display with platform: $TARGET_PLATFORM" + +if [ -f "./dist/volvodisplay" ]; then + ./dist/volvodisplay +else + python3 main.py +fi diff --git a/verify_sensors.py b/verify_sensors.py new file mode 100644 index 0000000..0d6b053 --- /dev/null +++ b/verify_sensors.py @@ -0,0 +1,58 @@ +import time +import sys + +try: + from w1thermsensor import W1ThermSensor +except ImportError: + print("Error: w1thermsensor module not found.") + print("Please install it using: pip install w1thermsensor") + sys.exit(1) + +def main(): + print("--- 1-Wire Temperature Sensor Verifier ---") + print("Checking for connected sensors...") + + try: + sensors = W1ThermSensor.get_available_sensors() + except Exception as e: + print(f"Error accessing 1-Wire bus: {e}") + print("Ensure 1-Wire is enabled in raspi-config or /boot/config.txt") + return + + if not sensors: + print("No sensors found!") + print("Check your wiring:") + print(" 1. VCC (Red) -> Pin 1 (3.3V)") + print(" 2. GND (Black) -> Pin 6 (GND)") + print(" 3. DATA (Yellow) -> Pin 7 (GPIO 4)") + print(" * Remember the 4.7k resistor between VCC and DATA") + return + + print(f"Found {len(sensors)} sensor(s):") + for i, sensor in enumerate(sensors): + try: + print(f" {i+1}: ID={sensor.id} (Type={getattr(sensor, 'type_name', 'Unknown')})") + except Exception as e: + print(f" {i+1}: Error reading sensor details: {e}") + print(f" Debug info: {dir(sensor)}") + + print("\nReading temperatures (Press Ctrl+C to stop)...") + try: + while True: + output = [] + for sensor in sensors: + try: + temp = sensor.get_temperature() + output.append(f"[{sensor.id}]: {temp:.1f}°C") + except Exception as e: + output.append(f"[{sensor.id}]: Error") + + # Print on same line to act like a dashboard + print(" | ".join(output), end="\r") + sys.stdout.flush() + time.sleep(1) + except KeyboardInterrupt: + print("\nStopped.") + +if __name__ == "__main__": + main() diff --git a/volvodisplay_splashscreen.kra b/volvodisplay_splashscreen.kra new file mode 100644 index 0000000000000000000000000000000000000000..16181ca569b2f6ababd5631788a84ce8f73fe9fb GIT binary patch literal 51978 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 literal 0 HcmV?d00001