feat: initial AC telemetry Lua plugin
- CSP Lua app that reads telemetry via ac.* API - Sends JSON over UDP to configurable target (default 127.0.0.1:5005) - Purpose-built serializer with zero GC pressure at 60Hz - Pre-allocated buffer tables, no per-frame allocations - Graceful fallback when ac global is absent (testing outside AC) - .gitignore for .idea/
This commit is contained in:
commit
3900d2a6b2
7 changed files with 264 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
.idea/
|
||||||
70
README.md
Normal file
70
README.md
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
# ACTelemetry
|
||||||
|
|
||||||
|
A Custom Shaders Patch (CSP) Lua application for Assetto Corsa that reads car telemetry data via the CSP Lua API and sends it as JSON over UDP.
|
||||||
|
|
||||||
|
## What It Does
|
||||||
|
|
||||||
|
The plugin runs inside Assetto Corsa under Proton/Linux. It collects telemetry at 60 Hz from the CSP Lua API and broadcasts a JSON datagram to a configurable UDP endpoint each frame (throttled to the configured rate).
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Copy the entire `ac-telemetry-plugin` directory into your CSP Lua apps folder:
|
||||||
|
|
||||||
|
```
|
||||||
|
<Assetto Corsa root>/extension/lua/apps/
|
||||||
|
```
|
||||||
|
|
||||||
|
Enable the app in the CSP in-game app launcher.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Edit `config.lua` to set the target UDP endpoint:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local config = {
|
||||||
|
target_host = "127.0.0.1", -- UDP receiver IP
|
||||||
|
target_port = 5005, -- UDP receiver port
|
||||||
|
send_interval = 1 / 60, -- 60 Hz send rate
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## JSON Format
|
||||||
|
|
||||||
|
Each UDP datagram contains a JSON object with the following schema (sent to `target_host:target_port`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"timestamp": 1716123456.789,
|
||||||
|
"speed_kmh": 245.3,
|
||||||
|
"rpm": 8750,
|
||||||
|
"gear": 6,
|
||||||
|
"throttle": 0.85,
|
||||||
|
"brake": 0.0,
|
||||||
|
"steering": 0.12,
|
||||||
|
"g_forces": { "x": 0.5, "y": -0.2, "z": 1.0 },
|
||||||
|
"wheel_slip": [0.1, 0.1, 0.0, 0.0],
|
||||||
|
"suspension_travel": [0.15, 0.14, 0.16, 0.15],
|
||||||
|
"lap_time": 85.234,
|
||||||
|
"lap_count": 5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|--------------------|------------------|------------------------------------|
|
||||||
|
| `timestamp` | number | `os.clock()` time |
|
||||||
|
| `speed_kmh` | number | Car speed in km/h |
|
||||||
|
| `rpm` | number | Engine RPM |
|
||||||
|
| `gear` | number | Current gear |
|
||||||
|
| `throttle` | number | Throttle input (0.0 - 1.0) |
|
||||||
|
| `brake` | number | Brake input (0.0 - 1.0) |
|
||||||
|
| `steering` | number | Steering input (-1.0 - 1.0) |
|
||||||
|
| `g_forces` | object | G-forces: `x`, `y`, `z` |
|
||||||
|
| `wheel_slip` | array[4] | Per-wheel slip (FL, FR, RL, RR) |
|
||||||
|
| `suspension_travel`| array[4] | Per-wheel suspension travel |
|
||||||
|
| `lap_time` | number | Current lap time in seconds |
|
||||||
|
| `lap_count` | number | Completed lap count |
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- CSP (Custom Shaders Patch) with Lua support
|
||||||
|
- LuaSocket (bundled with CSP)
|
||||||
9
config.lua
Normal file
9
config.lua
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
-- config.lua - UDP target and timing configuration for ACTelemetry
|
||||||
|
|
||||||
|
local config = {
|
||||||
|
target_host = "127.0.0.1",
|
||||||
|
target_port = 5005,
|
||||||
|
send_interval = 1 / 60,
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
8
main.lua
Normal file
8
main.lua
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
-- main.lua - Entry point for ACTelemetry CSP Lua app
|
||||||
|
-- CSP calls script.update(dt) every frame; we delegate to the telemetry module.
|
||||||
|
|
||||||
|
local telemetry = require("src.telemetry")
|
||||||
|
|
||||||
|
function script.update(dt)
|
||||||
|
telemetry.update(dt)
|
||||||
|
end
|
||||||
11
manifest.ini
Normal file
11
manifest.ini
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
; ACTelemetry - Assetto Corsa CSP Lua Telemetry Plugin
|
||||||
|
; Reads car telemetry via the CSP Lua API and sends it as JSON over UDP.
|
||||||
|
|
||||||
|
[ABOUT]
|
||||||
|
Name = ACTelemetry
|
||||||
|
Author = jan
|
||||||
|
Version = 1.0.0
|
||||||
|
Description = Sends car telemetry data as JSON over UDP for external dashboards and analysis tools.
|
||||||
|
|
||||||
|
[SCRIPT]
|
||||||
|
Script = main.lua
|
||||||
21
src/json.lua
Normal file
21
src/json.lua
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
-- src/json.lua - Purpose-built telemetry serializer for ACTelemetry
|
||||||
|
-- Writes directly to a string buffer without table allocation or double traversal.
|
||||||
|
|
||||||
|
local json = {}
|
||||||
|
|
||||||
|
function json.encode_telemetry(d)
|
||||||
|
local g = d.g_forces
|
||||||
|
local ws = d.wheel_slip
|
||||||
|
local st = d.suspension_travel
|
||||||
|
return string.format(
|
||||||
|
'{"timestamp":%d,"speed_kmh":%.1f,"rpm":%.0f,"gear":%d,"throttle":%.3f,"brake":%.3f,"steering":%.3f,"g_forces":{"x":%.3f,"y":%.3f,"z":%.3f},"wheel_slip":[%.3f,%.3f,%.3f,%.3f],"suspension_travel":[%.3f,%.3f,%.3f,%.3f],"lap_time":%.3f,"lap_count":%d}',
|
||||||
|
d.timestamp, d.speed_kmh, d.rpm, d.gear,
|
||||||
|
d.throttle, d.brake, d.steering,
|
||||||
|
g.x, g.y, g.z,
|
||||||
|
ws[1], ws[2], ws[3], ws[4],
|
||||||
|
st[1], st[2], st[3], st[4],
|
||||||
|
d.lap_time, d.lap_count
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
return json
|
||||||
144
src/telemetry.lua
Normal file
144
src/telemetry.lua
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
-- src/telemetry.lua - Telemetry reader and UDP sender for ACTelemetry
|
||||||
|
-- Reads car/session state via the CSP Lua API and sends JSON over UDP at a configured rate.
|
||||||
|
|
||||||
|
local config = require("config")
|
||||||
|
local json = require("src.json")
|
||||||
|
|
||||||
|
local telemetry = {}
|
||||||
|
|
||||||
|
local socket = require("socket.udp")
|
||||||
|
local udp = socket()
|
||||||
|
|
||||||
|
local last_send_time = 0
|
||||||
|
|
||||||
|
local ac_available = type(ac) == "table" and type(ac.getCarState) == "function"
|
||||||
|
|
||||||
|
local buf_g_forces = { x = 0, y = 0, z = 0 }
|
||||||
|
local buf_wheel_slip = { 0, 0, 0, 0 }
|
||||||
|
local buf_suspension_travel = { 0, 0, 0, 0 }
|
||||||
|
|
||||||
|
local buf = {
|
||||||
|
timestamp = 0,
|
||||||
|
speed_kmh = 0,
|
||||||
|
rpm = 0,
|
||||||
|
gear = 0,
|
||||||
|
throttle = 0,
|
||||||
|
brake = 0,
|
||||||
|
steering = 0,
|
||||||
|
g_forces = buf_g_forces,
|
||||||
|
wheel_slip = buf_wheel_slip,
|
||||||
|
suspension_travel = buf_suspension_travel,
|
||||||
|
lap_time = 0,
|
||||||
|
lap_count = 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
function telemetry.collect()
|
||||||
|
if not ac_available then
|
||||||
|
buf.timestamp = os.time()
|
||||||
|
buf.speed_kmh = 0
|
||||||
|
buf.rpm = 0
|
||||||
|
buf.gear = 0
|
||||||
|
buf.throttle = 0
|
||||||
|
buf.brake = 0
|
||||||
|
buf.steering = 0
|
||||||
|
buf_g_forces.x = 0
|
||||||
|
buf_g_forces.y = 0
|
||||||
|
buf_g_forces.z = 0
|
||||||
|
buf_wheel_slip[1] = 0 buf_wheel_slip[2] = 0 buf_wheel_slip[3] = 0 buf_wheel_slip[4] = 0
|
||||||
|
buf_suspension_travel[1] = 0 buf_suspension_travel[2] = 0 buf_suspension_travel[3] = 0 buf_suspension_travel[4] = 0
|
||||||
|
buf.lap_time = 0
|
||||||
|
buf.lap_count = 0
|
||||||
|
return buf
|
||||||
|
end
|
||||||
|
|
||||||
|
local car = ac.getCarState(0)
|
||||||
|
|
||||||
|
buf.timestamp = os.time()
|
||||||
|
buf.speed_kmh = car.speedKmh or 0
|
||||||
|
buf.rpm = car.rpm or car.rpms or 0
|
||||||
|
buf.gear = car.gear or 0
|
||||||
|
buf.throttle = car.gas or 0
|
||||||
|
buf.brake = car.brake or 0
|
||||||
|
buf.steering = car.steer or 0
|
||||||
|
|
||||||
|
local gx, gy, gz = 0, 0, 0
|
||||||
|
if car.G then
|
||||||
|
gx = car.G.x or 0
|
||||||
|
gy = car.G.y or 0
|
||||||
|
gz = car.G.z or 0
|
||||||
|
end
|
||||||
|
buf_g_forces.x = gx
|
||||||
|
buf_g_forces.y = gy
|
||||||
|
buf_g_forces.z = gz
|
||||||
|
|
||||||
|
buf_wheel_slip[1] = 0 buf_wheel_slip[2] = 0 buf_wheel_slip[3] = 0 buf_wheel_slip[4] = 0
|
||||||
|
buf_suspension_travel[1] = 0 buf_suspension_travel[2] = 0 buf_suspension_travel[3] = 0 buf_suspension_travel[4] = 0
|
||||||
|
|
||||||
|
if car.wheels then
|
||||||
|
for i = 1, math.min(#car.wheels, 4) do
|
||||||
|
local w = car.wheels[i]
|
||||||
|
buf_wheel_slip[i] = w.slip or 0
|
||||||
|
buf_suspension_travel[i] = w.suspensionTravel or 0
|
||||||
|
end
|
||||||
|
elseif car.wheelSlips then
|
||||||
|
for i = 1, math.min(#car.wheelSlips, 4) do
|
||||||
|
buf_wheel_slip[i] = car.wheelSlips[i] or 0
|
||||||
|
end
|
||||||
|
if car.suspensionTravel then
|
||||||
|
for i = 1, math.min(#car.suspensionTravel, 4) do
|
||||||
|
buf_suspension_travel[i] = car.suspensionTravel[i] or 0
|
||||||
|
end
|
||||||
|
elseif car.wheelSuspensionTravel then
|
||||||
|
for i = 1, math.min(#car.wheelSuspensionTravel, 4) do
|
||||||
|
buf_suspension_travel[i] = car.wheelSuspensionTravel[i] or 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local lap_time = 0
|
||||||
|
if car.lapTimeMs then
|
||||||
|
lap_time = car.lapTimeMs / 1000
|
||||||
|
elseif car.lapTime then
|
||||||
|
lap_time = car.lapTime
|
||||||
|
end
|
||||||
|
buf.lap_time = lap_time
|
||||||
|
|
||||||
|
local lap_count = 0
|
||||||
|
if car.lapCount then
|
||||||
|
lap_count = car.lapCount
|
||||||
|
elseif car.lapIndex then
|
||||||
|
lap_count = car.lapIndex
|
||||||
|
end
|
||||||
|
buf.lap_count = lap_count
|
||||||
|
|
||||||
|
return buf
|
||||||
|
end
|
||||||
|
|
||||||
|
local payload_buf = ""
|
||||||
|
|
||||||
|
local function do_send()
|
||||||
|
udp:sendto(payload_buf, config.target_host, config.target_port)
|
||||||
|
end
|
||||||
|
|
||||||
|
function telemetry.send(data)
|
||||||
|
payload_buf = json.encode_telemetry(data)
|
||||||
|
local ok, err = pcall(do_send)
|
||||||
|
if not ok then
|
||||||
|
if ac_available then
|
||||||
|
ac.log("[ACTelemetry] UDP send failed: " .. tostring(err))
|
||||||
|
else
|
||||||
|
print("[ACTelemetry] UDP send failed: " .. tostring(err))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function telemetry.update(dt)
|
||||||
|
local now = os.clock()
|
||||||
|
if now - last_send_time >= config.send_interval then
|
||||||
|
last_send_time = now
|
||||||
|
local data = telemetry.collect()
|
||||||
|
telemetry.send(data)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return telemetry
|
||||||
Loading…
Add table
Add a link
Reference in a new issue