Skip to content

xil: FIXS ships the simulated dyno; fixs.config.get; openpilot's hold; fixs.driver startInGear - #416

Merged
yunlishao merged 9 commits into
dev_v0.9.0from
feature/xil_udp_lockstep
Sep 29, 2026
Merged

yunlishao merged 9 commits into
dev_v0.9.0from
feature/xil_udp_lockstep

Conversation

@yunlishao-vibe

@yunlishao-vibe yunlishao-vibe commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Summary

FIXS ships the simulated dyno and nothing else; a rig on the wire is the application's. This PR gives an application what it needs to do that cleanly, and makes the simulated dyno able to start the way a real openpilot-driven rig does.

fixs.config.get(section): the scenario yaml, read-only

xil = fixs.config.get('xil')
xil['transport']      # 'inprocess' | 'udp' | 'tcp'
xil['ip'], xil['port']  # first VehicleSubscription's first ip and port
  • Reads the yaml this run uses ($FIXS_CONFIG_YAML, or the one fixs.connect() resolved).
  • Names are the yaml's, lowercased: a section drops Setup, every key goes to snake_case (XilSetup.EnableXil → get('xil')['enable_xil']).
  • Values are parsed (ConfigHelper's defaults and validation); a section the yaml omits still answers with its defaults.
  • Read-only: each call returns its own copy behind a MappingProxyType. Named get so a set can come later.

fixs.xil.dynosim(): the simulated dyno, a plain constructor

  • fixs.xil.dynosim(vehicle=..., dyno=..., driver=...) replaces fixs.xil.dyno(). It builds the simulated dyno from the parameters it is given and reads no scenario and no transport. Whether a dyno is in the loop is the caller's question: fixs.xil.enabled() and fixs.config.get('xil')['transport'].
  • Transport: udp means the application talks to its own rig at ip/port; FIXS opens no socket. tcp stays a valid value, reserved.
  • exchange(vref, aref, dt): the acceleration command is explicit and comes from the caller. The dyno never derives it.
  • XilSetup.Vehicle/Dyno/Driver are removed from ConfigHelper.py; no scenario set them.
  • The FIXS-side udp lockstep this branch first added (_Dyno udp/tcp, UdpLink.drain()/recv_wait(), UDP_REPLY_TIMEOUT_S, UDP_MAX_MISSES) is removed.

The dyno's robot driver can hold like openpilot

RobotDriver brakes while slower than stop_speed_mps and asked for less than stop_accel_mps2: openpilot's should_stop (drive_helpers.py: v_ego < 0.3 and a_target < 0.1). Both default to 0.0, which is off, so a default bench is unchanged. Set through dynosim(driver={'stop_speed_mps': 0.3, 'stop_accel_mps2': 0.1}); the rule reads the aref passed to exchange.

fixs.driver: startInGear, and fixs.driver.set()

CARLA's automatic gearbox leaves a spawned car in neutral below about 15% throttle. Measured on vehicle.tesla.model3: 10% never moves (gear stays 0), 15% moves after 3.6 s, 20% at once; with gear 1 forced, 2% throttle moves it. A pedal-driven ego asked to creep therefore never moves.

  • startInGear=True: on its first controlled step fixs.driver calls carla.ego.apply_control(carla.VehicleControl(manual_gear_shift=True, gear=1)) and writes no pedal command, so the host does not overwrite it. The autobox keeps first gear from then on, through stops. No creep at zero throttle. Off by default.
  • fixs.driver.set(**options): options for every driver built after the call, so a controller can set one conditionally and keep Driver = fixs.driver(exchange) plain. An option passed to fixs.driver(...) still wins; unknown names are refused.

Kept from the first commit

LocalLink rounds values through the same float32 packet the wire carries, so an inprocess run and a run over the wire see the same numbers. Behaviour change: inprocess results shift slightly against runs made before this branch.

Related Issues

No tracker. Found while preparing the ORNL rig test (comma.ai openpilot drives the vehicle on their hub dyno). The application side is ORNL-Real-Sim/FIXS_Applications#84 (MLK eco-driving rung 6), which depends on this PR.

How to test

pytest tests/Python/unit -q

New: test_fixs_config.py; the hold (test_xil_dyno_hookup.py: off by default, a flat 0.15 m/s with aref 0 stays at rest, aref 1.5 lets go, no hold above 0.3 m/s); startInGear and fixs.driver.set (test_fixs_driver.py). End-to-end runs are on #84.

Checklist

  • Code compiles/runs as expected
  • Tests pass locally (tests/Python/unit: 492 passed, 8 skipped)
  • Documentation is updated (doc/ConfigSetup.md: Transport row, fixs.config.get, fixs.xil.dynosim())
  • Issue linked above

Not changed: the C++ bridge (VirCarlaEnv); startInGear lives in fixs.driver, so it needs nothing there.

…actly

Transport: udp read the cell without waiting, so each tick got the answer
to the PREVIOUS tick's reference: one CARLA step behind inprocess. The udp
exchange now drains stale packets, sends, and blocks for this tick's reply
(UDP_REPLY_TIMEOUT_S 0.1 s). A miss passes the reference through and is
counted; UDP_MAX_MISSES (20) in a row stops the run with the cell's address.

LocalLink now carries the same 8-byte float32 packet as the wire. With
full precision the ~1e-7 rounding alone, fed back through the closed loop,
grew to 2.8 m/s of ego speed within 200 s against an otherwise identical
udp run. Inprocess results therefore shift slightly from before.

Measured on MLK rung 5 (to 29300 s, 3999 ticks): inprocess and udp logs
byte-identical, agent and bridge.
The block quoted a 0.73 s first-tick cost that the same change had already
removed, and read as current. Three lines now.
fixs.config.get(section) reads the run's scenario yaml, read-only, by
lowercase names (get('xil')['transport']), with ip/port taken from the
section's first VehicleSubscription.

fixs.xil.dyno() builds the inprocess dyno and returns None for udp and
tcp: an application that speaks to a rig opens its own socket, to
fixs.config.get('xil')['ip']. tcp stays accepted as a reserved value.

The udp lockstep this branch added to _Dyno and UdpLink (drain,
recv_wait, UDP_REPLY_TIMEOUT_S, UDP_MAX_MISSES) goes with it. LocalLink
still rounds through the float32 packet.
@yunlishao-vibe yunlishao-vibe changed the title xil: udp dyno exchange is lockstep, so a udp run matches inprocess exactly xil: FIXS ships the simulated dyno; fixs.config.get reads the scenario for a rig of the app's own Sep 28, 2026
It builds the simulated dyno the scenario describes whenever EnableXil
is on. Which dyno answers a run is the application's choice, made from
fixs.config.get('xil')['transport']; a udp application's stand-in rig
can now build the same dynosim from the same scenario.
@yunlishao-vibe yunlishao-vibe changed the title xil: FIXS ships the simulated dyno; fixs.config.get reads the scenario for a rig of the app's own xil: FIXS ships the simulated dyno (fixs.xil.dynosim); fixs.config.get reads the scenario Sep 28, 2026
It builds the simulated dyno from the vehicle, dyno and driver parameters
it is given. Whether a dyno is in the loop is the caller's question
(fixs.xil.enabled, fixs.config.get('xil')), so a udp application's
stand-in rig can build the same dynosim with no yaml at all.

XilSetup.Vehicle/Dyno/Driver go with it; no scenario set them.
…aref, dt)

RobotDriver brakes while slower than stop_speed_mps and asked for less
than stop_accel_mps2 -- openpilot's should_stop (0.3 m/s, 0.1 m/s^2).
Both default to 0.0, which is off, so a default bench is unchanged.

The acceleration command is explicit: dynosim().exchange(vref, aref, dt)
takes it from the caller and hands it to the driver's hold. Nothing in
the bench derives it.
CARLA's autobox starts a vehicle in neutral and does not leave it below
about 15% throttle (measured on vehicle.tesla.model3: 10% never moves,
15% moves after 3.6 s). A pedal-driven ego asked to creep never moves.
One control with manual_gear_shift=True, gear=1 right after spawn
engages first; the autobox keeps it through later stops, and 2-5%
throttle then moves the car. No creep at zero throttle.

The C++ bridge (VirCarlaEnv) does not have this yet.
fixs.driver(exchange, firstGear=True) calls carla.ego.apply_control with
manual_gear_shift=True, gear=1 on its first controlled step and commands
nothing else that step, so the host does not overwrite it; the autobox
keeps first from then on. Off by default.

Replaces the bridge doing this for every ego (47a00f2).
…ine stays plain

fixs.driver.set(**options) records options for every driver built after
the call; fixs.driver(exchange, **options) still wins over them. The
first-gear option is renamed startInGear (was firstGear), off by default.
@yunlishao-vibe yunlishao-vibe changed the title xil: FIXS ships the simulated dyno (fixs.xil.dynosim); fixs.config.get reads the scenario xil: FIXS ships the simulated dyno; fixs.config.get; openpilot's hold; fixs.driver startInGear Sep 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants