feat: Visual upgrades, Dallas sensor backend, and docs
This commit is contained in:
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# Virtual Environment
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
|
||||
# Distribution / Build
|
||||
build/
|
||||
dist/
|
||||
*.spec
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
154
README.md
Normal file
154
README.md
Normal file
@@ -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
|
||||
```
|
||||
721
TemperatureGauge.qml
Normal file
721
TemperatureGauge.qml
Normal file
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
35
build.sh
Executable file
35
build.sh
Executable file
@@ -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'"
|
||||
4
config.json
Normal file
4
config.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"left_sensor_id": "020391774d86",
|
||||
"right_sensor_id": "0213138491aa"
|
||||
}
|
||||
172
main.py
Normal file
172
main.py
Normal file
@@ -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()
|
||||
63
main.qml
Normal file
63
main.qml
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
4
requirements.txt
Normal file
4
requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
PySide6
|
||||
pyinstaller
|
||||
w1thermsensor
|
||||
|
||||
63
run_pi.sh
Normal file
63
run_pi.sh
Normal file
@@ -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
|
||||
58
verify_sensors.py
Normal file
58
verify_sensors.py
Normal file
@@ -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()
|
||||
BIN
volvodisplay_splashscreen.kra
Normal file
BIN
volvodisplay_splashscreen.kra
Normal file
Binary file not shown.
Reference in New Issue
Block a user