Skip to content

FAST-LIO pcap record/replay + Virtual Livox#2498

Draft
jeff-hykin wants to merge 15 commits into
mainfrom
jeff/feat/fastlio_record2
Draft

FAST-LIO pcap record/replay + Virtual Livox#2498
jeff-hykin wants to merge 15 commits into
mainfrom
jeff/feat/fastlio_record2

Conversation

@jeff-hykin

Copy link
Copy Markdown
Member

Adds virtual mid360 pcap record/replay to FAST-LIO; rips out the in-process pcap reader and the output-cloud downsampling/denoising.

Usage / Testing

Pcap to DB

# snippet that normally diverges
PCAP_PATH="$(python -c "from dimos.utils.data import get_data; print(get_data('ruwik2_part3/ruwik2_part3.pcap'))")"

# gen .db from pcap
python -m dimos.hardware.sensors.lidar.fastlio2.tools.pcap_to_db --pcap "$PCAP_PATH"
# add to existing .db
python -m dimos.hardware.sensors.lidar.fastlio2.tools.pcap_to_db --db mem2.db  --pcap "$PCAP_PATH"

Record a Mid-360 pcap

# Allow using tcpdump without sudo (only need to do once)
sudo setcap cap_net_raw,cap_net_admin=eip "$(which tcpdump)"
# set this (env var or python module config)
export DIMOS_PCAP_IFACE=enp2s0
from dimos.core.coordination.blueprints import autoconnect
from dimos.core.coordination.module_coordinator import ModuleCoordinator
from dimos.hardware.sensors.lidar.livox.pcap_recorder import LivoxPcapRecorder

record = autoconnect(
    LivoxPcapRecorder.blueprint(
        pcap_path="recordings/run1.pcap",
        # lidar_ip="192.168.1.107",
        # record_pcap_iface="enp2s0",
    ),
)
ModuleCoordinator.build(record).loop()

Replay a pcap to a module

from dimos.core.coordination.blueprints import autoconnect
from dimos.core.coordination.module_coordinator import ModuleCoordinator
from dimos.hardware.sensors.lidar.livox.virtual_mid360.module import VirtualMid360
from dimos.hardware.sensors.lidar.fastlio2.module import FastLio2
from dimos.visualization.vis_module import vis_module

replay = autoconnect(
    VirtualMid360.blueprint(
        pcap="recordings/run1.pcap",
        # lidar_ip="192.168.1.155",
    ),
    FastLio2.blueprint(),
    vis_module("rerun"),
).global_config(n_workers=3)
ModuleCoordinator.build(replay).loop()

Port the minimal pcap-replay subsystem from jeff/feat/go2_record into the
clean branch so FAST-LIO can run offline from a Mid-360 pcap, matching the
Point-LIO pcap_to_db workflow:

- cpp: pcap_replay.hpp + timing.hpp (header-only), main.cpp refactored so the
  main loop runs from either the live SDK or a pcap feeder thread, with an
  optional deterministic sensor-clock mode. Keeps the clean branch's
  velocity-cap (guarded set_max_velocity_norm_ms) and flake (velocity-cap
  fast-lio); does not pull go2_record's tcpdump record path.
- module.py: replay_pcap / replay_skip_until_ns / first_packet_marker /
  deterministic_clock config fields; skip network validation in replay mode.
- tools/pcap_to_db.py: replay a pcap through FastLio2 (real-time, non-
  deterministic) and append fastlio_odometry + fastlio_lidar into an existing
  memory2 db, time-aligned onto its clock. --force overwrites.
…FastLio2Recorder

- livox/pcap_recorder.py: standalone tcpdump pcap capture (LivoxPcapRecorder),
  decoupled from FAST-LIO. The lidar/SLAM module no longer owns packet capture.
- fastlio2/recorder.py: FastLio2Recorder records fastlio_odometry + fastlio_lidar
  and rewrites ONLY those streams' timestamps onto the db clock (promoted from
  pcap_to_db's inline recorder; fixes the ts==0.0 falsy-fallback bug).
- pcap_to_db.py now imports FastLio2Recorder instead of an inline copy.
FastLio2 no longer produces a global voxel map — odometry + registered lidar
only. Removed global_map Out, map config, and the mapping.GlobalPointcloud spec.
Updated all consumers (fastlio_blueprints, alfred_nav, g1_onboard, g1_nav_onboard,
mobile, pcap_to_db) to drop map args + the global_map remap. Nav blueprints lose
their fastlio map; full nav wants a separate mapper wired in (follow-up).
Remove the VoxelMap global-map machinery from the native binary: the
voxel_map.hpp include, g_map_topic, map_freq/map_voxel_size/map_max_range args,
the VoxelMap instance + insert/prune/publish loop, and its timing sections.
FAST-LIO now only publishes registered lidar + odometry.
Testing on the ruwik2_pt3 replay showed 0.5 is borderline — it bounds most
runs but still diverged ~1 in 3 (the divergence is stochastic; FAST-LIO isn't
bit-exact). acc_cov=1.0 held bounded across every rep (~4 m/s, matching
Point-LIO), so default to 1.0 for margin.
Port the Rust virtual_mid360 native module (fake Mid-360 on a virtual NIC that
replays a pcap with a synthesized SDK2 handshake) and make pcap_to_db a live-only
recorder driven by it: FastLio2 runs in live SDK mode, fed by virtual_mid360 over
a veth via tools/replay_via_virtual_mid360.sh, and the two fastlio streams are
recorded + time-aligned into a memory2 db. Replaces the in-process pcap reader
(removed next). The harness uses a configurable $SUDO for the netns setup.
virtual_mid360 is now the only replay path, so strip the in-process reader from
the binary: delete cpp/pcap_replay.hpp and remove the feeder thread, virtual
clock, deterministic-clock mode, and first-packet-marker from main.cpp; drop
replay_pcap/deterministic_clock/replay_skip_until_ns/first_packet_marker from
module.py and always validate the network. FastLio2 is now a pure live-SDK binary.
Restore the simple CLI on top of the over-the-wire replay: 'pcap_to_db --pcap X'
builds <X>.db, '--pcap X --db Y' appends. The tool now sets up the netns/veth
itself (via $SUDO), runs virtual_mid360 streaming the pcap in one ns and a
FastLio2 live-SDK recorder in the other, records + time-aligns the two fastlio
streams, stops when the pcap drains, and hands the (root-written) db back to the
caller via chown. Folds in the old replay_via_virtual_mid360.sh wrapper, which
is removed.
Grab pointlio's virtual_mid360 updates (single-pass streamer, configurable
multicast, required lidar_ip/host_ip/lidar_netns). Update the FastLio2 demo
blueprint + pcap_to_db's VM config JSON for the now-required fields. Fix the
orchestrator stop: watch virtual_mid360 log 'data stream finished', drain the
scan backlog, then stop the recorder via a stop-file (stream-stagnation is
unreliable — a diverging FastLio2 keeps emitting after the sensor goes quiet).
Verified: --pcap reproduces divergence and stops cleanly.
…ng/comment cleanup)

Grab pointlio's newest virtual_mid360: Config now defaults pcap/lidar_ip/host_ip/
lidar_netns from DIMOS_MID360_* env vars (field names unchanged, so the
pcap_to_db orchestrator's explicit JSON still applies), meaningful Rust names,
trimmed comments. Kept my flake.lock (pointlio's pins their branch's dimos-repo
rev) and my FastLio2 demo blueprint. Rebuilt VM; --pcap interface verified.
…lio)

Remove cloud_filter.hpp (PCL voxel-grid + statistical-outlier-removal) and its
config (voxel_size/sor_mean_k/sor_stddev); publish_lidar now emits the registered
world-frame cloud as-is. The pre-KF downsample (FAST-LIO's own filter_size_surf
in mid360.yaml) is untouched. Mirrors pointlio c64c2ba.
…-deref race)

On shutdown, LivoxLidarSdkUninit() (which stops + joins the SDK callback
threads) now runs before g_fastlio/g_lcm are nulled, so a late on_point/on_imu
callback can't race the assignment and dereference null. Mirrors pointlio
c4fa47b.
@codecov

codecov Bot commented Jun 15, 2026

Copy link
Copy Markdown

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
1881 1 1880 155
View the top 1 failed test(s) by shortest run time
dimos.project.test_no_sections::test_no_section_markers
Stack Traces | 0.477s run time
def test_no_section_markers():
        """
        Fail if any file contains section-style comment markers.
    
        If a file is too complicated to be understood without sections, then the
        sections should be files. We don't need "subfiles".
        """
        violations = find_section_markers()
        if violations:
            report_lines = [
                f"Found {len(violations)} section marker(s). "
                "If a file is too complicated to be understood without sections, "
                'then the sections should be files. We don\'t need "subfiles".',
                "",
            ]
            for path, lineno, text in violations:
                report_lines.append(f"  {path}:{lineno}: {text.strip()}")
>           raise AssertionError("\n".join(report_lines))
E           AssertionError: Found 4 section marker(s). If a file is too complicated to be understood without sections, then the sections should be files. We don't need "subfiles".
E           
E             .../fastlio2/tools/pcap_to_db.py:112: # ---------------------------------------------------------------------------
E             .../fastlio2/tools/pcap_to_db.py:114: # ---------------------------------------------------------------------------
E             .../fastlio2/tools/pcap_to_db.py:294: # ---------------------------------------------------------------------------
E             .../fastlio2/tools/pcap_to_db.py:296: # ---------------------------------------------------------------------------

lineno     = 296
path       = '.../fastlio2/tools/pcap_to_db.py'
report_lines = ['Found 4 section marker(s). If a file is too complicated to be understood without sections, then the sections should ....../fastlio2/tools/pcap_to_db.py:296: # ---------------------------------------------------------------------------']
text       = '# ---------------------------------------------------------------------------'
violations = [('.../fastlio2/tools/pcap_to_db.py', 112, '# -----------------------------------------------....../fastlio2/tools/pcap_to_db.py', 296, '# ---------------------------------------------------------------------------')]

dimos/project/test_no_sections.py:145: AssertionError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

Condense the over-explanatory comments/docstrings on the fastlio recorder work
(main.cpp, timing.hpp, module.py, recorder.py, pcap_to_db.py, pcap_recorder.py,
virtual_mid360/blueprints.py), keeping license headers + non-obvious rationale
(velocity cap, mutex atomicity, shutdown-race, chown-for-root, stop-file logic).
Also fixes two now-stale references: timing.hpp's removed replay feeder and
blueprints.py's deleted replay_via_virtual_mid360.sh (-> pcap_to_db.py).
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.

1 participant