Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
misc/
output/
output/
telemetry.log
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
- 📊 **Live Graphs**: Real-time charts for engine, suspension, input, etc.
- 💾 **CSV Logging**: Save sessions to file for playback and later review.
- ⏪ **Replay Mode**: Open saved CSV files to view logged data with graph control.
- 🔧 **User Controls**: Adjustable sample count, start/stop, open/clear logs, etc.
- 🔧 **User Controls**: Adjustable sample count, start/stop, open/clear logs, change IP/port, etc.

## 📷 Preview

Expand Down Expand Up @@ -96,5 +96,4 @@ See [`LICENSE`](./LICENSE) for full details.

- Torque conversion (not entirely accurate right now)
- Fix for speed, suspension, and velocity charts (jittering at idle/standstill)
- Input to choose port?
- UI Improvements
87 changes: 72 additions & 15 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,14 @@
QTabWidget, QLabel, QTextEdit, QLineEdit, QGridLayout, QSlider, QFileDialog
)
from PySide6.QtCharts import QChart, QChartView, QLineSeries, QValueAxis
from PySide6.QtGui import QPainter
from PySide6.QtGui import QPainter, QIcon
from struct import unpack
import csv

# Defaults
tele_IP_addr = "192.168.1.9"
tele_port = 5500

logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s [%(levelname)s] %(message)s",
Expand Down Expand Up @@ -126,20 +130,22 @@ def to_dict(self):
return {prop: getattr(self, prop) for prop in self.get_props()}

class TelemetryReceiver(QObject):
global tele_IP_addr, tele_port
data_received = Signal(dict)
log_message = Signal(str)

def __init__(self, ip="0.0.0.0", port=5607):

def __init__(self):
super().__init__()
self.ip = ip
self.port = port
self._running = False
self.sock = None
self.thread = None

def start(self):
if self._running:
return
self.ip = tele_IP_addr
self.port = tele_port
self._running = True
self.thread = threading.Thread(target=self._listen_loop, daemon=True)
self.thread.start()
Expand Down Expand Up @@ -236,20 +242,22 @@ def add_values(self, vals):
self.series.replace(points)

if self.data:
mn, mx = min(self.data), max(self.data)
if mn == mx:
mn -= 0.1
mx += 0.1
self.axis_y.setRange(mn, mx)
minimum, maximum = min(self.data), max(self.data)
if minimum == maximum:
minimum -= 0.1
maximum += 0.1
self.axis_y.setRange(minimum, maximum)
self.axis_x.setRange(0, len(self.data))

class ForzaTelemetryApp(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("FH5 Telemetry")
self.setStyleSheet("background-color: #121212; color: #808080;")
self.setWindowIcon(QIcon("./img/logo.ico"))


self.receiver = TelemetryReceiver(ip="0.0.0.0", port=5607)
self.receiver = TelemetryReceiver()
self.receiver.data_received.connect(self.buffer_data)
self.receiver.log_message.connect(self.log)

Expand Down Expand Up @@ -300,6 +308,22 @@ def __init__(self):

controls.addStretch()


# Ip / Port Changer
ip_input = QLabel("IP:Port")
ip_input.setAlignment(Qt.AlignLeft)
controls.addWidget(ip_input)
self.changeIP = QLineEdit("")
self.changeIP.setFixedWidth(100)
self.changeIP.setPlaceholderText("IP:Port")
controls.addWidget(self.changeIP)

self.btn_changeIP = QPushButton("Apply")
self.btn_changeIP.clicked.connect(self.update_IP_port)
controls.addWidget(self.btn_changeIP)

controls.addStretch()

self.btn_clear_logs = QPushButton("Clear Logs")
self.btn_clear_logs.clicked.connect(self.log_panel.clear)
controls.addWidget(self.btn_clear_logs)
Expand Down Expand Up @@ -362,13 +386,28 @@ def _setup_charts(self):

def buffer_data(self, data: dict):
self.sample_count += 1
imperial_toggle = False # Set to False to show speed in metric instead of imperial

def scale_controls(val): return val * 100 / 255
def norm_steer(val): return val * 100 / 127
def to_mph(val): return val * 2.23694
def to_hp(val): return val / 745.7
def to_mph(val):
if imperial_toggle:
return val * 2.23694 # MPH
else:
return val * 3.6 # KPH
def to_hp(val):
if imperial_toggle:
return val / 745.7 # HP
else:
return val / 1000 # KW
def clamp_zero(val): return max(val, 0)

def to_psi(val):
if imperial_toggle:
return val # PSI
else:
return val / 14.504 # BAR


patch_map = {
'accel': scale_controls,
'brake': scale_controls,
Expand All @@ -378,7 +417,7 @@ def clamp_zero(val): return max(val, 0)
'speed': to_mph,
'power': to_hp,
'torque': clamp_zero,
'boost': clamp_zero
'boost': to_psi,
}

raw_snapshot = {}
Expand Down Expand Up @@ -514,6 +553,24 @@ def stop(self):
chart.data.clear()
chart.add_values([])

def update_IP_port(self, ):
global tele_IP_addr, tele_port
try:
ip_port = self.changeIP.text().split(":")
if len(ip_port) != 2:
raise ValueError("Invalid format. Use IP:Port")
print(f"IP port input: {ip_port}")
tele_IP_addr = ip_port[0]
tele_port = int(ip_port[1])
log.info(f"Telemetry IP/Port changed to {tele_IP_addr}:{tele_port}")
self.log(f"Telemetry IP/Port changed to {tele_IP_addr}:{tele_port}")
if self.receiver._running:
self.receiver.stop()
self.receiver.start()
except Exception as e:
log.error(f"Failed to change IP/Port: {e}")
self.log(f"Failed to change IP/Port: {e}")

def toggle_logging(self, checked):
if checked:
self._start_logging()
Expand Down Expand Up @@ -565,4 +622,4 @@ def closeEvent(self, event):
app = QApplication(sys.argv)
window = ForzaTelemetryApp()
window.showMaximized()
sys.exit(app.exec())
sys.exit(app.exec())