Skip to content

qryptixai/MotoraxeN

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 

Repository files navigation

DriveCoach

A physically-grounded driving simulator that teaches real-world driving skills through force feedback.

DriveCoach pairs a Logitech G920 racing wheel with a physics-accurate Honda Civic simulation to teach learners how to actually feel a car — the self-aligning torque that tells you the tires are gripping, the lightening that warns you're about to understeer, the viscous resistance of real power steering. It scores you against the California Vehicle Code in real time, coaches you through corners, and gives you an analytics report when you're done.

Built in a single weekend for a hackathon. One Python file. No game engine. Runs natively on Apple Silicon.


Why This Matters

38,000 people die on US roads every year. The #1 cause is driver error — not mechanical failure, not weather, not road design. New drivers learn by reading a handbook and circling a parking lot. They never feel what a car does at 65 mph in a curve until they're on a real highway with real consequences.

Professional racing drivers train on simulators with force feedback wheels because steering feel is the primary channel through which a driver perceives grip. When you feel the wheel go light in a corner, that's the front tires telling you they're running out of traction — a 200-millisecond warning before the car pushes wide. No textbook teaches this. No parking-lot session covers this. But a force feedback simulator can.

DriveCoach brings that same principle to driver education:

  • Ghost lap mode: The wheel physically moves your hands through the correct steering inputs for each corner while you feel the forces. Motor-memory learning — the same technique fighter pilots use in simulator training.
  • Your turn mode: You drive the same road with real physics FFB. The car rewards smooth inputs with a planted, heavy wheel and punishes overdriving with a lightening feel at the limit.
  • Real-time scoring: Every red light, every stop sign, every instance of tailgating or lane departure is caught and penalized — exactly like a DMV examiner sitting in the passenger seat.

Technical Architecture

One File, No Engine

The entire simulator is companion/drive3d.py — ~3,300 lines of Python. No Unity, no Unreal, no Godot. The rendering uses pygfx (a wgpu-based graphics library) which compiles directly to Metal on macOS. This means:

  • Zero OpenGL dependency — critical on Apple Silicon where OpenGL is deprecated and crashes on some configurations
  • Native GPU pipeline — wgpu → Metal with no translation layer
  • ~60 FPS on a MacBook with a procedural city, cockpit model, and 16 NPC vehicles

Force Feedback via HID++ 2.0

The Logitech G920 on macOS is essentially undocumented. There is no official SDK, no DirectInput (that's Windows), and no Linux ff-memless layer. We wrote a complete FFB driver from scratch:

companion/wheel.py — 600 lines that implement:

  1. Input via GameController.framework (PyObjC) — macOS sees the G920 as a _GCLogitechRacingWheel and parses steering angle (900-degree range), three analog pedals, paddle shifters, and 11 buttons through the native HID stack.

  2. Output via HID++ 2.0 protocol over IOHIDDeviceSetReport — ported from the Linux kernel driver (hid-logitech-hidpp.c). At connect time:

    1. Root GET_FEATURE(0x8123) → discovers FFB feature index (0x0b on G920)
    2. RESET_ALL → clears stale effects from prior sessions  
    3. DOWNLOAD_EFFECT(CONSTANT | AUTOSTART, force=0) → uploads a constant-force
       effect into the hardware slot the device assigns
    

    After handshake, set_torque(nm) converts Nm to int16 and a 100 Hz output loop re-sends the effect with the new force value. The G920 has a 200ms hardware watchdog — if we stop sending packets, the motor zeros out automatically (safety feature). Our output loop re-sends even when the value hasn't changed to keep the watchdog fed.

  3. macOS-specific quirk handling — the G920 answers HID++ long reports (0x11) on the very-long report ID (0x12). Standard HID libraries don't expect this. We register a raw input-report callback and parse both report types to correctly receive handshake replies.

Physics: Bicycle Model with Friction Circle

The car physics in SimCar.update() implements a kinematic bicycle model with:

  • Friction-circle grip limiting — longitudinal and lateral forces share a tire's total grip budget. Hard braking reduces available cornering grip (and vice versa). This produces natural understeer: ask for more yaw rate than the front tires can deliver, and the car pushes wide.

  • Load transfer — braking shifts weight to the front axle (increasing front grip, decreasing rear), acceleration shifts it back. CG height, wheelbase, and weight distribution are tuned to a real Honda Civic (62/38 front/rear, 0.55m CG height).

  • Engine model with gear ratios — six-speed manual transmission with realistic torque curves per gear. Engine braking in higher gears, rev-matching feel through the pedals.

  • Aerodynamic drag + rolling resistanceF_drag = C_rr + C_d * v^2. At partial throttle, drag balances drive force and the car settles at a realistic cruise speed. 30% throttle in 4th gear holds ~45 mph — just like a real Civic.

Force Feedback Model: How a Real Car Feels

This is the core innovation. Most sim-racing FFB implementations use a centering spring — a force proportional to wheel angle that pushes the wheel back to center. This is physically wrong and creates a motor-position feedback loop that oscillates ("wobble").

DriveCoach uses the same force model as a real hydraulic power steering system:

# The ONLY forces sent to the G920 motor:
SAT_GAIN        = 0.70   # Self-aligning torque — from tire lateral force
RF_VISCOUS_DAMP = 0.35   # Viscous damping — from hydraulic steering fluid  
RF_COULOMB_NM   = 0.06   # Coulomb friction — dry rack friction for texture

Self-Aligning Torque (SAT) — When the front tires generate lateral force (cornering), the force acts behind the wheel's steering axis through the pneumatic trail. This creates a torque that tries to straighten the wheel. The magnitude is proportional to cornering force, and critically, it fades as the tire approaches its grip limit (pneumatic trail collapse). This is the "wheel goes light" feeling that warns a real driver they're about to lose grip:

front_lat_per_mass = self.lat_accel * (L_R / WHEELBASE)
pt_scale = max(0.0, 1.0 - min(1.0, understeer * SAT_TRAIL_COLLAPSE))
sat = -SAT_GAIN * front_lat_per_mass * pt_scale

Viscous Damping — Opposes wheel velocity, not position. This is physically what hydraulic power steering fluid does — it resists rapid wheel movements while allowing slow, deliberate inputs. Because it depends on velocity (not position), it cannot create a feedback oscillation with the motor:

viscous = -RF_VISCOUS_DAMP * d_wheel_filtered

No Centering Spring — Real power steering has no spring. The SAT provides all the centering force naturally. Removing the spring eliminated the wobble problem entirely while producing more realistic feel.

Ghost Coach: Pure Pursuit Steering

The ghost autopilot uses a pure pursuit algorithm with multi-segment waypoint lookahead:

LOOKAHEAD_TIME = 1.40  # seconds of preview
LOOKAHEAD_MIN  = 18.0  # metres minimum (tight turns)
LOOKAHEAD_MAX  = 65.0  # metres maximum (highway)

It walks forward along the waypoint chain by Ld = speed * lookahead_time metres, finds the target point, and computes the ideal steering angle:

ideal_wheel = atan2(2 * WHEELBASE * sin(alpha), L_eff)

This is the same algorithm used by autonomous vehicles for path following. The servo then drives the G920 motor to match this angle with a PD controller:

servo = K * (ideal_angle - actual_angle) - D * angular_velocity

The learner's hands rest on the wheel and feel it move through each turn — building muscle memory for correct steering inputs before they ever take control.

DMV Scoring Engine

The Coach class watches the player against the California Vehicle Code in real time:

Rule Code What It Catches
Speed limit CVC 22350 Exceeding posted limit + 5 mph grace
Stop signs CVC 22450 Rolling through without fully stopping
Red lights CVC 21453 Entering intersection on red
Following distance CVC 21703 < 1.6 second gap to vehicle ahead
Lane departure Drifting past lane edge for > 0.7s
Wrong side Driving in oncoming lane for > 0.5s
Hard braking Panic stop (> 8.5 m/s^2 deceleration)
Understeer Pushing past tire grip limit

Each violation deducts points from a starting score of 100, displays a real-time HUD warning with the CVC section number, and logs to a session record. When the player presses Space to end their turn, an analytics summary identifies their top violation category and gives a specific improvement tip:

═══ DRIVE REPORT ═══
  Score: 78/100
  Drive time: 2:34
  Violations: 4

  Breakdown:
    SPEEDING: 2x
    LANE DRIFT: 1x
    HARD BRAKE: 1x

  Focus area: SPEEDING
  Tip: Watch the speed limit — ease off the gas earlier approaching slower zones

The Route: Sunnyvale, California

The procedural map models a 3.2 km loop through seven road sections with diverse driving challenges:

Section Speed Lanes Challenge
I-280 East 65 mph 6 Highway merge, sustained speed
Exit Ramp 35 mph 2 Deceleration, tight right curve
Oak Lane 25 mph 2 Tight S-curves, stop signs
Sunnyvale Collector 35 mph 4 Sweeping curves, speed transitions
El Camino Real 45 mph 4 Broad curves, lane discipline
I-280 Freeway 65 mph 6 High-speed sweepers
I-280 West 65 mph 6 Return highway, merge home

Highway sections feature paved shoulders, concrete jersey barriers, and green verge strips. Local roads have sidewalks, parked cars, street trees, and buildings. The Exit Ramp has metal guardrails. All road markings follow MUTCD standards (dashed white lane dividers at 10ft/30ft spacing, double yellow centerlines on two-way roads, white median stripes on divided highways).

288 waypoints define the route at ~11-13m spacing for smooth curve rendering. The route was generated procedurally using a turtle-graphics approach with arc and line primitives to ensure no sharp discontinuities.

Real-Time Synthesised Audio

The SoundEngine class generates all audio in real-time on a background thread via sounddevice — no pre-recorded samples, no audio files. Every sound reacts to live car physics:

# Engine: 4-cylinder harmonic profile driven by RPM
fund_hz = rpm / 60.0 * 2.0  # 4-cyl fires twice per rev
engine = (sin(phase) * 1.0           # fundamental
        + sin(phase * 2) * 0.55      # 2nd harmonic
        + sin(phase * 3) * 0.25      # 3rd harmonic
        + sin(phase * 4) * 0.12)     # 4th harmonic
# Volume: idle hum + throttle response + rev-based rise
volume = 0.06 + throttle * 0.30 + rpm_fraction * 0.15
Sound Source Trigger
Engine Phase-continuous sine oscillator with 4 harmonics Always — pitch from RPM, volume from throttle
Tire screech Bandpass-modulated noise at 1800 Hz Lateral accel > 3 m/s^2 or understeer detected
Wind Highpass-filtered noise Speed > 27 mph, scales with velocity
Road rumble Lowpass-filtered noise at 120 Hz Always — volume proportional to speed

All filters are fully vectorised with numpy — no Python loops in the audio callback. Phase is continuous across blocks to prevent clicks. Volume changes are smoothed with exponential moving averages (50-150ms time constants) to eliminate pops.


Running It

Requirements

  • macOS (Apple Silicon or Intel with Metal support)
  • Python 3.9+
  • Logitech G920 steering wheel (USB)

Setup

pip install pygfx numpy pyobjc-framework-GameController pyobjc-framework-Cocoa

# Switch G920 from Xbox compatibility mode to native HID mode
python companion/g920_native_switch.py

# Run the simulator
python companion/drive3d.py

Controls

Input Action
G920 wheel Steering
Gas pedal Throttle (pressure-sensitive)
Brake pedal Brake (pressure-sensitive)
Right paddle Upshift
Left paddle Downshift
Space Restart (shows drive report)
Escape Quit
T Toggle performance trace

The simulator starts in Ghost Lap mode — watch and feel the wheel move through correct steering inputs for 45 seconds. Then it switches to Your Turn — drive the same route yourself while the coach scores you.


What Makes This Different

It's not a game. There are no points for drifting, no boost pads, no finish lines. The scoring penalizes exactly the behaviors that cause real accidents — running red lights, following too close, overcorrecting on the steering, panic braking. Every metric maps directly to California DMV evaluation criteria.

The physics are real. The bicycle model, friction circle, load transfer, and SAT-based FFB are the same models used in professional driving simulators. The tire feel — heavy in a corner, light at the limit — is the actual mechanism through which experienced drivers perceive grip. Teaching this feel to new drivers before they encounter it at 65 mph on I-280 is the entire point.

It runs on a laptop. No $50,000 motion platform. No dedicated sim rig. A $250 G920 clamped to a desk and a MacBook. The wgpu/Metal rendering pipeline means no GPU compatibility issues on any Mac made after 2020.

The FFB driver is open source and correct. There is no other working implementation of G920 force feedback on macOS that we're aware of. The HID++ 2.0 protocol implementation in wheel.py handles the macOS-specific report ID mismatch, the 200ms hardware watchdog, and the full handshake sequence. It could be extracted as a standalone library for any macOS application that needs G920 FFB.


License

Copyright 2026 DriveCoach. All rights reserved.

This source code is proprietary and confidential. No part of this codebase may be reproduced, distributed, or transmitted in any form without prior written permission.


Built at a hackathon in Sunnyvale, California. The roads are modeled after real streets.

About

Force feedback driving simulator that teaches real-world driving skills through a Logitech G920 on macOS. Physics-accurate Honda Civic, DMV scoring, ghost coaching.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages