From 741702cedfb948118d68c9c7680e362d12bcb5cb Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sun, 11 Jan 2026 03:11:28 +0000 Subject: [PATCH] feat: Add Offline Activation Dialog and Registration Dialog - Implemented OfflineActivationDialog.qml for offline activation process with detailed instructions and file handling. - Created RegistrationDialog.qml for user registration with manual input and email parsing options. - Added new components: RoundGauge.qml, ScalarEditor.qml, Table2DEditor.qml, Table3DEditor.qml, TableEditorPanel.qml, and TableEditorView.qml for enhanced table editing features. - Updated main.py to include clipboard operations and file handling for saving/loading activation requests and codes. - Enhanced README.md to document new features and file structure. --- app_qml/BarGauge.qml | 132 +++++++++++ app_qml/DashboardView.qml | 352 ++++++++++++++++++++++++++++ app_qml/DataLoggingView.qml | 259 ++++++++++++++++++++ app_qml/DiagnosticsView.qml | 107 +++++++++ app_qml/Main.qml | 234 ++++++++++++++++++ app_qml/OfflineActivationDialog.qml | 278 ++++++++++++++++++++++ app_qml/README.md | 129 ++++++++++ app_qml/RegistrationDialog.qml | 305 ++++++++++++++++++++++++ app_qml/RoundGauge.qml | 176 ++++++++++++++ app_qml/ScalarEditor.qml | 41 ++++ app_qml/Table2DEditor.qml | 51 ++++ app_qml/Table3DEditor.qml | 148 ++++++++++++ app_qml/TableEditorPanel.qml | 72 ++++++ app_qml/TableEditorView.qml | 25 ++ app_qml/TuningView.qml | 76 ++++++ app_qml/main.py | 114 +++++++++ 16 files changed, 2499 insertions(+) create mode 100644 app_qml/BarGauge.qml create mode 100644 app_qml/DashboardView.qml create mode 100644 app_qml/DataLoggingView.qml create mode 100644 app_qml/DiagnosticsView.qml create mode 100644 app_qml/Main.qml create mode 100644 app_qml/OfflineActivationDialog.qml create mode 100644 app_qml/README.md create mode 100644 app_qml/RegistrationDialog.qml create mode 100644 app_qml/RoundGauge.qml create mode 100644 app_qml/ScalarEditor.qml create mode 100644 app_qml/Table2DEditor.qml create mode 100644 app_qml/Table3DEditor.qml create mode 100644 app_qml/TableEditorPanel.qml create mode 100644 app_qml/TableEditorView.qml create mode 100644 app_qml/TuningView.qml create mode 100755 app_qml/main.py diff --git a/app_qml/BarGauge.qml b/app_qml/BarGauge.qml new file mode 100644 index 00000000..fb80439f --- /dev/null +++ b/app_qml/BarGauge.qml @@ -0,0 +1,132 @@ +import QtQuick +import QtQuick.Controls + +Item { + id: barGauge + + property string title: "Gauge" + property real minValue: 0 + property real maxValue: 100 + property real value: 50 + property real warningValue: 85 + property real dangerValue: 95 + property string units: "" + + width: 300 + height: 300 + + Rectangle { + anchors.fill: parent + color: "#2c3e50" + border.color: "#34495e" + border.width: 3 + radius: 10 + + Column { + anchors.fill: parent + anchors.margins: 20 + spacing: 15 + + // Title + Label { + width: parent.width + text: title + font.pixelSize: 18 + font.bold: true + color: "#ecf0f1" + horizontalAlignment: Text.AlignHCenter + } + + // Digital value + Label { + width: parent.width + text: value.toFixed(1) + " " + units + font.pixelSize: 32 + font.bold: true + color: { + if (value >= dangerValue) return "#e74c3c" + if (value >= warningValue) return "#f39c12" + return "#2ecc71" + } + horizontalAlignment: Text.AlignHCenter + } + + // Bar graph + Item { + width: parent.width + height: parent.height - 120 + + // Background + Rectangle { + anchors.fill: parent + color: "#34495e" + radius: 5 + } + + // Value bar + Rectangle { + anchors.left: parent.left + anchors.bottom: parent.bottom + width: parent.width + height: parent.height * ((value - minValue) / (maxValue - minValue)) + radius: 5 + + gradient: Gradient { + GradientStop { + position: 0.0 + color: { + if (value >= dangerValue) return "#c0392b" + if (value >= warningValue) return "#d68910" + return "#27ae60" + } + } + GradientStop { + position: 1.0 + color: { + if (value >= dangerValue) return "#e74c3c" + if (value >= warningValue) return "#f39c12" + return "#2ecc71" + } + } + } + + Behavior on height { + NumberAnimation { duration: 100 } + } + } + + // Scale marks + Column { + anchors.fill: parent + Repeater { + model: 11 + Rectangle { + width: parent.width + height: 1 + color: "#7f8c8d" + opacity: 0.5 + } + } + } + } + + // Min/Max labels + Row { + width: parent.width + Label { + width: parent.width / 2 + text: minValue.toFixed(0) + color: "#95a5a6" + font.pixelSize: 12 + } + Label { + width: parent.width / 2 + text: maxValue.toFixed(0) + color: "#95a5a6" + font.pixelSize: 12 + horizontalAlignment: Text.AlignRight + } + } + } + } +} diff --git a/app_qml/DashboardView.qml b/app_qml/DashboardView.qml new file mode 100644 index 00000000..7c322b9c --- /dev/null +++ b/app_qml/DashboardView.qml @@ -0,0 +1,352 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +Item { + id: dashboardView + + // Driving simulation state + property int drivePhase: 0 // 0=idle, 1=accelerate, 2=cruise, 3=brake, 4=turn, 5=stop + property real currentSpeed: 0 + property real targetRpm: 800 + property real currentRpm: 800 + property real currentThrottle: 0 + property real currentTemp: 85 + property real currentAfr: 14.7 + property real currentBoost: -10 + property real currentVoltage: 14.2 + property int phaseTimer: 0 + + // Realistic driving simulation around the block + Timer { + interval: 50 // 20 Hz update + running: true + repeat: true + onTriggered: { + phaseTimer++ + + // Drive cycle phases (simulating driving around a block) + if (drivePhase === 0) { + // Idle at start + targetRpm = 850 + currentThrottle = Math.max(0, currentThrottle - 2) + currentAfr = 14.7 + currentBoost = -10 + + if (phaseTimer > 30) { // ~1.5 seconds + drivePhase = 1 + phaseTimer = 0 + } + } + else if (drivePhase === 1) { + // Accelerate from stop - pulling away + currentThrottle = Math.min(45, currentThrottle + 3) + targetRpm = 800 + currentThrottle * 60 // Up to ~3500 RPM + currentAfr = 13.2 + (currentThrottle / 100) * 0.8 // Richer under load + currentBoost = -8 + (currentThrottle / 100) * 8 // Light positive boost + + if (phaseTimer > 50) { // ~2.5 seconds of acceleration + drivePhase = 2 + phaseTimer = 0 + } + } + else if (drivePhase === 2) { + // Cruise at steady speed down the street + currentThrottle = 25 + Math.sin(phaseTimer * 0.1) * 3 + targetRpm = 2200 + Math.sin(phaseTimer * 0.05) * 200 + currentAfr = 14.5 + Math.sin(phaseTimer * 0.08) * 0.3 + currentBoost = -2 + Math.sin(phaseTimer * 0.1) * 2 + + if (phaseTimer > 80) { // ~4 seconds cruising + drivePhase = 3 + phaseTimer = 0 + } + } + else if (drivePhase === 3) { + // Braking for turn + currentThrottle = Math.max(0, currentThrottle - 4) + targetRpm = Math.max(850, targetRpm - 80) + currentAfr = 15.5 // Lean on decel + currentBoost = -12 + + if (phaseTimer > 25) { // ~1.25 seconds braking + drivePhase = 4 + phaseTimer = 0 + } + } + else if (drivePhase === 4) { + // Turn corner - light acceleration + currentThrottle = Math.min(35, currentThrottle + 2) + targetRpm = 1200 + currentThrottle * 40 + currentAfr = 13.8 + currentBoost = -5 + (currentThrottle / 100) * 5 + + if (phaseTimer > 40) { // ~2 seconds through turn + drivePhase = 5 + phaseTimer = 0 + } + } + else if (drivePhase === 5) { + // Accelerate harder - straightaway + currentThrottle = Math.min(70, currentThrottle + 4) + targetRpm = 1500 + currentThrottle * 70 // Up to ~6400 RPM + currentAfr = 12.8 + (currentThrottle / 100) * 0.5 // Richer for power + currentBoost = -3 + (currentThrottle / 100) * 18 // Build boost + + if (phaseTimer > 60) { // ~3 seconds hard acceleration + drivePhase = 6 + phaseTimer = 0 + } + } + else if (drivePhase === 6) { + // Cruise again on next street + currentThrottle = 30 + Math.sin(phaseTimer * 0.15) * 5 + targetRpm = 2500 + Math.sin(phaseTimer * 0.1) * 300 + currentAfr = 14.3 + Math.sin(phaseTimer * 0.12) * 0.4 + currentBoost = 0 + Math.sin(phaseTimer * 0.08) * 3 + + if (phaseTimer > 70) { // ~3.5 seconds + drivePhase = 7 + phaseTimer = 0 + } + } + else if (drivePhase === 7) { + // Braking for another turn + currentThrottle = Math.max(0, currentThrottle - 5) + targetRpm = Math.max(850, targetRpm - 100) + currentAfr = 15.2 + currentBoost = -11 + + if (phaseTimer > 20) { + drivePhase = 8 + phaseTimer = 0 + } + } + else if (drivePhase === 8) { + // Final turn back home + currentThrottle = Math.min(30, currentThrottle + 2) + targetRpm = 1400 + currentThrottle * 35 + currentAfr = 14.0 + currentBoost = -6 + + if (phaseTimer > 35) { + drivePhase = 9 + phaseTimer = 0 + } + } + else if (drivePhase === 9) { + // Approaching home - gentle decel + currentThrottle = Math.max(0, currentThrottle - 2) + targetRpm = Math.max(850, targetRpm - 50) + currentAfr = 15.0 + currentBoost = -10 + + if (phaseTimer > 40) { + drivePhase = 10 + phaseTimer = 0 + } + } + else { + // Coming to stop - back to idle + currentThrottle = 0 + targetRpm = 850 + currentAfr = 14.7 + currentBoost = -10 + + if (phaseTimer > 50) { // Wait at idle + drivePhase = 0 // Start over + phaseTimer = 0 + } + } + + // Smooth RPM transition + currentRpm += (targetRpm - currentRpm) * 0.15 + + // Temperature gradually increases with driving, decreases at idle + if (currentThrottle > 20) { + currentTemp = Math.min(105, currentTemp + 0.03) + } else { + currentTemp = Math.max(85, currentTemp - 0.02) + } + + // Voltage drops slightly under load + var targetVoltage = 14.2 - (currentThrottle / 100) * 0.8 + currentVoltage += (targetVoltage - currentVoltage) * 0.1 + + // Add small random variations for realism + rpm.value = currentRpm + (Math.random() - 0.5) * 50 + temp.value = currentTemp + (Math.random() - 0.5) * 0.5 + afr.value = currentAfr + (Math.random() - 0.5) * 0.2 + boost.value = currentBoost + (Math.random() - 0.5) * 0.5 + throttle.value = currentThrottle + (Math.random() - 0.5) * 2 + voltage.value = currentVoltage + (Math.random() - 0.5) * 0.1 + } + } + + ScrollView { + anchors.fill: parent + + ColumnLayout { + width: dashboardView.width - 20 + anchors.left: parent.left + anchors.leftMargin: 20 + anchors.topMargin: 20 + spacing: 20 + + // Driving status indicator + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 60 + color: "#34495e" + radius: 10 + border.color: "#1976D2" + border.width: 2 + + RowLayout { + anchors.fill: parent + anchors.margins: 15 + spacing: 15 + + Rectangle { + width: 30 + height: 30 + radius: 15 + color: { + if (drivePhase <= 0) return "#95a5a6" // Gray - idle + if (drivePhase === 1 || drivePhase === 5) return "#2ecc71" // Green - accelerating + if (drivePhase === 2 || drivePhase === 6) return "#3498db" // Blue - cruising + if (drivePhase === 3 || drivePhase === 7) return "#e74c3c" // Red - braking + if (drivePhase === 4 || drivePhase === 8) return "#f39c12" // Orange - turning + return "#95a5a6" // Gray - stopping + } + + SequentialAnimation on opacity { + running: drivePhase > 0 && drivePhase < 10 + loops: Animation.Infinite + NumberAnimation { to: 0.3; duration: 500 } + NumberAnimation { to: 1.0; duration: 500 } + } + } + + Label { + text: { + if (drivePhase === 0) return "🏠 Idle at Start" + if (drivePhase === 1) return "🚀 Accelerating from Stop" + if (drivePhase === 2) return "đŸ›Ŗī¸ Cruising Down Street" + if (drivePhase === 3) return "🛑 Braking for Turn" + if (drivePhase === 4) return "â†Ēī¸ Turning Corner" + if (drivePhase === 5) return "⚡ Hard Acceleration" + if (drivePhase === 6) return "đŸ›Ŗī¸ Cruising Next Street" + if (drivePhase === 7) return "🛑 Braking for Turn" + if (drivePhase === 8) return "â†Šī¸ Final Turn Home" + if (drivePhase === 9) return "🏁 Approaching Home" + return "🏠 Coming to Stop" + } + font.pixelSize: 18 + font.bold: true + color: "white" + Layout.fillWidth: true + } + + Label { + text: "Speed: " + Math.round(currentRpm / 50) + " mph" + font.pixelSize: 14 + color: "#ecf0f1" + } + } + } + + GridLayout { + Layout.fillWidth: true + columns: 3 + rowSpacing: 20 + columnSpacing: 20 + + // RPM Gauge + RoundGauge { + id: rpm + Layout.preferredWidth: 300 + Layout.preferredHeight: 300 + title: "RPM" + minValue: 0 + maxValue: 8000 + warningValue: 6500 + dangerValue: 7500 + units: "rpm" + majorTicks: 9 + minorTicks: 5 + } + + // Coolant Temp Gauge + RoundGauge { + id: temp + Layout.preferredWidth: 300 + Layout.preferredHeight: 300 + title: "Coolant Temp" + minValue: 0 + maxValue: 120 + warningValue: 95 + dangerValue: 105 + units: "°C" + majorTicks: 7 + minorTicks: 4 + } + + // AFR Gauge + RoundGauge { + id: afr + Layout.preferredWidth: 300 + Layout.preferredHeight: 300 + title: "Air/Fuel Ratio" + minValue: 10 + maxValue: 18 + warningValue: 12 + dangerValue: 11 + units: ":1" + majorTicks: 9 + minorTicks: 2 + value: 14.7 + } + + // Boost/Vacuum Gauge + RoundGauge { + id: boost + Layout.preferredWidth: 300 + Layout.preferredHeight: 300 + title: "Boost" + minValue: -15 + maxValue: 30 + warningValue: 20 + dangerValue: 25 + units: "psi" + majorTicks: 10 + minorTicks: 5 + } + + // Throttle Position Bar + BarGauge { + id: throttle + Layout.preferredWidth: 300 + Layout.preferredHeight: 300 + title: "Throttle Position" + minValue: 0 + maxValue: 100 + units: "%" + } + + // Battery Voltage + BarGauge { + id: voltage + Layout.preferredWidth: 300 + Layout.preferredHeight: 300 + title: "Battery Voltage" + minValue: 10 + maxValue: 16 + warningValue: 11.5 + dangerValue: 11 + units: "V" + } + } + } + } +} diff --git a/app_qml/DataLoggingView.qml b/app_qml/DataLoggingView.qml new file mode 100644 index 00000000..f1076c24 --- /dev/null +++ b/app_qml/DataLoggingView.qml @@ -0,0 +1,259 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +Item { + id: dataLoggingView + + ColumnLayout { + anchors.fill: parent + anchors.margins: 10 + spacing: 10 + + // Control panel + RowLayout { + Layout.fillWidth: true + spacing: 10 + + Button { + text: "Start Logging" + highlighted: true + onClicked: console.log("Start logging") + } + + Button { + text: "Stop Logging" + enabled: false + } + + Button { + text: "Load Log File..." + onClicked: console.log("Load log") + } + + Item { Layout.fillWidth: true } + + Label { + text: "Sample Rate:" + } + + ComboBox { + model: ["10 Hz", "20 Hz", "50 Hz", "100 Hz"] + currentIndex: 2 + } + } + + // Chart area + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: "#2c3e50" + border.color: "#34495e" + border.width: 2 + radius: 5 + + ColumnLayout { + anchors.fill: parent + anchors.margins: 10 + spacing: 5 + + Label { + text: "Data Log Chart" + color: "white" + font.pixelSize: 16 + font.bold: true + Layout.alignment: Qt.AlignHCenter + } + + // Chart canvas + Canvas { + id: chartCanvas + Layout.fillWidth: true + Layout.fillHeight: true + + property var dataPoints: [] + + property int logTime: 0 + property real logRpm: 850 + property real logTemp: 85 + property real logAfr: 14.7 + + Timer { + interval: 100 + running: true + repeat: true + onTriggered: { + chartCanvas.logTime++ + var t = chartCanvas.logTime + + // Simulate driving around the block (synchronized with dashboard) + if (t < 30) { + // Idle + chartCanvas.logRpm = 850 + Math.sin(t * 0.5) * 30 + chartCanvas.logAfr = 14.7 + } else if (t < 80) { + // Accelerate + var accel = (t - 30) / 50 + chartCanvas.logRpm = 850 + accel * 2700 + Math.sin(t * 0.3) * 100 + chartCanvas.logAfr = 13.2 + accel * 0.8 + chartCanvas.logTemp = Math.min(95, 85 + accel * 10) + } else if (t < 160) { + // Cruise + chartCanvas.logRpm = 2200 + Math.sin(t * 0.1) * 300 + chartCanvas.logAfr = 14.5 + Math.sin(t * 0.15) * 0.3 + chartCanvas.logTemp = Math.min(98, chartCanvas.logTemp + 0.05) + } else if (t < 185) { + // Brake + var brake = (t - 160) / 25 + chartCanvas.logRpm = 2200 - brake * 1350 + chartCanvas.logAfr = 15.5 + } else if (t < 225) { + // Turn + chartCanvas.logRpm = 1200 + Math.sin(t * 0.2) * 200 + chartCanvas.logAfr = 13.8 + } else if (t < 285) { + // Hard accelerate + var hardAccel = (t - 225) / 60 + chartCanvas.logRpm = 1500 + hardAccel * 4900 + Math.sin(t * 0.3) * 150 + chartCanvas.logAfr = 12.8 + hardAccel * 0.5 + chartCanvas.logTemp = Math.min(103, chartCanvas.logTemp + 0.08) + } else if (t < 355) { + // Cruise 2 + chartCanvas.logRpm = 2500 + Math.sin(t * 0.12) * 350 + chartCanvas.logAfr = 14.3 + Math.sin(t * 0.18) * 0.4 + } else if (t < 375) { + // Brake 2 + var brake2 = (t - 355) / 20 + chartCanvas.logRpm = 2500 - brake2 * 1650 + chartCanvas.logAfr = 15.2 + } else if (t < 410) { + // Turn 2 + chartCanvas.logRpm = 1400 + Math.sin(t * 0.25) * 150 + chartCanvas.logAfr = 14.0 + } else if (t < 450) { + // Decel + var decel = (t - 410) / 40 + chartCanvas.logRpm = 1400 - decel * 550 + chartCanvas.logAfr = 15.0 + chartCanvas.logTemp = Math.max(85, chartCanvas.logTemp - 0.1) + } else if (t < 500) { + // Back to idle + chartCanvas.logRpm = 850 + Math.sin(t * 0.4) * 25 + chartCanvas.logAfr = 14.7 + chartCanvas.logTemp = Math.max(85, chartCanvas.logTemp - 0.05) + } else { + // Reset and loop + chartCanvas.logTime = 0 + } + + chartCanvas.dataPoints.push({ + rpm: chartCanvas.logRpm, + afr: chartCanvas.logAfr, + temp: chartCanvas.logTemp + }) + if (chartCanvas.dataPoints.length > 100) { + chartCanvas.dataPoints.shift() + } + chartCanvas.requestPaint() + } + } + + onPaint: { + var ctx = getContext("2d") + ctx.reset() + + // Draw background grid + ctx.strokeStyle = "#34495e" + ctx.lineWidth = 1 + + for (var i = 0; i < 10; i++) { + var y = height * i / 10 + ctx.beginPath() + ctx.moveTo(0, y) + ctx.lineTo(width, y) + ctx.stroke() + } + + for (var j = 0; j < 20; j++) { + var x = width * j / 20 + ctx.beginPath() + ctx.moveTo(x, 0) + ctx.lineTo(x, height) + ctx.stroke() + } + + if (dataPoints.length < 2) return + + // Draw RPM line + ctx.beginPath() + ctx.strokeStyle = "#3498db" + ctx.lineWidth = 2 + + for (var k = 0; k < dataPoints.length; k++) { + var xPos = (k / dataPoints.length) * width + var yPos = height - (dataPoints[k].rpm / 8000 * height) + + if (k === 0) { + ctx.moveTo(xPos, yPos) + } else { + ctx.lineTo(xPos, yPos) + } + } + ctx.stroke() + + // Draw AFR line + ctx.beginPath() + ctx.strokeStyle = "#2ecc71" + ctx.lineWidth = 2 + + for (var m = 0; m < dataPoints.length; m++) { + var xPos2 = (m / dataPoints.length) * width + var yPos2 = height - ((dataPoints[m].afr - 10) / 8 * height) + + if (m === 0) { + ctx.moveTo(xPos2, yPos2) + } else { + ctx.lineTo(xPos2, yPos2) + } + } + ctx.stroke() + } + } + + // Legend + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: 20 + + Row { + spacing: 5 + Rectangle { + width: 20 + height: 10 + color: "#3498db" + anchors.verticalCenter: parent.verticalCenter + } + Label { + text: "RPM" + color: "white" + } + } + + Row { + spacing: 5 + Rectangle { + width: 20 + height: 10 + color: "#2ecc71" + anchors.verticalCenter: parent.verticalCenter + } + Label { + text: "AFR" + color: "white" + } + } + } + } + } + } +} diff --git a/app_qml/DiagnosticsView.qml b/app_qml/DiagnosticsView.qml new file mode 100644 index 00000000..40b6ac63 --- /dev/null +++ b/app_qml/DiagnosticsView.qml @@ -0,0 +1,107 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +Item { + id: diagnosticsView + + ColumnLayout { + anchors.fill: parent + anchors.margins: 10 + spacing: 10 + + Label { + text: "ECU Diagnostics" + font.pixelSize: 18 + font.bold: true + } + + GroupBox { + Layout.fillWidth: true + title: "Connection Status" + + GridLayout { + anchors.fill: parent + columns: 2 + rowSpacing: 8 + columnSpacing: 15 + + Label { text: "Connection:" } + Label { + text: "Not Connected" + color: "#e74c3c" + font.bold: true + } + + Label { text: "Port:" } + Label { text: "N/A" } + + Label { text: "Baud Rate:" } + Label { text: "115200" } + + Label { text: "ECU Type:" } + Label { text: "MegaSquirt-II" } + + Label { text: "Firmware:" } + Label { text: "v3.4.2" } + } + } + + GroupBox { + Layout.fillWidth: true + title: "Error Codes" + + ScrollView { + anchors.fill: parent + implicitHeight: 200 + + ListView { + model: ListModel { + ListElement { code: "P0301"; description: "Cylinder 1 Misfire Detected" } + ListElement { code: "P0171"; description: "System Too Lean (Bank 1)" } + ListElement { code: "P0420"; description: "Catalyst System Efficiency Below Threshold" } + } + + delegate: ItemDelegate { + width: ListView.view.width + + RowLayout { + anchors.fill: parent + spacing: 10 + + Label { + text: code + font.bold: true + color: "#e74c3c" + } + + Label { + text: description + Layout.fillWidth: true + } + } + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 10 + + Button { + text: "Clear Codes" + onClicked: console.log("Clear error codes") + } + + Button { + text: "Refresh" + onClicked: console.log("Refresh diagnostics") + } + + Item { Layout.fillWidth: true } + } + + Item { Layout.fillHeight: true } + } +} diff --git a/app_qml/Main.qml b/app_qml/Main.qml new file mode 100644 index 00000000..5d0f71bf --- /dev/null +++ b/app_qml/Main.qml @@ -0,0 +1,234 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +ApplicationWindow { + id: mainWindow + width: 1200 + height: 800 + visible: true + title: "TunerStudio MS - Engine Management System" + + // Menu bar + menuBar: MenuBar { + Menu { + title: "&File" + MenuItem { text: "Open Project..."; onTriggered: console.log("Open") } + MenuItem { text: "Recent Projects" } + MenuSeparator {} + MenuItem { text: "Save Configuration"; shortcut: "Ctrl+S" } + MenuItem { text: "Save As..." } + MenuSeparator {} + MenuItem { text: "Exit"; onTriggered: Qt.quit() } + } + + Menu { + title: "&Tools" + MenuItem { text: "Calibrate TPS" } + MenuItem { text: "Reset Control Unit" } + MenuItem { text: "Tooth Logger" } + MenuSeparator {} + MenuItem { text: "Firmware Update..." } + } + + Menu { + title: "&Communications" + MenuItem { text: "Connect"; checkable: true } + MenuItem { text: "Settings..." } + MenuSeparator {} + MenuItem { text: "Detect Port" } + } + + Menu { + title: "&Tuning" + MenuItem { text: "Open Tuning Dialog" } + MenuItem { text: "Table Editor" } + MenuItem { text: "3D Table View" } + } + + Menu { + title: "&Data Logging" + MenuItem { text: "Start Log"; shortcut: "F2" } + MenuItem { text: "Stop Log"; shortcut: "F3" } + MenuSeparator {} + MenuItem { text: "View Logs..." } + } + + Menu { + title: "&Help" + MenuItem { text: "Documentation" } + MenuItem { text: "Forum" } + MenuSeparator {} + MenuItem { text: "About" } + MenuItem { text: "Register..." } + } + } + + // Tool bar + header: ToolBar { + RowLayout { + anchors.fill: parent + spacing: 5 + + ToolButton { + text: "Connect" + icon.source: "qrc:/icons/connect.png" + onClicked: statusLabel.text = "Connecting..." + } + + ToolSeparator {} + + ToolButton { + text: "Upload" + icon.source: "qrc:/icons/upload.png" + enabled: false + } + + ToolButton { + text: "Download" + icon.source: "qrc:/icons/download.png" + enabled: false + } + + ToolSeparator {} + + ToolButton { + text: "Start Log" + icon.source: "qrc:/icons/log.png" + } + + ToolButton { + text: "Stop Log" + icon.source: "qrc:/icons/stop.png" + enabled: false + } + + ToolSeparator {} + + ToolButton { + text: "Dashboard" + icon.source: "qrc:/icons/dashboard.png" + onClicked: tabBar.currentIndex = 1 + } + + Item { Layout.fillWidth: true } + + Label { + text: "Status: " + font.bold: true + } + + Label { + id: statusLabel + text: "Simulation Running - Driving Around Block" + color: "#2ecc71" + } + } + } + + // Main content with tab bar + ColumnLayout { + anchors.fill: parent + spacing: 0 + + TabBar { + id: tabBar + Layout.fillWidth: true + + TabButton { + text: "Tuning" + width: implicitWidth + } + TabButton { + text: "Dashboard" + width: implicitWidth + } + TabButton { + text: "Data Logging" + width: implicitWidth + } + TabButton { + text: "Table Editor" + width: implicitWidth + } + TabButton { + text: "Diagnostics" + width: implicitWidth + } + } + + StackLayout { + Layout.fillWidth: true + Layout.fillHeight: true + currentIndex: tabBar.currentIndex + + // Tab 0: Tuning View + TuningView { + id: tuningView + } + + // Tab 1: Dashboard with gauges + DashboardView { + id: dashboardView + } + + // Tab 2: Data Logging + DataLoggingView { + id: dataLoggingView + } + + // Tab 3: Table Editor + TableEditorView { + id: tableEditorView + } + + // Tab 4: Diagnostics + DiagnosticsView { + id: diagnosticsView + } + } + } + + // Status bar + footer: ToolBar { + RowLayout { + anchors.fill: parent + spacing: 10 + + Label { + text: "Port: COM3" + font.pixelSize: 11 + } + + Rectangle { + width: 1 + height: 15 + color: "#ccc" + } + + Label { + text: "Baud: 115200" + font.pixelSize: 11 + } + + Rectangle { + width: 1 + height: 15 + color: "#ccc" + } + + Label { + text: "ECU: MegaSquirt-II" + font.pixelSize: 11 + } + + Item { Layout.fillWidth: true } + + Label { + text: "Ready" + font.pixelSize: 11 + color: "#4CAF50" + } + } + } +} diff --git a/app_qml/OfflineActivationDialog.qml b/app_qml/OfflineActivationDialog.qml new file mode 100644 index 00000000..4580b00b --- /dev/null +++ b/app_qml/OfflineActivationDialog.qml @@ -0,0 +1,278 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Dialogs + +ApplicationWindow { + id: offlineActivationDialog + width: 650 + height: 700 + minimumWidth: 600 + minimumHeight: 650 + title: "Offline Activate TunerStudio MS" + color: "#f5f5f5" + + // Signals + signal activationComplete(string activationCode) + signal activationCancelled() + + // Properties + property string productName: "TunerStudio MS" + property string activationRequest: "" + + ColumnLayout { + anchors.fill: parent + anchors.margins: 15 + spacing: 15 + + // Main panel + GroupBox { + Layout.fillWidth: true + title: productName + " Offline Registration Activation" + + ColumnLayout { + anchors.fill: parent + spacing: 10 + + // Instructions + Label { + Layout.fillWidth: true + text: "

5 Step Offline Activation

" + + "

Step 1 - Save Activation Request to File ActivationRequest.txt on a USB drive or other medium.

" + + "

Step 2 - On a Computer that is connected to the Internet, open a web browser and go to
" + + "    https://www.efianalytics.com/activate

" + + "

Step 3 - Upload your saved ActivationRequest.txt, the site will provide you with ActivationCode.txt

" + + "

Step 4 - Return to TunerStudio and click Load Activation From File to load ActivationCode.txt into TunerStudio

" + + "

Step 5 - Click Accept

" + + "

Done!

" + wrapMode: Text.WordWrap + textFormat: Text.RichText + onLinkActivated: Qt.openUrlExternally(link) + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + } + } + } + } + + // Activation Request section + GroupBox { + Layout.fillWidth: true + title: "Activation Request" + + ColumnLayout { + anchors.fill: parent + spacing: 10 + + ScrollView { + Layout.fillWidth: true + Layout.preferredHeight: 120 + Layout.minimumHeight: 60 + + TextArea { + id: requestTextArea + text: activationRequest + readOnly: true + selectByMouse: true + wrapMode: TextEdit.Wrap + background: Rectangle { + color: "#d3d3d3" + border.color: "#999" + border.width: 1 + radius: 3 + } + + Component.onCompleted: { + selectAll() + } + } + } + + RowLayout { + Layout.alignment: Qt.AlignRight + spacing: 10 + + Label { + text: "Step 1 -->" + font.bold: true + } + + Button { + text: "Save Request to File" + onClicked: saveRequestDialog.open() + } + + Button { + text: "Copy Request to Clipboard" + onClicked: { + clipboardHelper.setClipboardText(requestTextArea.text) + statusLabel.text = "✓ Copied to clipboard" + statusTimer.start() + } + } + } + } + } + + // Server Activation Code section + GroupBox { + Layout.fillWidth: true + title: "Server Activation Code" + + ColumnLayout { + anchors.fill: parent + spacing: 10 + + ScrollView { + Layout.fillWidth: true + Layout.preferredHeight: 120 + Layout.minimumHeight: 60 + + TextArea { + id: activationCodeArea + placeholderText: "Paste or load activation code here..." + selectByMouse: true + wrapMode: TextEdit.Wrap + background: Rectangle { + color: "white" + border.color: "#ccc" + border.width: 1 + radius: 3 + } + + onTextChanged: { + acceptButton.enabled = text.trim().length > 0 + } + } + } + + RowLayout { + Layout.alignment: Qt.AlignRight + spacing: 10 + + Label { + text: "Step 4 -->" + font.bold: true + } + + Button { + text: "Load Activation From File" + onClicked: loadActivationDialog.open() + } + + Button { + text: "Paste Activation Code" + onClicked: { + activationCodeArea.text = clipboardHelper.getClipboardText() + } + } + } + } + } + + // Status label + Label { + id: statusLabel + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + color: "#4CAF50" + font.bold: true + visible: text.length > 0 + } + + Timer { + id: statusTimer + interval: 3000 + onTriggered: statusLabel.text = "" + } + + Item { + Layout.fillHeight: true + } + + // Bottom buttons + RowLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignRight + spacing: 10 + + Label { + text: "Step 5 -->" + font.bold: true + } + + Button { + id: acceptButton + text: "Accept" + enabled: false + highlighted: true + onClicked: { + if (activationCodeArea.text.trim().length > 0) { + activationComplete(activationCodeArea.text.trim()) + offlineActivationDialog.close() + } + } + } + + Button { + text: "Cancel" + onClicked: { + activationCancelled() + offlineActivationDialog.close() + } + } + } + } + + // File dialogs + FileDialog { + id: saveRequestDialog + fileMode: FileDialog.SaveFile + defaultSuffix: "txt" + nameFilters: ["Text files (*.txt)", "All files (*)"] + currentFolder: "file:///" + clipboardHelper.getHomeDirectory() + + onAccepted: { + clipboardHelper.saveTextToFile(selectedFile.toString(), requestTextArea.text) + statusLabel.text = "✓ Saved to file" + statusTimer.start() + } + } + + FileDialog { + id: loadActivationDialog + fileMode: FileDialog.OpenFile + nameFilters: ["Text files (*.txt)", "All files (*)"] + currentFolder: "file:///" + clipboardHelper.getHomeDirectory() + + onAccepted: { + var content = clipboardHelper.loadTextFromFile(selectedFile.toString()) + if (content) { + activationCodeArea.text = content + statusLabel.text = "✓ Loaded from file" + statusTimer.start() + } + } + } + + Component.onCompleted: { + // Generate mock activation request on load + generateMockActivationRequest() + } + + function generateMockActivationRequest() { + var timestamp = new Date().toISOString() + activationRequest = "=== ACTIVATION REQUEST ===\n" + + "Product: " + productName + "\n" + + "Computer ID: ABC123XYZ789\n" + + "Timestamp: " + timestamp + "\n" + + "Request Code: 1A2B3C4D5E6F7G8H\n" + + "=========================\n" + + "Please submit this to:\n" + + "https://www.efianalytics.com/activate" + requestTextArea.text = activationRequest + } +} diff --git a/app_qml/README.md b/app_qml/README.md new file mode 100644 index 00000000..517ca245 --- /dev/null +++ b/app_qml/README.md @@ -0,0 +1,129 @@ +# TunerStudio MS QML Application + +A modern QML/Qt6 replica of the TunerStudio MS engine management software, originally implemented in Java/Swing. + +## Features + +### Main Application +- **Tuning View**: 3D table editors for VE tables, ignition timing, air/fuel ratios +- **Dashboard**: Real-time animated gauges including: + - RPM gauge (analog) + - Coolant temperature + - Air/Fuel ratio + - Boost/vacuum pressure + - Throttle position (bar gauge) + - Battery voltage (bar gauge) +- **Data Logging**: Real-time chart with multiple channels +- **Table Editor**: Advanced table editing with color-coded cells +- **Diagnostics**: ECU connection status and error codes + +### Registration System +- Registration dialog with manual input and email parsing +- Offline activation system with 5-step process +- Support for multiple editions (Standard, Professional, Ultra, Lite) + +## Installation + +```bash +# Install PyQt6 +pip install PyQt6 + +# Or use the requirements file +pip install -r requirements.txt +``` + +## Running the Application + +```bash +cd app_qml +python3 main.py +``` + +## File Structure + +``` +app_qml/ +├── main.py # Python launcher script +├── Main.qml # Main application window +├── TuningView.qml # Tuning tables view +├── TableEditorPanel.qml # Table editor component +├── Table3DEditor.qml # 3D table editor +├── Table2DEditor.qml # 2D table editor +├── ScalarEditor.qml # Scalar value editor +├── TableEditorView.qml # Advanced table editor view +├── DashboardView.qml # Dashboard with gauges +├── RoundGauge.qml # Analog gauge component +├── BarGauge.qml # Bar gauge component +├── DataLoggingView.qml # Data logging and charts +├── DiagnosticsView.qml # ECU diagnostics +├── RegistrationDialog.qml # Registration input dialog +└── OfflineActivationDialog.qml # Offline activation dialog +``` + +## Java/Swing to QML Mapping + +| Java Swing Component | QML Component | Used In | +|---------------------|---------------|---------| +| `JFrame` | `ApplicationWindow` | Main.qml | +| `JMenuBar` | `MenuBar` | Main.qml | +| `JTabbedPane` | `TabBar` + `StackLayout` | Main.qml | +| `JPanel` | `Item` / `Rectangle` | All views | +| `JButton` | `Button` | All views | +| `JTextField` | `TextField` | Tables, dialogs | +| `JLabel` | `Label` | All views | +| `JTable` | `GridLayout` + `TextField` | Table editors | +| `JDialog` | `ApplicationWindow` | Registration dialogs | +| `JRadioButton` | `RadioButton` | Registration dialog | +| `JComboBox` | `ComboBox` | Various views | +| `JScrollPane` | `ScrollView` | Various views | +| `JTextPane` | `TextArea` | Offline activation | +| Custom Gauge | `Canvas` with painting | Dashboard gauges | + +## Features Replicated from Java/Swing + +### Main Application (cd.java) +- Menu bar with File, Tools, Communications, Tuning, Data Logging, Help +- Toolbar with connection and data logging controls +- Tabbed interface for different views +- Status bar showing connection info + +### Dashboard (Gauge.java) +- Analog gauges with configurable ranges +- Color zones (green/yellow/red) +- Digital value display +- Warning and danger thresholds +- Animated needle movement + +### Table Editor (SelectableTable.java) +- 3D table editing with color coding +- 2D curve editing +- Scalar value editing +- Axis labels and values +- Cell-by-cell editing + +### Registration (dS.java, f.java) +- Manual registration input +- Email parsing for auto-fill +- Edition selection +- Offline activation workflow +- Clipboard operations +- File save/load for activation codes + +## Simulated Data + +The application includes simulated data for demonstration: +- Gauges update with random values in realistic ranges +- Data logging charts show simulated sensor data +- Table cells contain sample tuning values + +## Notes + +This is a faithful UI recreation of the TunerStudio MS application. The backend ECU communication logic would need to be implemented separately using appropriate serial communication libraries and the MegaSquirt protocol. + +## Original Java Application + +The original application structure from `app/`: +- Main frame: `ao/cd.java` +- Dashboard components: `com/efiAnalytics/apps/ts/dashboard/` +- Tuning views: `com/efiAnalytics/apps/ts/tuningViews/` +- Registration: `com/efiAnalytics/ui/dS.java`, `az/f.java` diff --git a/app_qml/RegistrationDialog.qml b/app_qml/RegistrationDialog.qml new file mode 100644 index 00000000..cff95704 --- /dev/null +++ b/app_qml/RegistrationDialog.qml @@ -0,0 +1,305 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Dialogs + +ApplicationWindow { + id: registrationDialog + width: 600 + height: 580 + minimumWidth: 550 + minimumHeight: 520 + title: "Enter Registration Information" + color: "#f5f5f5" + + // Signals + signal registrationComplete(string firstName, string lastName, string email, string key, string edition, string serialNumber) + signal registrationCancelled() + + // Properties + property var editions: ["Standard", "Professional", "Ultra", "Lite"] + property string productName: "TunerStudio MS" + + ColumnLayout { + anchors.fill: parent + anchors.margins: 10 + spacing: 10 + + // Radio buttons for input method + RowLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + spacing: 20 + + RadioButton { + id: inputsRadio + text: "Inputs" + checked: true + onCheckedChanged: { + if (checked) { + stackLayout.currentIndex = 0 + } + } + } + + RadioButton { + id: pasteEmailRadio + text: "Paste Email" + onCheckedChanged: { + if (checked) { + stackLayout.currentIndex = 1 + } + } + } + } + + // Stack layout for switching between input methods + StackLayout { + id: stackLayout + Layout.fillWidth: true + Layout.fillHeight: true + currentIndex: 0 + + // Page 0: Manual Inputs + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + ColumnLayout { + anchors.fill: parent + spacing: 10 + + // Edition selector + GroupBox { + Layout.fillWidth: true + title: "Register " + productName + " (Select Edition)" + + ColumnLayout { + anchors.fill: parent + + ComboBox { + id: editionCombo + Layout.fillWidth: true + model: editions + currentIndex: 0 + } + } + } + + // Registration Information + GroupBox { + Layout.fillWidth: true + title: "Registration Information" + + GridLayout { + anchors.fill: parent + columns: 2 + columnSpacing: 10 + rowSpacing: 8 + + Label { + text: "Registered First Name:" + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + } + TextField { + id: firstNameField + Layout.fillWidth: true + placeholderText: "Enter first name" + } + + Label { + text: "Registered Last Name:" + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + } + TextField { + id: lastNameField + Layout.fillWidth: true + placeholderText: "Enter last name" + } + + Label { + text: "Registered eMail Address:" + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + } + TextField { + id: emailField + Layout.fillWidth: true + placeholderText: "your.email@example.com" + } + + Label { + text: "Registration Key:" + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + } + TextField { + id: keyField + Layout.fillWidth: true + placeholderText: "Enter registration key" + } + + Label { + text: "Serial Number:" + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + } + TextField { + id: serialNumberField + Layout.fillWidth: true + placeholderText: "Optional" + } + } + } + + Item { + Layout.fillHeight: true + } + } + } + + // Page 1: Paste Email + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + ColumnLayout { + anchors.fill: parent + spacing: 10 + + Button { + text: "Paste" + Layout.alignment: Qt.AlignHCenter + onClicked: { + pasteEmailText.text = clipboardHelper.getClipboardText() + parseEmailContent() + } + } + + ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + + TextArea { + id: pasteEmailText + wrapMode: TextEdit.Wrap + placeholderText: "Paste your registration email here..." + onTextChanged: parseEmailContent() + + background: Rectangle { + color: "white" + border.color: "#ccc" + border.width: 1 + radius: 3 + } + } + } + } + } + } + + // Buttons + RowLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignRight + spacing: 10 + + Button { + text: "Ok" + highlighted: true + onClicked: validateAndAccept() + } + + Button { + text: "Cancel" + onClicked: { + registrationCancelled() + registrationDialog.close() + } + } + } + } + + // Dialog for validation errors + MessageDialog { + id: errorDialog + buttons: MessageDialog.Ok + } + + // Functions + function parseEmailContent() { + var text = pasteEmailText.text + var lines = text.split('\n') + var inRegistrationSection = false + + for (var i = 0; i < lines.length; i++) { + var line = lines[i].trim() + + if (line.startsWith("[Registration]")) { + inRegistrationSection = true + continue + } + + if (line.startsWith("[End Registration]")) { + inRegistrationSection = false + continue + } + + if (inRegistrationSection) { + if (line.startsWith("First Name")) { + var value = line.substring(line.indexOf(":") + 1).trim() + firstNameField.text = value + } + else if (line.startsWith("Last Name")) { + var value = line.substring(line.indexOf(":") + 1).trim() + lastNameField.text = value + } + else if (line.startsWith("Registered email")) { + var value = line.substring(line.indexOf(":") + 1).trim() + emailField.text = value + } + else if (line.includes("Serial Number:")) { + var value = line.substring(line.indexOf(":") + 1).trim() + serialNumberField.text = value + } + else if (line.startsWith("Registration Key")) { + var value = line.substring(line.indexOf(":") + 1).trim() + keyField.text = value + } + } + } + } + + function validateAndAccept() { + var errors = [] + + if (firstNameField.text.trim() === "") { + errors.push("First Name") + } + if (lastNameField.text.trim() === "") { + errors.push("Last Name") + } + if (emailField.text.trim() === "") { + errors.push("eMail Address") + } + if (keyField.text.trim() === "") { + errors.push("Registration Key") + } + + if (errors.length > 0) { + errorDialog.title = "Missing Information" + errorDialog.text = "You must provide the information used during registration for:\n" + errors.join("\n") + errorDialog.open() + return + } + + // Emit signal with registration data + registrationComplete( + firstNameField.text.trim(), + lastNameField.text.trim(), + emailField.text.trim(), + keyField.text.trim(), + editionCombo.currentText, + serialNumberField.text.trim() + ) + + registrationDialog.close() + } +} diff --git a/app_qml/RoundGauge.qml b/app_qml/RoundGauge.qml new file mode 100644 index 00000000..a5d5f5ed --- /dev/null +++ b/app_qml/RoundGauge.qml @@ -0,0 +1,176 @@ +import QtQuick +import QtQuick.Controls + +Item { + id: roundGauge + + property string title: "Gauge" + property real minValue: 0 + property real maxValue: 100 + property real value: 0 + property real warningValue: 85 + property real dangerValue: 95 + property string units: "" + property int majorTicks: 11 + property int minorTicks: 5 + + width: 300 + height: 300 + + Canvas { + id: canvas + anchors.fill: parent + antialiasing: true + + onPaint: { + var ctx = getContext("2d") + ctx.reset() + + var centerX = width / 2 + var centerY = height / 2 + var radius = Math.min(width, height) / 2 - 30 + + // Background circle + ctx.beginPath() + ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI) + ctx.fillStyle = "#2c3e50" + ctx.fill() + + // Outer rim + ctx.beginPath() + ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI) + ctx.strokeStyle = "#34495e" + ctx.lineWidth = 8 + ctx.stroke() + + // Draw ticks + var startAngle = 0.75 * Math.PI + var endAngle = 2.25 * Math.PI + var angleRange = endAngle - startAngle + + for (var i = 0; i <= majorTicks; i++) { + var angle = startAngle + (i / majorTicks) * angleRange + var innerRadius = radius - 15 + var outerRadius = radius - 5 + + var x1 = centerX + innerRadius * Math.cos(angle) + var y1 = centerY + innerRadius * Math.sin(angle) + var x2 = centerX + outerRadius * Math.cos(angle) + var y2 = centerY + outerRadius * Math.sin(angle) + + ctx.beginPath() + ctx.moveTo(x1, y1) + ctx.lineTo(x2, y2) + ctx.strokeStyle = "#ecf0f1" + ctx.lineWidth = 3 + ctx.stroke() + + // Tick labels + var tickValue = minValue + (i / majorTicks) * (maxValue - minValue) + var labelRadius = radius - 40 + var labelX = centerX + labelRadius * Math.cos(angle) + var labelY = centerY + labelRadius * Math.sin(angle) + + ctx.font = "12px Arial" + ctx.fillStyle = "#ecf0f1" + ctx.textAlign = "center" + ctx.textBaseline = "middle" + ctx.fillText(Math.round(tickValue), labelX, labelY) + } + + // Color zones + var normalZone = (warningValue - minValue) / (maxValue - minValue) + var warningZone = (dangerValue - minValue) / (maxValue - minValue) + + // Green zone + ctx.beginPath() + ctx.arc(centerX, centerY, radius - 20, startAngle, + startAngle + normalZone * angleRange, false) + ctx.strokeStyle = "#2ecc71" + ctx.lineWidth = 10 + ctx.stroke() + + // Yellow zone + ctx.beginPath() + ctx.arc(centerX, centerY, radius - 20, + startAngle + normalZone * angleRange, + startAngle + warningZone * angleRange, false) + ctx.strokeStyle = "#f39c12" + ctx.lineWidth = 10 + ctx.stroke() + + // Red zone + ctx.beginPath() + ctx.arc(centerX, centerY, radius - 20, + startAngle + warningZone * angleRange, + endAngle, false) + ctx.strokeStyle = "#e74c3c" + ctx.lineWidth = 10 + ctx.stroke() + + // Needle + var valueAngle = startAngle + ((value - minValue) / (maxValue - minValue)) * angleRange + var needleLength = radius - 30 + + ctx.save() + ctx.translate(centerX, centerY) + ctx.rotate(valueAngle) + + // Needle shadow + ctx.beginPath() + ctx.moveTo(0, 0) + ctx.lineTo(needleLength + 2, 2) + ctx.strokeStyle = "rgba(0,0,0,0.3)" + ctx.lineWidth = 4 + ctx.stroke() + + // Needle + ctx.beginPath() + ctx.moveTo(0, 0) + ctx.lineTo(needleLength, 0) + ctx.strokeStyle = "#e74c3c" + ctx.lineWidth = 3 + ctx.stroke() + + ctx.restore() + + // Center cap + ctx.beginPath() + ctx.arc(centerX, centerY, 10, 0, 2 * Math.PI) + ctx.fillStyle = "#34495e" + ctx.fill() + + ctx.beginPath() + ctx.arc(centerX, centerY, 8, 0, 2 * Math.PI) + ctx.fillStyle = "#e74c3c" + ctx.fill() + } + + Connections { + target: roundGauge + function onValueChanged() { canvas.requestPaint() } + } + } + + // Title + Label { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: 15 + text: title + font.pixelSize: 16 + font.bold: true + color: "#ecf0f1" + } + + // Digital value + Label { + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: 50 + text: value.toFixed(1) + " " + units + font.pixelSize: 24 + font.bold: true + color: "#ecf0f1" + } +} diff --git a/app_qml/ScalarEditor.qml b/app_qml/ScalarEditor.qml new file mode 100644 index 00000000..218bb048 --- /dev/null +++ b/app_qml/ScalarEditor.qml @@ -0,0 +1,41 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +Item { + id: scalarEditor + + ColumnLayout { + anchors.fill: parent + anchors.margins: 20 + spacing: 15 + + Label { + text: "Scalar Value Editor" + font.pixelSize: 16 + font.bold: true + } + + GridLayout { + columns: 2 + rowSpacing: 10 + columnSpacing: 15 + + Label { text: "Value:" } + SpinBox { + from: 0 + to: 1000 + value: 100 + editable: true + } + + Label { text: "Units:" } + ComboBox { + model: ["ms", "degrees", "%", "kPa"] + Layout.preferredWidth: 150 + } + } + + Item { Layout.fillHeight: true } + } +} diff --git a/app_qml/Table2DEditor.qml b/app_qml/Table2DEditor.qml new file mode 100644 index 00000000..f3816716 --- /dev/null +++ b/app_qml/Table2DEditor.qml @@ -0,0 +1,51 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +Item { + id: table2DEditor + + ColumnLayout { + anchors.fill: parent + spacing: 10 + + Label { + text: "2D Table Editor" + font.pixelSize: 16 + font.bold: true + } + + ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + + GridLayout { + columns: 3 + rowSpacing: 5 + columnSpacing: 10 + + // Header + Label { text: "Index"; font.bold: true } + Label { text: "X-Axis"; font.bold: true } + Label { text: "Value"; font.bold: true } + + // Data rows + Repeater { + model: 8 + + delegate: RowLayout { + Label { text: index; width: 40 } + TextField { + text: (index * 500).toFixed(0) + Layout.preferredWidth: 80 + } + TextField { + text: (50 + Math.random() * 50).toFixed(1) + Layout.preferredWidth: 80 + } + } + } + } + } + } +} diff --git a/app_qml/Table3DEditor.qml b/app_qml/Table3DEditor.qml new file mode 100644 index 00000000..ef1d5f30 --- /dev/null +++ b/app_qml/Table3DEditor.qml @@ -0,0 +1,148 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +Item { + id: table3DEditor + + ColumnLayout { + anchors.fill: parent + spacing: 10 + + // Axis labels + RowLayout { + Layout.fillWidth: true + spacing: 10 + + Label { + text: "RPM →" + font.bold: true + } + + Item { Layout.fillWidth: true } + + Label { + text: "↓ MAP (kPa)" + font.bold: true + } + } + + // Table grid + ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + + GridLayout { + id: tableGrid + columns: 13 + rowSpacing: 2 + columnSpacing: 2 + + // Header row (RPM values) + Repeater { + model: 13 + + Rectangle { + width: 60 + height: 30 + color: "#e0e0e0" + border.color: "#999" + border.width: 1 + + Label { + anchors.centerIn: parent + text: index === 0 ? "MAP\\RPM" : (500 + index * 500) + font.bold: true + font.pixelSize: 10 + } + } + } + + // Data rows + Repeater { + model: 12 * 13 + + Rectangle { + width: 60 + height: 35 + color: { + if (index % 13 === 0) return "#e0e0e0" + var value = parseFloat(cellField.text) + if (value < 50) return "#4dabf7" + if (value < 70) return "#51cf66" + if (value < 85) return "#ffd43b" + return "#ff6b6b" + } + border.color: cellMouseArea.containsMouse ? "#1976D2" : "#666" + border.width: cellMouseArea.containsMouse ? 2 : 1 + + Label { + id: cellLabel + anchors.centerIn: parent + text: { + if (index % 13 === 0) { + return (20 + Math.floor(index / 13) * 10) + } + return "" + } + font.bold: index % 13 === 0 + font.pixelSize: 10 + visible: index % 13 === 0 + } + + TextField { + id: cellField + anchors.fill: parent + anchors.margins: 2 + text: (45 + Math.random() * 50).toFixed(1) + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.pixelSize: 11 + font.bold: true + visible: index % 13 !== 0 + + background: Rectangle { + color: "transparent" + } + + onEditingFinished: { + parent.color = Qt.binding(function() { + var value = parseFloat(cellField.text) + if (value < 50) return "#4dabf7" + if (value < 70) return "#51cf66" + if (value < 85) return "#ffd43b" + return "#ff6b6b" + }) + } + } + + MouseArea { + id: cellMouseArea + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + z: -1 + } + } + } + } + } + + // 3D visualization placeholder + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 200 + color: "#2c3e50" + border.color: "#34495e" + border.width: 2 + radius: 5 + + Label { + anchors.centerIn: parent + text: "3D Visualization" + color: "white" + font.pixelSize: 16 + } + } + } +} diff --git a/app_qml/TableEditorPanel.qml b/app_qml/TableEditorPanel.qml new file mode 100644 index 00000000..09318f8a --- /dev/null +++ b/app_qml/TableEditorPanel.qml @@ -0,0 +1,72 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +Item { + id: tableEditorPanel + + property var currentTable: null + + function loadTable(table) { + currentTable = table + if (table.type === "3D") { + stackLayout.currentIndex = 0 + } else if (table.type === "2D") { + stackLayout.currentIndex = 1 + } else { + stackLayout.currentIndex = 2 + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 10 + spacing: 10 + + // Header + RowLayout { + Layout.fillWidth: true + + Label { + text: currentTable ? currentTable.name : "Select a Table" + font.pixelSize: 18 + font.bold: true + Layout.fillWidth: true + } + + Button { + text: "Burn to ECU" + highlighted: true + enabled: currentTable !== null + } + + Button { + text: "Get from ECU" + enabled: currentTable !== null + } + } + + // Table editor stack + StackLayout { + id: stackLayout + Layout.fillWidth: true + Layout.fillHeight: true + currentIndex: 0 + + // 3D Table Editor + Table3DEditor { + id: table3DEditor + } + + // 2D Table Editor + Table2DEditor { + id: table2DEditor + } + + // Scalar Editor + ScalarEditor { + id: scalarEditor + } + } + } +} diff --git a/app_qml/TableEditorView.qml b/app_qml/TableEditorView.qml new file mode 100644 index 00000000..1079b02f --- /dev/null +++ b/app_qml/TableEditorView.qml @@ -0,0 +1,25 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +Item { + id: tableEditorView + + ColumnLayout { + anchors.fill: parent + anchors.margins: 10 + spacing: 10 + + Label { + text: "Advanced Table Editor" + font.pixelSize: 18 + font.bold: true + } + + // Same as tuning view but with additional tools + TuningView { + Layout.fillWidth: true + Layout.fillHeight: true + } + } +} diff --git a/app_qml/TuningView.qml b/app_qml/TuningView.qml new file mode 100644 index 00000000..d88b6fdc --- /dev/null +++ b/app_qml/TuningView.qml @@ -0,0 +1,76 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +Item { + id: tuningView + + SplitView { + anchors.fill: parent + orientation: Qt.Horizontal + + // Left panel - Table selector + Item { + SplitView.minimumWidth: 200 + SplitView.preferredWidth: 250 + SplitView.maximumWidth: 400 + + ColumnLayout { + anchors.fill: parent + anchors.margins: 10 + spacing: 10 + + Label { + text: "Tables" + font.pixelSize: 16 + font.bold: true + } + + ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + + ListView { + id: tableListView + model: tableModel + clip: true + + delegate: ItemDelegate { + width: ListView.view.width + text: modelData.name + highlighted: ListView.isCurrentItem + onClicked: { + tableListView.currentIndex = index + tableEditorPanel.loadTable(modelData) + } + } + } + } + + Button { + Layout.fillWidth: true + text: "New Table" + onClicked: console.log("Create new table") + } + } + } + + // Right panel - Table editor + TableEditorPanel { + id: tableEditorPanel + SplitView.fillWidth: true + } + } + + ListModel { + id: tableModel + ListElement { name: "VE Table"; type: "3D"; rows: 12; cols: 12 } + ListElement { name: "Ignition Advance"; type: "3D"; rows: 12; cols: 12 } + ListElement { name: "Air/Fuel Ratio"; type: "3D"; rows: 12; cols: 12 } + ListElement { name: "Boost Target"; type: "2D"; rows: 8; cols: 1 } + ListElement { name: "Idle Target RPM"; type: "2D"; rows: 8; cols: 1 } + ListElement { name: "Warmup Enrichment"; type: "2D"; rows: 10; cols: 1 } + ListElement { name: "Acceleration Enrichment"; type: "Scalar" } + ListElement { name: "Cranking Pulse"; type: "2D"; rows: 6; cols: 1 } + } +} diff --git a/app_qml/main.py b/app_qml/main.py new file mode 100755 index 00000000..2727529d --- /dev/null +++ b/app_qml/main.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +""" +TunerStudio MS QML Application Launcher +Replica of the Java/Swing TunerStudio application in QML +""" + +import sys +import os +from pathlib import Path +from PyQt6.QtCore import QObject, pyqtSlot, QUrl +from PyQt6.QtGui import QGuiApplication, QClipboard +from PyQt6.QtQml import QQmlApplicationEngine, qmlRegisterType + + +class ClipboardHelper(QObject): + """Helper class for clipboard operations accessible from QML""" + + def __init__(self, app): + super().__init__() + self.app = app + self.clipboard = app.clipboard() + + @pyqtSlot(result=str) + def getClipboardText(self): + """Get text from system clipboard""" + return self.clipboard.text() + + @pyqtSlot(str) + def setClipboardText(self, text): + """Set text to system clipboard""" + self.clipboard.setText(text) + + @pyqtSlot(result=str) + def getHomeDirectory(self): + """Get user's home directory""" + return str(Path.home()) + + @pyqtSlot(str, str, result=bool) + def saveTextToFile(self, file_url, content): + """Save text content to a file""" + try: + # Convert QML file:// URL to path + file_path = QUrl(file_url).toLocalFile() + with open(file_path, 'w') as f: + f.write(content) + return True + except Exception as e: + print(f"Error saving file: {e}") + return False + + @pyqtSlot(str, result=str) + def loadTextFromFile(self, file_url): + """Load text content from a file""" + try: + # Convert QML file:// URL to path + file_path = QUrl(file_url).toLocalFile() + with open(file_path, 'r') as f: + return f.read() + except Exception as e: + print(f"Error loading file: {e}") + return "" + + +def main(): + """Main application entry point""" + + # Set up application + app = QGuiApplication(sys.argv) + app.setApplicationName("TunerStudio MS") + app.setOrganizationName("EFI Analytics") + app.setOrganizationDomain("efianalytics.com") + + # Create QML engine + engine = QQmlApplicationEngine() + + # Create and register clipboard helper + clipboard_helper = ClipboardHelper(app) + engine.rootContext().setContextProperty("clipboardHelper", clipboard_helper) + + # Get the directory where this script is located + script_dir = Path(__file__).parent + qml_file = script_dir / "Main.qml" + + if not qml_file.exists(): + print(f"Error: QML file not found at {qml_file}") + print(f"Current directory: {Path.cwd()}") + print(f"Script directory: {script_dir}") + sys.exit(1) + + # Load the main QML file + engine.load(QUrl.fromLocalFile(str(qml_file))) + + if not engine.rootObjects(): + print("Error: Failed to load QML file") + sys.exit(1) + + print(f"TunerStudio MS QML Application Started") + print(f"Loaded from: {qml_file}") + print(f"\nAvailable views:") + print(f" - Tuning: 3D tables for VE, ignition, AFR") + print(f" - Dashboard: Real-time gauges and indicators") + print(f" - Data Logging: Chart and log viewer") + print(f" - Table Editor: Advanced table editing") + print(f" - Diagnostics: ECU status and error codes") + print(f"\nRegistration dialogs available in separate QML files:") + print(f" - RegistrationDialog.qml") + print(f" - OfflineActivationDialog.qml") + + # Run the application + sys.exit(app.exec()) + + +if __name__ == "__main__": + main()