Compare commits

..

4 Commits

Author SHA1 Message Date
rattatwinko
420fb93a05 fix this shit l8tr
All checks were successful
Maven Build / build (push) Successful in 12m28s
2025-06-10 20:05:39 +02:00
rattatwinko
890abc303d shit
All checks were successful
Maven Build / build (push) Successful in 10m22s
2025-06-10 19:47:40 +02:00
rattatwinko
67e707eac8 very good very nice, C++ working now! so you can test the app with a ESP32's serial port
All checks were successful
Maven Build / build (push) Successful in 10m13s
2025-06-10 14:15:06 +02:00
rattatwinko
c6602315ba fuck you all
All checks were successful
Maven Build / build (push) Successful in 10m48s
2025-06-09 21:47:58 +02:00
10 changed files with 1091 additions and 268 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
*.py linguist-vendored

View File

@@ -0,0 +1,55 @@
name: Maven Build
on:
push:
branches: [ main, master, dev ]
pull_request:
branches: [ main, master ]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
cache: 'maven'
- name: Install Maven
run: |
if ! command -v mvn &> /dev/null; then
echo "Maven not found, installing..."
sudo apt-get update
sudo apt-get install -y maven
fi
mvn --version
- name: Debug Info
run: |
echo "Current workspace directory: $GITHUB_WORKSPACE"
echo "Current directory: $(pwd)"
echo "Project structure:"
find . -type f -name "*.kt" | sort
find . -type f -name "pom.xml"
echo "Maven version: $(mvn --version)"
- name: Build PuchDyno (PuchDyno)
run: |
echo "Building PuchDyno"
echo "Current directory: $(pwd)"
# Run Maven build directly using the POM file path
mvn -B clean package -f "$GITHUB_WORKSPACE/pom.xml" -Dmaven.compiler.failOnError=true
- name: Upload PuchDyno artifact
uses: actions/upload-artifact@v3
with:
name: PuchDyno
path: target/PuchDyno-*.jar
if-no-files-found: error

4
.gitignore vendored
View File

@@ -2,6 +2,9 @@
# Compiled class file # Compiled class file
*.class *.class
# ---> CSV
*.csv
# Log file # Log file
*.log *.log
@@ -64,3 +67,4 @@ target/maven-archiver/pom.properties
target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
>>>>>>> f462d53 (yessssssssss!) >>>>>>> f462d53 (yessssssssss!)
logs/dyno_2025-06-10_19-41-50.csv

View File

@@ -1,10 +1,201 @@
#include <WiFi.h>
// Mock dyno simulation parameters
const int SERIAL_BAUD_RATE = 9600;
const int DATA_SEND_INTERVAL_MS = 100; // Send data every 100ms
const int SIMULATION_STEP_MS = 50; // Update simulation every 50ms
// Simulation state
enum SimulationState {
IDLE,
ACCELERATING,
HOLDING,
DECELERATING
};
SimulationState currentState = IDLE;
unsigned long lastDataSend = 0;
unsigned long lastSimulationUpdate = 0;
unsigned long stateStartTime = 0;
// RPM simulation variables
float currentRPM = 0.0;
float targetRPM = 0.0;
float rpmAcceleration = 0.0;
float maxRPM = 11000.0;
float minRPM = 800.0; // Idle RPM
float idleRPM = 800.0;
// Dyno run parameters
const float ACCELERATION_RATE = 25.0; // RPM per 100ms during acceleration
const float DECELERATION_RATE = 30.0; // RPM per 100ms during deceleration
const int HOLD_DURATION_MS = 2000; // Hold at max RPM for 2 seconds
const int IDLE_DURATION_MS = 3000; // Stay at idle for 3 seconds
// Add some realistic noise and variations
float rpmNoise = 0.0;
int noiseCounter = 0;
void setup() { void setup() {
Serial.begin(9600); // Ensure this matches the baud rate in Main.java Serial.begin(SERIAL_BAUD_RATE);
// Initialize random seed
randomSeed(analogRead(0));
// Wait for serial to initialize
delay(2000);
Serial.println("ESP32 Mock Dyno Data Generator");
Serial.println("Sending RPM data for dyno simulation...");
Serial.println("==================================");
// Start at idle
currentRPM = idleRPM;
targetRPM = idleRPM;
currentState = IDLE;
stateStartTime = millis();
delay(1000);
} }
void loop() { void loop() {
// Replace this with your actual RPM measurement logic unsigned long currentTime = millis();
double currentRPM = 1500.0; // Example RPM value
Serial.println(currentRPM); // Update simulation state
delay(100); // Send data every 100ms, adjust as needed if (currentTime - lastSimulationUpdate >= SIMULATION_STEP_MS) {
updateSimulation();
lastSimulationUpdate = currentTime;
}
// Send data to Java application
if (currentTime - lastDataSend >= DATA_SEND_INTERVAL_MS) {
sendRPMData();
lastDataSend = currentTime;
}
delay(10); // Small delay to prevent excessive CPU usage
} }
void updateSimulation() {
unsigned long currentTime = millis();
unsigned long stateElapsed = currentTime - stateStartTime;
switch (currentState) {
case IDLE:
// Stay at idle RPM for a while, then start accelerating
targetRPM = idleRPM + random(-50, 51); // Add some idle variation
if (stateElapsed > IDLE_DURATION_MS) {
currentState = ACCELERATING;
targetRPM = maxRPM;
stateStartTime = currentTime;
Serial.println("Starting dyno run - ACCELERATING");
}
break;
case ACCELERATING:
// Accelerate towards max RPM
if (currentRPM < maxRPM - 100) {
currentRPM += ACCELERATION_RATE * (SIMULATION_STEP_MS / 100.0);
// Add some realistic acceleration curve (not perfectly linear)
float accelerationFactor = 1.0 - (currentRPM / maxRPM) * 0.3;
currentRPM = currentRPM * accelerationFactor + currentRPM * (1.0 - accelerationFactor);
} else {
currentState = HOLDING;
stateStartTime = currentTime;
Serial.println("Reached max RPM - HOLDING");
}
break;
case HOLDING:
// Hold at max RPM with some variation
targetRPM = maxRPM + random(-100, 101);
if (stateElapsed > HOLD_DURATION_MS) {
currentState = DECELERATING;
targetRPM = idleRPM;
stateStartTime = currentTime;
Serial.println("Starting deceleration - DECELERATING");
}
break;
case DECELERATING:
// Decelerate back to idle
if (currentRPM > idleRPM + 100) {
currentRPM -= DECELERATION_RATE * (SIMULATION_STEP_MS / 100.0);
// Add engine braking effect (faster deceleration at higher RPM)
float brakingFactor = 1.0 + (currentRPM / maxRPM) * 0.5;
currentRPM -= (DECELERATION_RATE * brakingFactor - DECELERATION_RATE) * (SIMULATION_STEP_MS / 100.0);
} else {
currentRPM = idleRPM;
currentState = IDLE;
stateStartTime = currentTime;
Serial.println("Returned to idle - IDLE");
Serial.println("==================================");
}
break;
}
// Add realistic noise and fluctuations
addRealisticNoise();
// Ensure RPM doesn't go below 0 or above reasonable limits
currentRPM = constrain(currentRPM, 0, maxRPM + 500);
}
void addRealisticNoise() {
// Add different types of noise based on RPM range
float baseNoise = random(-10, 11); // Basic noise
// Engine vibration increases with RPM
float vibrationNoise = (currentRPM / 1000.0) * random(-5, 6);
// Periodic fluctuations (simulating engine cycles)
noiseCounter++;
float periodicNoise = sin(noiseCounter * 0.1) * (currentRPM / 2000.0) * 3.0;
// Combine all noise sources
rpmNoise = baseNoise + vibrationNoise + periodicNoise;
// Apply noise with some smoothing
currentRPM += rpmNoise * 0.3; // Reduce noise impact
}
void sendRPMData() {
// Send just the RPM value as your Java code expects
Serial.println(currentRPM, 0); // Send as integer (no decimal places)
// Optional: Send debug info to Serial Monitor (comment out for production)
// Serial.print("DEBUG - State: ");
// Serial.print(getStateString());
// Serial.print(", RPM: ");
// Serial.print(currentRPM, 1);
// Serial.print(", Noise: ");
// Serial.println(rpmNoise, 1);
}
String getStateString() {
switch (currentState) {
case IDLE: return "IDLE";
case ACCELERATING: return "ACCELERATING";
case HOLDING: return "HOLDING";
case DECELERATING: return "DECELERATING";
default: return "UNKNOWN";
}
}
// Function to manually trigger a dyno run (call this if you want manual control)
void triggerDynoRun() {
if (currentState == IDLE) {
currentState = ACCELERATING;
stateStartTime = millis();
Serial.println("Manual dyno run triggered!");
}
}
// Function to reset to idle (emergency stop)
void resetToIdle() {
currentState = IDLE;
currentRPM = idleRPM;
targetRPM = idleRPM;
stateStartTime = millis();
Serial.println("Reset to idle!");
}

10
build.sh Executable file
View File

@@ -0,0 +1,10 @@
#!/bin/bash
# Direct build script for Maven project
echo "Current directory: $(pwd)"
echo "Building project with Maven..."
# Run Maven build using the exact pom.xml location
mvn clean package -f "$(pwd)/pom.xml"
echo "Build complete. JAR file should be in target/ directory."

291
pommer.py vendored Normal file
View File

@@ -0,0 +1,291 @@
#!/usr/bin/env python3
"""
POMMER.PY
THIS IS PROPRIETARY SOFTWARE DO NOT DISTRIBUTE TO OUTSIDERS!
This Python File is distributed with every Kotlin Plugin Repository!
If you find this to be confusing to use look at the Documentation in "rattatwinko/pommer"
Run this Script with Python 3.11 ; 3.9
This YET only works with Maven!
"""
import os
import xml.etree.ElementTree as ET
import re
from pathlib import Path
import argparse
import glob
def parse_pom_xml(pom_path):
"""
Parse a pom.xml file and extract relevant information
"""
try:
print(f"Parsing POM file: {pom_path}")
# Register the default namespace
ET.register_namespace('', "http://maven.apache.org/POM/4.0.0")
# Parse the XML file
tree = ET.parse(pom_path)
root = tree.getroot()
# Define namespace for easier XPath queries
ns = {'mvn': "http://maven.apache.org/POM/4.0.0"}
# Extract project info
artifact_id = root.find('./mvn:artifactId', ns).text
group_id = root.find('./mvn:groupId', ns).text if root.find('./mvn:groupId', ns) is not None else "unknown"
version = root.find('./mvn:version', ns).text if root.find('./mvn:version', ns) is not None else "unknown"
name = root.find('./mvn:name', ns).text if root.find('./mvn:name', ns) is not None else artifact_id
# Extract Java version
java_version_elem = root.find('./mvn:properties/mvn:java.version', ns)
java_version = java_version_elem.text if java_version_elem is not None else "17" # Default to Java 17 if not specified
# Extract packaging type (default to jar if not specified)
packaging = root.find('./mvn:packaging', ns)
packaging = packaging.text if packaging is not None else "jar"
# Check if Kotlin is used
kotlin_version_elem = root.find('./mvn:properties/mvn:kotlin.version', ns)
kotlin_version = kotlin_version_elem.text if kotlin_version_elem is not None else None
# Check for Kotlin plugin or dependency
kotlin_plugin = None
kotlin_dep = None
# Check for Kotlin plugin
plugins = root.findall('.//mvn:plugin', ns)
for plugin in plugins:
plugin_group = plugin.find('./mvn:groupId', ns)
if plugin_group is not None and plugin_group.text == 'org.jetbrains.kotlin':
kotlin_plugin = plugin
break
# Check for Kotlin dependency
deps = root.findall('.//mvn:dependency', ns)
for dep in deps:
dep_group = dep.find('./mvn:groupId', ns)
dep_artifact = dep.find('./mvn:artifactId', ns)
if (dep_group is not None and dep_group.text == 'org.jetbrains.kotlin' and
dep_artifact is not None and 'kotlin-stdlib' in dep_artifact.text):
kotlin_dep = dep
break
# Determine if this is a Kotlin project
is_kotlin = kotlin_version is not None or kotlin_plugin is not None or kotlin_dep is not None
# Check for source directories
source_dir = None
source_dirs = root.findall('.//mvn:sourceDirectory', ns)
if source_dirs:
source_dir = source_dirs[0].text
# Check for default goal (to use the same command as IntelliJ)
default_goal = None
default_goal_elem = root.find('./mvn:build/mvn:defaultGoal', ns)
if default_goal_elem is not None:
default_goal = default_goal_elem.text
return {
"artifact_id": artifact_id,
"group_id": group_id,
"version": version,
"name": name,
"java_version": java_version,
"packaging": packaging,
"is_kotlin": is_kotlin,
"kotlin_version": kotlin_version,
"source_dir": source_dir,
"default_goal": default_goal,
"pom_path": pom_path
}
except Exception as e:
print(f"Error parsing {pom_path}: {e}")
return None
def generate_gitea_workflow(pom_infos):
"""
Generate a Gitea workflow YAML file based on multiple POM information
"""
if not pom_infos:
print("No valid POM files found")
return None
# Get the highest Java version required
java_version = max([info["java_version"] for info in pom_infos])
# Check if any project uses Kotlin
uses_kotlin = any(info["is_kotlin"] for info in pom_infos)
# Kotlin version (if any)
kotlin_version = None
for info in pom_infos:
if info["kotlin_version"]:
kotlin_version = info["kotlin_version"]
break
# Construct the workflow content
workflow_content = f"""name: Maven Build
on:
push:
branches: [ main, master, dev ]
pull_request:
branches: [ main, master ]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK {java_version}
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '{java_version}'
cache: 'maven'
- name: Install Maven
run: |
if ! command -v mvn &> /dev/null; then
echo "Maven not found, installing..."
sudo apt-get update
sudo apt-get install -y maven
fi
mvn --version
- name: Debug Info
run: |
echo "Current workspace directory: $GITHUB_WORKSPACE"
echo "Current directory: $(pwd)"
echo "Project structure:"
find . -type f -name "*.kt" | sort
find . -type f -name "pom.xml"
echo "Maven version: $(mvn --version)"
"""
# Add individual build steps for each POM
for i, info in enumerate(pom_infos):
# Determine the Maven command to use (same as the default goal if specified)
maven_command = "clean package"
if info["default_goal"]:
maven_command = info["default_goal"]
workflow_content += f"""
- name: Build {info["name"]} ({info["artifact_id"]})
run: |
echo "Building {info["artifact_id"]}"
echo "Current directory: $(pwd)"
# Run Maven build directly using the POM file path
mvn -B {maven_command} -f "$GITHUB_WORKSPACE/pom.xml" -Dmaven.compiler.failOnError=true
"""
# Add artifact upload step
workflow_content += f"""
- name: Upload {info["artifact_id"]} artifact
uses: actions/upload-artifact@v3
with:
name: {info["artifact_id"]}
path: target/{info['artifact_id']}-*.jar
if-no-files-found: error
"""
return workflow_content
def find_pom_files(base_dir="."):
"""Find all pom.xml files in the given directory and subdirectories"""
return glob.glob(f"{base_dir}/**/pom.xml", recursive=True)
def main():
parser = argparse.ArgumentParser(description='Generate Gitea workflow for Maven/Kotlin projects')
parser.add_argument('--dir', '-d', default='.', help='Base directory to search for pom.xml files')
parser.add_argument('--specific-pom', '-p', help='Path to a specific pom.xml file to process')
args = parser.parse_args()
pom_files = []
if args.specific_pom:
pom_files = [args.specific_pom]
else:
pom_files = find_pom_files(args.dir)
if not pom_files:
print(f"No pom.xml files found in {args.dir}")
return
print(f"Found {len(pom_files)} pom.xml files")
# Parse all POM files
pom_infos = []
for pom_file in pom_files:
info = parse_pom_xml(pom_file)
if info:
pom_infos.append(info)
if not pom_infos:
print("No valid POM files could be parsed")
return
# Generate the workflow content
workflow_content = generate_gitea_workflow(pom_infos)
if not workflow_content:
return
# Create the .gitea/workflows directory if it doesn't exist
workflow_dir = Path(".gitea/workflows")
workflow_dir.mkdir(parents=True, exist_ok=True)
# Write the workflow file
workflow_file = workflow_dir / "maven_build.yaml"
with open(workflow_file, "w") as f:
f.write(workflow_content)
print(f"Gitea workflow generated at: {workflow_file}")
print(f"This workflow will build {len(pom_infos)} Maven projects")
# Print summary of detected projects
print("\nDetected projects:")
for info in pom_infos:
kotlin_info = "with Kotlin" if info["is_kotlin"] else "Java only"
build_command = info["default_goal"] if info["default_goal"] else "clean package"
print(
f"- {info['name']} ({info['artifact_id']}): {kotlin_info}, Java {info['java_version']}, build: {build_command}")
# Create a simple direct build script as fallback
with open("build.sh", "w") as f:
f.write("""#!/bin/bash
# Direct build script for Maven project
echo "Current directory: $(pwd)"
echo "Building project with Maven..."
# Run Maven build using the exact pom.xml location
mvn clean package -f "$(pwd)/pom.xml"
echo "Build complete. JAR file should be in target/ directory."
""")
# Make it executable
os.chmod("build.sh", 0o755)
print(f"Simple build script generated at: build.sh")
if __name__ == "__main__":
main()

View File

@@ -16,7 +16,7 @@ public class AnalogMeter extends JPanel {
this.maxValue = maxValue; this.maxValue = maxValue;
this.currentValue = 0.0; this.currentValue = 0.0;
this.needleColor = needleColor; this.needleColor = needleColor;
setPreferredSize(new Dimension(200, 200)); setPreferredSize(new Dimension(250, 250));
} }
public void setValue(double value) { public void setValue(double value) {
@@ -41,8 +41,9 @@ public class AnalogMeter extends JPanel {
int centerY = getHeight() / 2; int centerY = getHeight() / 2;
int radius = Math.min(centerX, centerY) - 10; int radius = Math.min(centerX, centerY) - 10;
// Draw meter background // Draw meter background with gradient
g2d.setColor(Color.LIGHT_GRAY); GradientPaint gradient = new GradientPaint(0, 0, Color.LIGHT_GRAY, 0, getHeight(), Color.WHITE);
g2d.setPaint(gradient);
g2d.fillOval(centerX - radius, centerY - radius, radius * 2, radius * 2); g2d.fillOval(centerX - radius, centerY - radius, radius * 2, radius * 2);
// Draw scale and ticks // Draw scale and ticks
@@ -73,7 +74,7 @@ public class AnalogMeter extends JPanel {
int needleX = (int) (centerX + needleLength * Math.cos(angle)); int needleX = (int) (centerX + needleLength * Math.cos(angle));
int needleY = (int) (centerY - needleLength * Math.sin(angle)); int needleY = (int) (centerY - needleLength * Math.sin(angle));
g2d.setStroke(new BasicStroke(3)); g2d.setStroke(new BasicStroke(4)); // Thicker needle
g2d.drawLine(centerX, centerY, needleX, needleY); g2d.drawLine(centerX, centerY, needleX, needleY);
g2d.fillOval(centerX - 5, centerY - 5, 10, 10); g2d.fillOval(centerX - 5, centerY - 5, 10, 10);
@@ -82,6 +83,6 @@ public class AnalogMeter extends JPanel {
g2d.setFont(new Font("Arial", Font.BOLD, 16)); g2d.setFont(new Font("Arial", Font.BOLD, 16));
FontMetrics fm = g2d.getFontMetrics(); FontMetrics fm = g2d.getFontMetrics();
int labelWidth = fm.stringWidth(label); int labelWidth = fm.stringWidth(label);
g2d.drawString(label, centerX - labelWidth / 2, centerY + radius + 20); g2d.drawString(label, centerX - labelWidth / 2, centerY + radius + 30); // Adjusted position
} }
} }

View File

@@ -0,0 +1,53 @@
package com.puchdyno;
import java.io.BufferedWriter;
import java.io.Closeable;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* Utility class that appends each sample coming from the dyno run to a csv file.
* The file will be created the first time it is needed and automatically closed
* when the logger is closed.
*/
public class CSVLogger implements Closeable {
private static final DateTimeFormatter FILE_TS = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss");
private static final DateTimeFormatter ROW_TS = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
private final BufferedWriter writer;
/**
* Creates a new logger that writes to the given directory using the current timestamp as file name.
*/
public CSVLogger(File directory) throws IOException {
if (!directory.exists() && !directory.mkdirs()) {
throw new IOException("Unable to create log directory " + directory);
}
File file = new File(directory, "dyno_" + LocalDateTime.now().format(FILE_TS) + ".csv");
boolean created = file.createNewFile();
writer = new BufferedWriter(new FileWriter(file, true));
if (created) {
writer.write("timestamp,rpm,ps,nm");
writer.newLine();
}
}
public synchronized void log(double rpm, double ps, double nm) {
try {
writer.write(String.format("%s,%.2f,%.2f,%.2f", LocalDateTime.now().format(ROW_TS), rpm, ps, nm));
writer.newLine();
} catch (IOException e) {
// we can't do much more than print the stacktrace at this point
e.printStackTrace();
}
}
@Override
public void close() throws IOException {
writer.flush();
writer.close();
}
}

View File

@@ -2,99 +2,186 @@ package com.puchdyno;
import javax.swing.*; import javax.swing.*;
import java.awt.*; import java.awt.*;
import org.jfree.chart.ChartFactory; import java.time.LocalDate;
import org.jfree.chart.ChartPanel; import java.time.format.DateTimeFormatter;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.plot.PlotOrientation; import org.jfree.chart.*;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.axis.NumberAxis; import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.plot.*;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.data.xy.*;
import org.jfree.chart.renderer.xy.XYSplineRenderer;
import java.awt.BasicStroke;
public class DynoGUI extends JFrame { public class DynoGUI extends JFrame {
private JLabel leistungLabel; private final JLabel leistungLabel;
private JLabel drehmomentLabel; private final JLabel drehmomentLabel;
private AnalogMeter rpmMeter; private final AnalogMeter rpmMeter;
private AnalogMeter hpMeter; private final AnalogMeter hpMeter;
private XYSeries powerSeries; private final XYSeries powerSeries;
private XYSeries torqueSeries; private final XYSeries torqueSeries;
private ChartPanel chartPanel; private final XYSeries efficiencySeries;
private final ChartPanel chartPanel;
private final JLabel maxLeistungLabel;
private final JLabel maxDrehmomentLabel;
private double maxLeistung = 0.0;
private double maxDrehmoment = 0.0;
private double maxLeistungRpm = 0.0;
private double maxLeistungNmAtHp = 0.0;
private double maxDrehmomentRpm = 0.0;
private double maxDrehmomentPsAtNm = 0.0;
public DynoGUI() { public DynoGUI() {
setTitle("Puch Maxi Dyno"); setTitle("Puch Maxi Dyno");
setSize(1000, 700); // Increased size for more elements setSize(1000, 700);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(new BorderLayout()); setLayout(new BorderLayout(10, 10));
// Top panel for analog meters // Top panel: analog meters
JPanel meterPanel = new JPanel(new GridLayout(1, 2)); JPanel meterPanel = new JPanel(new GridLayout(1, 2, 20, 20));
rpmMeter = new AnalogMeter("RPM", 12000, Color.BLUE); // Max RPM, adjusted for new test range meterPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
hpMeter = new AnalogMeter("Horse Power", 15, Color.BLUE); // Max HP, e.g., 15 rpmMeter = new AnalogMeter("RPM", 12000, Color.BLUE);
meterPanel.add(rpmMeter); hpMeter = new AnalogMeter("Horse Power", 15, Color.RED);
meterPanel.add(hpMeter);
rpmMeter.setToolTipText("Motordrehzahl in U/min");
hpMeter.setToolTipText("Motorleistung in PS");
JPanel rpmWrapper = new JPanel(new BorderLayout());
rpmWrapper.setBorder(BorderFactory.createTitledBorder("Drehzahl [RPM]"));
rpmWrapper.add(rpmMeter, BorderLayout.CENTER);
JPanel hpWrapper = new JPanel(new BorderLayout());
hpWrapper.setBorder(BorderFactory.createTitledBorder("Leistung [PS]"));
hpWrapper.add(hpMeter, BorderLayout.CENTER);
meterPanel.add(rpmWrapper);
meterPanel.add(hpWrapper);
add(meterPanel, BorderLayout.NORTH); add(meterPanel, BorderLayout.NORTH);
// Center panel for labels and chart // Center panel: chart and labels
JPanel centerPanel = new JPanel(new BorderLayout()); JPanel centerPanel = new JPanel(new BorderLayout(10, 10));
centerPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
JPanel labelPanel = new JPanel(new GridLayout(2, 1));
leistungLabel = new JLabel("Leistung: 0.00 PS", SwingConstants.CENTER); // Labels panel with BoxLayout for better scaling
drehmomentLabel = new JLabel("Drehmoment: 0.00 Nm", SwingConstants.CENTER); JPanel labelPanel = new JPanel();
leistungLabel.setFont(new Font("Arial", Font.BOLD, 24)); labelPanel.setLayout(new BoxLayout(labelPanel, BoxLayout.Y_AXIS));
drehmomentLabel.setFont(new Font("Arial", Font.BOLD, 24)); labelPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
leistungLabel = new JLabel("Leistung: 0.00 PS");
drehmomentLabel = new JLabel("Drehmoment: 0.00 Nm");
maxLeistungLabel = new JLabel("Max Leistung: 0.00 PS");
maxDrehmomentLabel = new JLabel("Max Drehmoment: 0.00 Nm");
leistungLabel.setFont(new Font("SansSerif", Font.BOLD, 24));
drehmomentLabel.setFont(new Font("SansSerif", Font.BOLD, 24));
maxLeistungLabel.setFont(new Font("SansSerif", Font.PLAIN, 14));
maxDrehmomentLabel.setFont(new Font("SansSerif", Font.PLAIN, 14));
leistungLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
drehmomentLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
maxLeistungLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
maxDrehmomentLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
labelPanel.add(leistungLabel); labelPanel.add(leistungLabel);
labelPanel.add(drehmomentLabel); labelPanel.add(drehmomentLabel);
labelPanel.add(Box.createVerticalStrut(5));
labelPanel.add(maxLeistungLabel);
labelPanel.add(Box.createVerticalStrut(5));
labelPanel.add(maxDrehmomentLabel);
centerPanel.add(labelPanel, BorderLayout.NORTH); centerPanel.add(labelPanel, BorderLayout.NORTH);
// Chart setup // Data series
powerSeries = new XYSeries("Power (PS)"); powerSeries = new XYSeries("Power (PS)");
torqueSeries = new XYSeries("Torque (Nm)"); torqueSeries = new XYSeries("Torque (Nm)");
XYSeriesCollection dataset = new XYSeriesCollection(); efficiencySeries = new XYSeries("Efficiency");
dataset.addSeries(powerSeries);
dataset.addSeries(torqueSeries);
XYSeriesCollection powerDataset = new XYSeriesCollection(powerSeries);
XYSeriesCollection torqueDataset = new XYSeriesCollection(torqueSeries);
XYSeriesCollection efficiencyDataset = new XYSeriesCollection(efficiencySeries);
// Date for chart title
String dateStr = LocalDate.now().format(DateTimeFormatter.ofPattern("dd.MM.yyyy"));
// Create chart
JFreeChart chart = ChartFactory.createXYLineChart( JFreeChart chart = ChartFactory.createXYLineChart(
"Leistungsdiagramm - Motorleistung", "LeistungsPrüfstand - Puch Maxi (" + dateStr + ")",
"U/min", "U/min",
"Leistung [PS]", "Leistung [PS]",
dataset, powerDataset,
PlotOrientation.VERTICAL, PlotOrientation.VERTICAL,
true, true,
true, true,
false false
); );
XYPlot plot = chart.getXYPlot(); chart.setBackgroundPaint(Color.WHITE);
chart.getTitle().setFont(new Font("SansSerif", Font.BOLD, 18));
XYPlot plot = chart.getXYPlot();
plot.setBackgroundPaint(new Color(245, 245, 245));
plot.setDomainGridlinePaint(Color.LIGHT_GRAY);
plot.setRangeGridlinePaint(Color.LIGHT_GRAY);
plot.setOutlineVisible(false);
plot.setDomainCrosshairVisible(true);
plot.setRangeCrosshairVisible(true);
// Power axis (left)
NumberAxis powerAxis = (NumberAxis) plot.getRangeAxis(); NumberAxis powerAxis = (NumberAxis) plot.getRangeAxis();
powerAxis.setRange(0.0, 10.0); powerAxis.setRange(0.0, 10.0);
powerAxis.setLabel("Leistung [PS]"); powerAxis.setLabel("Leistung [PS]");
powerAxis.setTickLabelFont(new Font("SansSerif", Font.PLAIN, 12));
powerAxis.setLabelFont(new Font("SansSerif", Font.BOLD, 14));
// Torque axis (right)
NumberAxis torqueAxis = new NumberAxis("Drehmoment [Nm]"); NumberAxis torqueAxis = new NumberAxis("Drehmoment [Nm]");
torqueAxis.setRange(0.0, 15.0); torqueAxis.setRange(0.0, 15.0);
torqueAxis.setTickLabelFont(new Font("SansSerif", Font.PLAIN, 12));
torqueAxis.setLabelFont(new Font("SansSerif", Font.BOLD, 14));
plot.setRangeAxis(1, torqueAxis); plot.setRangeAxis(1, torqueAxis);
plot.setDataset(1, torqueDataset);
plot.mapDatasetToRangeAxis(1, 1); plot.mapDatasetToRangeAxis(1, 1);
// Efficiency axis (additional, right side 2)
NumberAxis effAxis = new NumberAxis("Effizienz [arb.u.]");
effAxis.setAutoRangeIncludesZero(false);
effAxis.setTickLabelFont(new Font("SansSerif", Font.PLAIN, 12));
effAxis.setLabelFont(new Font("SansSerif", Font.BOLD, 14));
plot.setRangeAxis(2, effAxis);
plot.setDataset(2, efficiencyDataset);
plot.mapDatasetToRangeAxis(2, 2);
// X axis
NumberAxis xAxis = (NumberAxis) plot.getDomainAxis(); NumberAxis xAxis = (NumberAxis) plot.getDomainAxis();
xAxis.setRange(0.0, 12000.0); xAxis.setRange(0.0, 12000.0);
xAxis.setLabel("U/min"); xAxis.setLabel("U/min");
xAxis.setTickLabelFont(new Font("SansSerif", Font.PLAIN, 12));
xAxis.setLabelFont(new Font("SansSerif", Font.BOLD, 14));
XYLineAndShapeRenderer renderer1 = new XYLineAndShapeRenderer(); // Renderers
renderer1.setSeriesPaint(0, Color.RED); org.jfree.chart.renderer.xy.XYSplineRenderer powerRenderer = new org.jfree.chart.renderer.xy.XYSplineRenderer();
renderer1.setSeriesShapesVisible(0, false); powerRenderer.setSeriesPaint(0, new Color(220, 50, 47)); // Red
plot.setRenderer(0, renderer1); powerRenderer.setSeriesStroke(0, new BasicStroke(2f));
plot.setRenderer(0, powerRenderer);
XYLineAndShapeRenderer renderer2 = new XYLineAndShapeRenderer(); org.jfree.chart.renderer.xy.XYSplineRenderer torqueRenderer = new org.jfree.chart.renderer.xy.XYSplineRenderer();
renderer2.setSeriesPaint(0, Color.BLUE); torqueRenderer.setSeriesPaint(0, new Color(38, 139, 210)); // Blue
renderer2.setSeriesShapesVisible(0, false); torqueRenderer.setSeriesStroke(0, new BasicStroke(2f));
plot.setRenderer(1, renderer2); plot.setRenderer(1, torqueRenderer);
XYLineAndShapeRenderer effRenderer = new XYLineAndShapeRenderer(false, true);
effRenderer.setSeriesPaint(0, new Color(133, 153, 0)); // green
effRenderer.setSeriesShapesVisible(0, true);
effRenderer.setSeriesShape(0, new java.awt.geom.Ellipse2D.Double(-4, -4, 8, 8));
plot.setRenderer(2, effRenderer);
// Chart panel setup
chartPanel = new ChartPanel(chart); chartPanel = new ChartPanel(chart);
chartPanel.setPreferredSize(new Dimension(600, 400)); chartPanel.setPreferredSize(new Dimension(800, 400));
chartPanel.setMouseWheelEnabled(true);
chartPanel.setPopupMenu(null);
centerPanel.add(chartPanel, BorderLayout.CENTER); centerPanel.add(chartPanel, BorderLayout.CENTER);
add(centerPanel, BorderLayout.CENTER);
add(centerPanel, BorderLayout.CENTER);
setVisible(true); setVisible(true);
} }
@@ -102,7 +189,6 @@ public class DynoGUI extends JFrame {
rpmMeter.setValue(rpm); rpmMeter.setValue(rpm);
hpMeter.setValue(leistung); hpMeter.setValue(leistung);
// Change HP meter needle color based on performance
if (leistung >= 15.0) { if (leistung >= 15.0) {
hpMeter.setNeedleColor(Color.RED); hpMeter.setNeedleColor(Color.RED);
} else if (leistung >= 10.0) { } else if (leistung >= 10.0) {
@@ -114,17 +200,65 @@ public class DynoGUI extends JFrame {
leistungLabel.setText(String.format("Leistung: %.2f PS", leistung)); leistungLabel.setText(String.format("Leistung: %.2f PS", leistung));
drehmomentLabel.setText(String.format("Drehmoment: %.2f Nm", drehmoment)); drehmomentLabel.setText(String.format("Drehmoment: %.2f Nm", drehmoment));
// Add data to chart // keine Plot-Aktualisierung hier siehe addPlotData()
powerSeries.add(rpm, leistung);
torqueSeries.add(rpm, drehmoment);
} }
public void resetChart() { public void resetChart() {
powerSeries.clear(); powerSeries.clear();
torqueSeries.clear(); torqueSeries.clear();
efficiencySeries.clear();
}
public void resetMaxHold() {
maxLeistung = 0.0;
maxDrehmoment = 0.0;
maxLeistungRpm = 0.0;
maxLeistungNmAtHp = 0.0;
maxDrehmomentRpm = 0.0;
maxDrehmomentPsAtNm = 0.0;
efficiencySeries.clear();
maxLeistungLabel.setText("Max Leistung: 0.00 PS");
maxDrehmomentLabel.setText("Max Drehmoment: 0.00 Nm");
} }
public JFreeChart getChart() { public JFreeChart getChart() {
return chartPanel.getChart(); return chartPanel.getChart();
} }
}
/**
* Fügt Datenpunkte in den Diagrammen hinzu. Nur während aktiver Messung aufrufen.
*/
public void addPlotData(double leistung, double drehmoment, double rpm) {
boolean effUpdated = false;
if (leistung > maxLeistung) {
maxLeistung = leistung;
maxLeistungLabel.setText(String.format("Max Leistung: %.2f PS", maxLeistung));
maxLeistungRpm = rpm;
maxLeistungNmAtHp = drehmoment;
effUpdated = true;
}
if (drehmoment > maxDrehmoment) {
maxDrehmoment = drehmoment;
maxDrehmomentLabel.setText(String.format("Max Drehmoment: %.2f Nm", maxDrehmoment));
maxDrehmomentRpm = rpm;
maxDrehmomentPsAtNm = leistung;
effUpdated = true;
}
if (effUpdated) {
efficiencySeries.clear();
if (maxLeistungNmAtHp > 0) {
efficiencySeries.add(maxLeistungRpm, maxLeistung / maxLeistungNmAtHp);
}
if (maxDrehmoment > 0) {
double effNm = maxDrehmomentPsAtNm == 0 ? 0.0 : maxDrehmomentPsAtNm / maxDrehmoment;
if (maxDrehmomentRpm != maxLeistungRpm) {
efficiencySeries.add(maxDrehmomentRpm, effNm);
}
}
}
powerSeries.add(rpm, leistung);
torqueSeries.add(rpm, drehmoment);
}
}

View File

@@ -29,66 +29,36 @@ public class Main {
private static LeistungBerechner leistungBerechner; private static LeistungBerechner leistungBerechner;
private static long lastTimestamp = 0; private static long lastTimestamp = 0;
private static double lastOmega = 0.0; private static double lastOmega = 0.0;
private static final double WHEEL_RADIUS_METERS = 0.25; // Example: 25 cm radius for the dyno wheel private static final double WHEEL_RADIUS_METERS = 0.25;
private static boolean testMode = false; private static boolean testMode = false;
private static ScheduledExecutorService testDataExecutor; private static ScheduledExecutorService testDataExecutor;
private static int testRpmCounter = 1000; private static int testRpmCounter = 1000;
private static final double[] TEST_RPM_VALUES = {
0, 500, 1000, 1500, 2000, 2500, 3000, 3500,
4000, 4500, 5000, 5500, 6000, 6500, 7000, 7500, 8000, 8500,
9000, 9500, 10000, 10500, 11000, 11500, 11000, 10500, 10000, 9500,
9000, 8500, 8000, 7500, 7000, 6500, 6000, 5500, 5000, 4500,
4000, 3500, 3000, 2500, 2000, 1500, 1000, 500, 0
};
private static int testRpmCurrentIndex = 0; private static SerialPort currentSerialPort;
private static double testRpmCurrentValue = 0.0; private static ScheduledExecutorService serialExecutor;
private static final double TEST_RPM_INCREMENT_STEP = 100.0; // Increased for faster simulation private static BufferedReader serialReader;
private static java.util.List<Double> smoothRpmData; private static CSVLogger csvLogger;
private static int smoothRpmDataIndex = 0; private static boolean measurementActive = false;
private static final double TEST_SIMULATION_DELTA_TIME = 0.05; // 50 milliseconds
private static TreeMap<Double, Double> simulatedPowerCurve; private static double peakRpmDuringRun = 0.0;
private static TreeMap<Double, Double> simulatedTorqueCurve; private static int belowThresholdCounter = 0;
private static final int BELOW_THRESHOLD_LIMIT = 20; // 20 * 10ms = 200ms
private static final double RPM_DROP_FACTOR = 0.3; // 30% of peak
public static void main(String[] args) { public static void main(String[] args) {
// Initialize simulated curves
simulatedPowerCurve = new TreeMap<>();
simulatedPowerCurve.put(0.0, 0.0);
simulatedPowerCurve.put(4200.0, 2.0);
simulatedPowerCurve.put(6000.0, 5.0);
simulatedPowerCurve.put(7000.0, 6.0);
simulatedPowerCurve.put(8000.0, 7.5);
simulatedPowerCurve.put(8945.0, 8.5); // Peak Power
simulatedPowerCurve.put(9500.0, 8.0);
simulatedPowerCurve.put(10500.0, 7.0);
simulatedPowerCurve.put(11400.0, 6.5);
simulatedPowerCurve.put(12000.0, 6.0);
simulatedTorqueCurve = new TreeMap<>();
simulatedTorqueCurve.put(0.0, 0.0);
simulatedTorqueCurve.put(4200.0, 3.0);
simulatedTorqueCurve.put(5000.0, 4.0);
simulatedTorqueCurve.put(6000.0, 5.0);
simulatedTorqueCurve.put(8379.0, 6.8); // Peak Torque
simulatedTorqueCurve.put(9000.0, 6.0);
simulatedTorqueCurve.put(10000.0, 5.0);
simulatedTorqueCurve.put(11400.0, 4.0);
simulatedTorqueCurve.put(12000.0, 3.5);
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
dynoGUI = new DynoGUI(); dynoGUI = new DynoGUI();
leistungBerechner = new LeistungBerechner(); leistungBerechner = new LeistungBerechner();
addControlPanel(); addControlPanel();
// Optionally start in test mode for development startSerialPortListener(); // Start listening for serial data immediately
// toggleTestMode();
}); });
} }
private static void addControlPanel() { private static void addControlPanel() {
JPanel controlPanel = new JPanel(); JPanel controlPanel = new JPanel();
JButton toggleModeButton = new JButton("Toggle Test Mode"); JButton toggleModeButton = new JButton("Toggle Test Mode");
toggleModeButton.addActionListener(e -> toggleTestMode()); toggleModeButton.addActionListener(e -> toggleTestMode());
controlPanel.add(toggleModeButton); controlPanel.add(toggleModeButton);
@@ -105,11 +75,32 @@ public class Main {
resetChartButton.addActionListener(e -> dynoGUI.resetChart()); resetChartButton.addActionListener(e -> dynoGUI.resetChart());
controlPanel.add(resetChartButton); controlPanel.add(resetChartButton);
JButton resetMaxButton = new JButton("Reset Max-Hold");
resetMaxButton.addActionListener(e -> dynoGUI.resetMaxHold());
controlPanel.add(resetMaxButton);
JButton reconnectButton = new JButton("Reconnect Serial");
reconnectButton.addActionListener(e -> reconnectSerial());
controlPanel.add(reconnectButton);
JButton startMeasButton = new JButton("Start Messung");
startMeasButton.addActionListener(e -> startMeasurement());
controlPanel.add(startMeasButton);
JButton stopMeasButton = new JButton("Stop Messung");
stopMeasButton.addActionListener(e -> stopMeasurement());
controlPanel.add(stopMeasButton);
dynoGUI.add(controlPanel, BorderLayout.SOUTH); dynoGUI.add(controlPanel, BorderLayout.SOUTH);
dynoGUI.revalidate(); dynoGUI.revalidate();
dynoGUI.repaint(); dynoGUI.repaint();
} }
private static void reconnectSerial() {
stopSerialPortListener();
startSerialPortListener();
}
private static void toggleTestMode() { private static void toggleTestMode() {
testMode = !testMode; testMode = !testMode;
if (testMode) { if (testMode) {
@@ -133,69 +124,46 @@ public class Main {
} }
dynoGUI.resetChart(); dynoGUI.resetChart();
lastTimestamp = 0; // Reset for a new run measurementActive = true;
lastTimestamp = 0;
lastOmega = 0.0; lastOmega = 0.0;
testRpmCurrentIndex = 0; peakRpmDuringRun = 0.0;
testRpmCurrentValue = TEST_RPM_VALUES[0]; // Start at the first RPM value belowThresholdCounter = 0;
// Pre-generate smooth RPM data if (csvLogger != null) { try { csvLogger.close(); } catch (Exception ex) {} }
smoothRpmData = new ArrayList<>(); try { csvLogger = new CSVLogger(new java.io.File("logs")); } catch(Exception ex) { csvLogger = null; }
if (TEST_RPM_VALUES.length > 0) {
smoothRpmData.add(TEST_RPM_VALUES[0]); // Start with the first point
}
for (int i = 0; i < TEST_RPM_VALUES.length - 1; i++) { final long simStartTime = System.currentTimeMillis();
double startRpm = TEST_RPM_VALUES[i];
double endRpm = TEST_RPM_VALUES[i+1];
if (startRpm == endRpm) {
// If start and end RPM are the same, just add it if it's not already the last element
if (smoothRpmData.isEmpty() || smoothRpmData.get(smoothRpmData.size() - 1).doubleValue() != endRpm) {
smoothRpmData.add(endRpm);
}
continue; // Move to the next segment
}
// Determine the direction of change
int direction = (endRpm > startRpm) ? 1 : -1;
double currentRpm = startRpm;
// Generate intermediate points
while (true) {
double nextRpm = currentRpm + direction * TEST_RPM_INCREMENT_STEP;
// Check if we would overshoot the endRpm
if ((direction > 0 && nextRpm > endRpm) || (direction < 0 && nextRpm < endRpm)) {
// Add the exact endRpm if it's not already the last one
if (smoothRpmData.isEmpty() || smoothRpmData.get(smoothRpmData.size() - 1).doubleValue() != endRpm) {
smoothRpmData.add(endRpm);
}
break; // Segment finished
} else {
smoothRpmData.add(nextRpm);
currentRpm = nextRpm;
}
}
}
smoothRpmDataIndex = 0;
testDataExecutor = Executors.newSingleThreadScheduledExecutor(); testDataExecutor = Executors.newSingleThreadScheduledExecutor();
testDataExecutor.scheduleAtFixedRate(() -> { testDataExecutor.scheduleAtFixedRate(() -> {
simulateSmoothRpmRun(); // Simulate a realistic dyno run pattern
}, 0, 1, TimeUnit.MILLISECONDS); // Update RPM every 10ms for faster animation long currentTime = System.currentTimeMillis();
} double elapsedSeconds = (currentTime - simStartTime) / 1000.0;
private static void simulateSmoothRpmRun() { // Simulate acceleration, hold, then deceleration
if (smoothRpmDataIndex >= smoothRpmData.size()) { double simulatedRpm;
stopTestRun(); if (elapsedSeconds < 5.0) {
JOptionPane.showMessageDialog(null, "Test run finished.", "Info", JOptionPane.INFORMATION_MESSAGE); // Acceleration phase (0-5 seconds)
showPrintableChart(); simulatedRpm = 800 + (11000 - 800) * (elapsedSeconds / 5.0);
return; } else if (elapsedSeconds < 7.0) {
} // Hold at max RPM (5-7 seconds)
simulatedRpm = 11000;
} else if (elapsedSeconds < 12.0) {
// Deceleration phase (7-12 seconds)
simulatedRpm = 11000 - (11000 - 800) * ((elapsedSeconds - 7.0) / 5.0);
} else {
// Return to idle after 12 seconds
simulatedRpm = 800;
stopTestRun();
}
double currentSimulatedRpm = smoothRpmData.get(smoothRpmDataIndex); // Add some realistic noise
processRPMData(currentSimulatedRpm, TEST_SIMULATION_DELTA_TIME); simulatedRpm += (Math.random() * 200 - 100);
smoothRpmDataIndex++; simulatedRpm = Math.max(0, simulatedRpm);
processRPMData(simulatedRpm, 0.05); // Use 50ms delta time for test mode
}, 0, 50, TimeUnit.MILLISECONDS); // Update every 50ms
} }
private static void stopTestRun() { private static void stopTestRun() {
@@ -203,14 +171,17 @@ public class Main {
testDataExecutor.shutdownNow(); testDataExecutor.shutdownNow();
testDataExecutor = null; testDataExecutor = null;
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
dynoGUI.updateWerte(0, 0, 0); // Reset gauges dynoGUI.updateWerte(0, 0, 0);
if (csvLogger != null) {
try { csvLogger.close(); } catch (Exception ex) { ex.printStackTrace(); }
csvLogger = null;
}
showPrintableChart();
}); });
measurementActive = false;
} }
} }
private static SerialPort currentSerialPort;
private static ScheduledExecutorService serialExecutor;
private static void startSerialPortListener() { private static void startSerialPortListener() {
SerialPort[] comPorts = SerialPort.getCommPorts(); SerialPort[] comPorts = SerialPort.getCommPorts();
if (comPorts.length == 0) { if (comPorts.length == 0) {
@@ -250,23 +221,29 @@ public class Main {
return; return;
} }
final SerialPort finalChosenPort = chosenPort; currentSerialPort = chosenPort;
currentSerialPort = finalChosenPort; // Store for later closing chosenPort.setBaudRate(9600);
chosenPort.setComPortTimeouts(SerialPort.TIMEOUT_READ_SEMI_BLOCKING, 100, 0);
finalChosenPort.setBaudRate(9600); // Common baud rate, can be adjusted if (chosenPort.openPort()) {
if (finalChosenPort.openPort()) { System.out.println("Serial port " + chosenPort.getSystemPortName() + " opened successfully.");
System.out.println("Serial port " + finalChosenPort.getSystemPortName() + " opened successfully."); serialReader = new BufferedReader(new InputStreamReader(chosenPort.getInputStream()));
BufferedReader reader = new BufferedReader(new InputStreamReader(finalChosenPort.getInputStream()));
serialExecutor = Executors.newSingleThreadScheduledExecutor(); serialExecutor = Executors.newSingleThreadScheduledExecutor();
serialExecutor.scheduleAtFixedRate(() -> { serialExecutor.scheduleAtFixedRate(() -> {
try { try {
if (reader.ready()) { if (serialReader.ready()) {
String line = reader.readLine(); String line = serialReader.readLine();
if (line != null && !line.trim().isEmpty()) { if (line != null && !line.trim().isEmpty()) {
try { try {
double rpm = Double.parseDouble(line.trim()); // Handle potential incomplete lines or multiple values
processRPMData(rpm); // Call the main processRPMData for live data String[] values = line.trim().split("[\\s,]+");
for (String value : values) {
if (!value.isEmpty()) {
double rpm = Double.parseDouble(value);
processRPMData(rpm); // Process each valid RPM value
}
}
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
System.err.println("Received non-numeric data: " + line); System.err.println("Received non-numeric data: " + line);
} }
@@ -276,124 +253,159 @@ public class Main {
System.err.println("Error reading from serial port: " + e.getMessage()); System.err.println("Error reading from serial port: " + e.getMessage());
e.printStackTrace(); e.printStackTrace();
stopSerialPortListener(); stopSerialPortListener();
JOptionPane.showMessageDialog(null, "Serial port error: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); SwingUtilities.invokeLater(() -> {
JOptionPane.showMessageDialog(null,
"Serial port error: " + e.getMessage(),
"Error", JOptionPane.ERROR_MESSAGE);
});
} }
}, 0, 100, TimeUnit.MILLISECONDS); // Read every 100ms }, 0, 10, TimeUnit.MILLISECONDS); // Check more frequently for data
} else { } else {
JOptionPane.showMessageDialog(null, "Failed to open serial port " + finalChosenPort.getSystemPortName(), "Error", JOptionPane.ERROR_MESSAGE); JOptionPane.showMessageDialog(null,
"Failed to open serial port " + chosenPort.getSystemPortName(),
"Error", JOptionPane.ERROR_MESSAGE);
} }
} }
private static void stopSerialPortListener() { private static void stopSerialPortListener() {
if (serialExecutor != null) { if (serialExecutor != null) {
serialExecutor.shutdownNow(); serialExecutor.shutdownNow();
try {
if (!serialExecutor.awaitTermination(500, TimeUnit.MILLISECONDS)) {
serialExecutor.shutdownNow();
}
} catch (InterruptedException e) {
serialExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
serialExecutor = null; serialExecutor = null;
} }
if (serialReader != null) {
try {
serialReader.close();
} catch (Exception e) {
System.err.println("Error closing serial reader: " + e.getMessage());
}
serialReader = null;
}
if (currentSerialPort != null && currentSerialPort.isOpen()) { if (currentSerialPort != null && currentSerialPort.isOpen()) {
currentSerialPort.closePort(); currentSerialPort.closePort();
System.out.println("Serial port " + currentSerialPort.getSystemPortName() + " closed."); System.out.println("Serial port " + currentSerialPort.getSystemPortName() + " closed.");
currentSerialPort = null; currentSerialPort = null;
} }
if (csvLogger != null) {
try { csvLogger.close(); } catch (Exception ex) { ex.printStackTrace(); }
csvLogger = null;
}
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
dynoGUI.updateWerte(0, 0, 0); // Reset gauges dynoGUI.updateWerte(0, 0, 0); // Reset gauges
}); });
} }
// Overloaded processRPMData for live mode, calculates deltaZeit from system time
private static void processRPMData(double rpm) { private static void processRPMData(double rpm) {
processRPMData(rpm, 0.0); // Pass 0.0 to indicate no forced delta time, use system time
}
// Main processRPMData for both live and test mode, uses forcedDeltaTime if provided
private static void processRPMData(double rpm, double forcedDeltaTime) {
long currentTimestamp = System.currentTimeMillis(); long currentTimestamp = System.currentTimeMillis();
double effectiveDeltaZeit; if (lastTimestamp == 0) {
double leistung;
double drehmoment;
if (forcedDeltaTime > 0) {
// Test mode: Use simulated curves with linear interpolation
effectiveDeltaZeit = forcedDeltaTime;
leistung = interpolate(simulatedPowerCurve, rpm);
drehmoment = interpolate(simulatedTorqueCurve, rpm);
SwingUtilities.invokeLater(() -> {
dynoGUI.updateWerte(leistung, drehmoment, rpm);
});
// For test mode, we still need to update lastOmega for the next iteration
// in case the next simulated RPM is used for an actual calculation (e.g. if we switch modes).
lastOmega = rpmToOmega(rpm);
lastTimestamp = currentTimestamp; // Also update timestamp for consistency, though not used in test mode calculation
return;
} else {
// Live mode: Calculate from actual sensor data
if (lastTimestamp == 0) {
lastTimestamp = currentTimestamp;
lastOmega = rpmToOmega(rpm);
SwingUtilities.invokeLater(() -> {
dynoGUI.updateWerte(0, 0, rpm); // Update RPM, but no power/torque yet for the first point
});
return; // Skip calculations for the very first data point
}
effectiveDeltaZeit = (currentTimestamp - lastTimestamp) / 1000.0;
// Ensure effectiveDeltaZeit is always positive and non-zero for live mode
if (effectiveDeltaZeit <= 0) {
effectiveDeltaZeit = 0.001; // Assign a small positive value to avoid division by zero
}
double currentOmega = rpmToOmega(rpm);
// If there's no change in RPM, then acceleration is zero. Display zero power/torque.
if (currentOmega == lastOmega) {
SwingUtilities.invokeLater(() -> {
dynoGUI.updateWerte(0, 0, rpm); // Update RPM, but output zero power/torque
});
lastTimestamp = currentTimestamp; // Crucially update for the next iteration
lastOmega = currentOmega; // Crucially update for the next iteration
return;
}
drehmoment = leistungBerechner.berechneDrehmoment(lastOmega, currentOmega, effectiveDeltaZeit);
leistung = leistungBerechner.berechneLeistung(drehmoment, currentOmega);
SwingUtilities.invokeLater(() -> {
dynoGUI.updateWerte(leistung, drehmoment, rpm);
});
lastTimestamp = currentTimestamp; lastTimestamp = currentTimestamp;
lastOmega = currentOmega; lastOmega = rpmToOmega(rpm);
SwingUtilities.invokeLater(() -> {
dynoGUI.updateWerte(0, 0, rpm);
});
return;
}
double deltaTime = (currentTimestamp - lastTimestamp) / 1000.0;
if (deltaTime <= 0) deltaTime = 0.001;
double currentOmega = rpmToOmega(rpm);
double drehmoment = leistungBerechner.berechneDrehmoment(lastOmega, currentOmega, deltaTime);
double leistung = leistungBerechner.berechneLeistung(drehmoment, currentOmega);
if (measurementActive) {
if (rpm > peakRpmDuringRun) {
peakRpmDuringRun = rpm;
}
if (peakRpmDuringRun > 0 && rpm < peakRpmDuringRun * RPM_DROP_FACTOR) {
belowThresholdCounter++;
} else {
belowThresholdCounter = 0;
}
if (belowThresholdCounter >= BELOW_THRESHOLD_LIMIT) {
SwingUtilities.invokeLater(() -> stopMeasurement());
}
}
SwingUtilities.invokeLater(() -> {
dynoGUI.updateWerte(leistung, drehmoment, rpm);
if (measurementActive) {
dynoGUI.addPlotData(leistung, drehmoment, rpm);
}
});
lastTimestamp = currentTimestamp;
lastOmega = currentOmega;
if (measurementActive) {
if (csvLogger == null) {
try {
csvLogger = new CSVLogger(new java.io.File("logs"));
} catch (Exception ex) {
System.err.println("Could not create CSV logger: " + ex.getMessage());
}
}
if (csvLogger != null) {
csvLogger.log(rpm, leistung, drehmoment);
}
} }
} }
// Linear interpolation method private static void processRPMData(double rpm, double forcedDeltaTime) {
private static double interpolate(TreeMap<Double, Double> curve, double rpm) { // For test mode with forced delta time
if (curve.containsKey(rpm)) { double currentOmega = rpmToOmega(rpm);
return curve.get(rpm); double drehmoment = leistungBerechner.berechneDrehmoment(lastOmega, currentOmega, forcedDeltaTime);
} double leistung = leistungBerechner.berechneLeistung(drehmoment, currentOmega);
Map.Entry<Double, Double> lower = curve.floorEntry(rpm);
Map.Entry<Double, Double> upper = curve.ceilingEntry(rpm);
if (lower == null) { if (measurementActive) {
return upper.getValue(); // RPM is below lowest defined point, return lowest point's value if (rpm > peakRpmDuringRun) {
} peakRpmDuringRun = rpm;
if (upper == null) { }
return lower.getValue(); // RPM is above highest defined point, return highest point's value if (peakRpmDuringRun > 0 && rpm < peakRpmDuringRun * RPM_DROP_FACTOR) {
belowThresholdCounter++;
} else {
belowThresholdCounter = 0;
}
if (belowThresholdCounter >= BELOW_THRESHOLD_LIMIT) {
SwingUtilities.invokeLater(() -> stopMeasurement());
}
} }
double x1 = lower.getKey(); SwingUtilities.invokeLater(() -> {
double y1 = lower.getValue(); dynoGUI.updateWerte(leistung, drehmoment, rpm);
double x2 = upper.getKey(); if (measurementActive) {
double y2 = upper.getValue(); dynoGUI.addPlotData(leistung, drehmoment, rpm);
}
});
// Linear interpolation formula: y = y1 + ((y2 - y1) / (x2 - x1)) * (x - x1) lastOmega = currentOmega;
if (x2 == x1) { // Avoid division by zero if two points have the same RPM (shouldn't happen with proper data) lastTimestamp = System.currentTimeMillis();
return y1;
if (measurementActive) {
if (csvLogger == null) {
try {
csvLogger = new CSVLogger(new java.io.File("logs"));
} catch (Exception ex) {
System.err.println("Could not create CSV logger: " + ex.getMessage());
}
}
if (csvLogger != null) {
csvLogger.log(rpm, leistung, drehmoment);
}
} }
return y1 + ((y2 - y1) / (x2 - x1)) * (rpm - x1);
} }
private static double rpmToOmega(double rpm) { private static double rpmToOmega(double rpm) {
@@ -418,7 +430,9 @@ public class Main {
try { try {
job.print(); job.print();
} catch (PrinterException ex) { } catch (PrinterException ex) {
JOptionPane.showMessageDialog(chartFrame, "Printing error: " + ex.getMessage(), "Print Error", JOptionPane.ERROR_MESSAGE); JOptionPane.showMessageDialog(chartFrame,
"Printing error: " + ex.getMessage(),
"Print Error", JOptionPane.ERROR_MESSAGE);
} }
} }
}); });
@@ -426,9 +440,13 @@ public class Main {
JButton savePdfButton = new JButton("Save as PDF"); JButton savePdfButton = new JButton("Save as PDF");
savePdfButton.addActionListener(e -> saveChartAsPdf(printChartPanel)); savePdfButton.addActionListener(e -> saveChartAsPdf(printChartPanel));
JButton savePngButton = new JButton("Save as PNG");
savePngButton.addActionListener(e -> saveChartAsPng(printChartPanel));
JPanel controlPanel = new JPanel(); JPanel controlPanel = new JPanel();
controlPanel.add(printButton); controlPanel.add(printButton);
controlPanel.add(savePdfButton); controlPanel.add(savePdfButton);
controlPanel.add(savePngButton);
chartFrame.add(controlPanel, BorderLayout.SOUTH); chartFrame.add(controlPanel, BorderLayout.SOUTH);
chartFrame.setLocationRelativeTo(dynoGUI); chartFrame.setLocationRelativeTo(dynoGUI);
@@ -451,27 +469,92 @@ public class Main {
File finalFile = new File(filePath); File finalFile = new File(filePath);
try { try {
// Use the dimensions of the ChartPanel to create the PDF document
Document document = new Document(new Rectangle(chartPanel.getWidth(), chartPanel.getHeight())); Document document = new Document(new Rectangle(chartPanel.getWidth(), chartPanel.getHeight()));
PdfWriter.getInstance(document, new FileOutputStream(finalFile)); PdfWriter.getInstance(document, new FileOutputStream(finalFile));
document.open(); document.open();
// Create BufferedImage from the ChartPanel's chart java.awt.Image awtImage = chartPanel.getChart().createBufferedImage(
java.awt.Image awtImage = chartPanel.getChart().createBufferedImage(chartPanel.getWidth(), chartPanel.getHeight()); chartPanel.getWidth(), chartPanel.getHeight());
Image pdfImage = Image.getInstance(awtImage, null); Image pdfImage = Image.getInstance(awtImage, null);
// Scale image to fit document pdfImage.scaleToFit(document.getPageSize().getWidth() - 20,
pdfImage.scaleToFit(document.getPageSize().getWidth() - 20, document.getPageSize().getHeight() - 20); // Add some padding document.getPageSize().getHeight() - 20);
pdfImage.setAlignment(Image.ALIGN_CENTER); pdfImage.setAlignment(Image.ALIGN_CENTER);
document.add(pdfImage); document.add(pdfImage);
document.close(); document.close();
JOptionPane.showMessageDialog(null, "Chart saved as PDF successfully!\n" + finalFile.getAbsolutePath(), "Save Successful", JOptionPane.INFORMATION_MESSAGE); JOptionPane.showMessageDialog(null,
"Chart saved as PDF successfully!\n" + finalFile.getAbsolutePath(),
"Save Successful", JOptionPane.INFORMATION_MESSAGE);
} catch (Exception ex) { } catch (Exception ex) {
JOptionPane.showMessageDialog(null, "Error saving chart as PDF: " + ex.getMessage(), "Save Error", JOptionPane.ERROR_MESSAGE); JOptionPane.showMessageDialog(null,
"Error saving chart as PDF: " + ex.getMessage(),
"Save Error", JOptionPane.ERROR_MESSAGE);
ex.printStackTrace(); ex.printStackTrace();
} }
} }
} }
}
private static void saveChartAsPng(ChartPanel chartPanel) {
JFileChooser fileChooser = new JFileChooser();
fileChooser.setDialogTitle("Save Chart as PNG");
fileChooser.setFileFilter(new FileNameExtensionFilter("PNG Image (*.png)", "png"));
int userSelection = fileChooser.showSaveDialog(null);
if (userSelection == JFileChooser.APPROVE_OPTION) {
File fileToSave = fileChooser.getSelectedFile();
String filePath = fileToSave.getAbsolutePath();
if (!filePath.toLowerCase().endsWith(".png")) {
filePath += ".png";
}
File finalFile = new File(filePath);
try {
org.jfree.chart.ChartUtils.saveChartAsPNG(finalFile, chartPanel.getChart(), chartPanel.getWidth(), chartPanel.getHeight());
JOptionPane.showMessageDialog(null, "Chart saved as PNG successfully!\n" + finalFile.getAbsolutePath(), "Save Successful", JOptionPane.INFORMATION_MESSAGE);
} catch (Exception ex) {
JOptionPane.showMessageDialog(null, "Error saving chart as PNG: " + ex.getMessage(), "Save Error", JOptionPane.ERROR_MESSAGE);
ex.printStackTrace();
}
}
}
private static void startMeasurement() {
if (measurementActive) {
JOptionPane.showMessageDialog(null, "Messung läuft bereits.", "Info", JOptionPane.INFORMATION_MESSAGE);
return;
}
measurementActive = true;
dynoGUI.resetChart();
dynoGUI.resetMaxHold();
if (csvLogger != null) {
try { csvLogger.close(); } catch (Exception ex) { ex.printStackTrace(); }
}
try {
csvLogger = new CSVLogger(new java.io.File("logs"));
} catch (Exception ex) {
csvLogger = null;
System.err.println("CSV Logger konnte nicht erstellt werden: " + ex.getMessage());
}
peakRpmDuringRun = 0.0;
belowThresholdCounter = 0;
}
private static void stopMeasurement() {
if (!measurementActive) {
JOptionPane.showMessageDialog(null, "Keine laufende Messung.", "Info", JOptionPane.INFORMATION_MESSAGE);
return;
}
measurementActive = false;
if (csvLogger != null) {
try { csvLogger.close(); } catch (Exception ex) { ex.printStackTrace(); }
csvLogger = null;
}
// Snapshot chart for printing
showPrintableChart();
peakRpmDuringRun = 0.0;
belowThresholdCounter = 0;
JOptionPane.showMessageDialog(null, "Messung gestoppt.", "Info", JOptionPane.INFORMATION_MESSAGE);
}
}