mirror of
https://github.com/johndoe6345789/tustu.git
synced 2026-04-24 13:45:00 +00:00
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.
This commit is contained in:
132
app_qml/BarGauge.qml
Normal file
132
app_qml/BarGauge.qml
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
352
app_qml/DashboardView.qml
Normal file
352
app_qml/DashboardView.qml
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
259
app_qml/DataLoggingView.qml
Normal file
259
app_qml/DataLoggingView.qml
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
107
app_qml/DiagnosticsView.qml
Normal file
107
app_qml/DiagnosticsView.qml
Normal file
@@ -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 }
|
||||
}
|
||||
}
|
||||
234
app_qml/Main.qml
Normal file
234
app_qml/Main.qml
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
278
app_qml/OfflineActivationDialog.qml
Normal file
278
app_qml/OfflineActivationDialog.qml
Normal file
@@ -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: "<h2>5 Step Offline Activation</h2>" +
|
||||
"<p><b>Step 1</b> - Save Activation Request to File ActivationRequest.txt on a USB drive or other medium.</p>" +
|
||||
"<p><b>Step 2</b> - On a Computer that is connected to the Internet, open a web browser and go to<br>" +
|
||||
" <font color=\"blue\"><u>https://www.efianalytics.com/activate</u></font></p>" +
|
||||
"<p><b>Step 3</b> - Upload your saved ActivationRequest.txt, the site will provide you with ActivationCode.txt</p>" +
|
||||
"<p><b>Step 4</b> - Return to TunerStudio and click Load Activation From File to load ActivationCode.txt into TunerStudio</p>" +
|
||||
"<p><b>Step 5</b> - Click Accept</p>" +
|
||||
"<p><b>Done!</b></p>"
|
||||
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
|
||||
}
|
||||
}
|
||||
129
app_qml/README.md
Normal file
129
app_qml/README.md
Normal file
@@ -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`
|
||||
305
app_qml/RegistrationDialog.qml
Normal file
305
app_qml/RegistrationDialog.qml
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
176
app_qml/RoundGauge.qml
Normal file
176
app_qml/RoundGauge.qml
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
41
app_qml/ScalarEditor.qml
Normal file
41
app_qml/ScalarEditor.qml
Normal file
@@ -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 }
|
||||
}
|
||||
}
|
||||
51
app_qml/Table2DEditor.qml
Normal file
51
app_qml/Table2DEditor.qml
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
148
app_qml/Table3DEditor.qml
Normal file
148
app_qml/Table3DEditor.qml
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
72
app_qml/TableEditorPanel.qml
Normal file
72
app_qml/TableEditorPanel.qml
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
25
app_qml/TableEditorView.qml
Normal file
25
app_qml/TableEditorView.qml
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
76
app_qml/TuningView.qml
Normal file
76
app_qml/TuningView.qml
Normal file
@@ -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 }
|
||||
}
|
||||
}
|
||||
114
app_qml/main.py
Executable file
114
app_qml/main.py
Executable file
@@ -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()
|
||||
Reference in New Issue
Block a user