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:
2026-01-11 03:11:28 +00:00
parent 680251178f
commit 741702cedf
16 changed files with 2499 additions and 0 deletions

132
app_qml/BarGauge.qml Normal file
View 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
View 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
View 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
View 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
View 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"
}
}
}
}

View 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>" +
"&nbsp;&nbsp;&nbsp;&nbsp;<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
View 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`

View 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
View 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
View 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
View 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
View 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
}
}
}
}

View 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
}
}
}
}

View 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
View 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
View 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()