Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
176 commits
Select commit Hold shift + click to select a range
7ef91f6
pointlio native module: mirror fastlio2 module (PointLio class, point…
jeff-hykin Jun 3, 2026
2b31c91
pointlio native: define ROOT_DIR for the laserMapping debug paths
jeff-hykin Jun 3, 2026
0500790
pointlio native: lock fast-lio input to built pointlio rev (pointlio_…
jeff-hykin Jun 3, 2026
a14bae8
pointlio native: point fast-lio input at github remote (was macOS-onl…
jeff-hykin Jun 4, 2026
8d98e13
pointlio native: add mid360-pointlio blueprints
jeff-hykin Jun 4, 2026
f8a6b65
pointlio native: replay harness, deterministic clock, pcap tooling + …
jeff-hykin Jun 5, 2026
fa6325d
pointlio native: consume fast-lio from github instead of local path
jeff-hykin Jun 5, 2026
89151a7
pointlio_native: fix replay thread race + iVox glog dep + master-fait…
jeff-hykin Jun 11, 2026
b35d0f2
pointlio_native: pin fast-lio to local path so production nix build c…
jeff-hykin Jun 11, 2026
4303fd6
pointlio_native: repoint fast-lio at pushed github pointlio (02d5066)
jeff-hykin Jun 11, 2026
360133c
pointlio: pcap_to_db time-aligns pointlio_lidar + odometry into a db,…
jeff-hykin Jun 12, 2026
21cb8f2
Merge branch 'main' into jeff/feat/pointlio_native
jeff-hykin Jun 13, 2026
01d8872
pointlio: single default.yaml config; drop extra sensor presets + dem…
jeff-hykin Jun 13, 2026
7ee3308
pointlio: pcap_to_db builds a db from scratch; keep recorder out of r…
jeff-hykin Jun 13, 2026
a472d25
pointlio: document lidar_type enum + link Livox CustomMsg in default.…
jeff-hykin Jun 13, 2026
3b5f988
pointlio: one default.yaml (strip ROS-only keys, note mid360 params);…
jeff-hykin Jun 13, 2026
c53c73a
pointlio: add ruwik2_part3 divergence sample to LFS; fix from-scratch…
jeff-hykin Jun 13, 2026
d48a945
pointlio: pcap_to_db docstring uses nested get_data for the ruwik2_pa…
jeff-hykin Jun 13, 2026
c062727
pointlio: address greptile review — fix stale fastlio docstring; ts=0…
jeff-hykin Jun 13, 2026
22d9ec0
feat(virtual_mid360): Rust native module — pcap replay over a virtual…
jeff-hykin Jun 13, 2026
0504aae
feat(pointlio): publish body-frame cloud, split frame_ids, remove glo…
jeff-hykin Jun 13, 2026
333ece4
merge: bring virtual_mid360 rust module into pointlio branch
jeff-hykin Jun 13, 2026
8bb723b
feat(virtual_mid360): Python NativeModule wrapper for blueprints
jeff-hykin Jun 13, 2026
32757d6
feat(virtual_mid360): add demo_virtual_mid360_pointlio blueprint + re…
jeff-hykin Jun 13, 2026
4570885
fix(virtual_mid360): don't fetch LFS pcap at blueprint import
jeff-hykin Jun 13, 2026
3aff05f
fastlio2: offline pcap replay + pcap_to_db tool (mirror pointlio)
jeff-hykin Jun 13, 2026
6dbb391
fastlio2: pcap_to_db builds a db from scratch + privatize recorder
jeff-hykin Jun 13, 2026
24c19fa
fastlio2: set mid360 acc_cov=0.5 to bound velocity divergence
jeff-hykin Jun 13, 2026
8d9a461
lidar: split recording — standalone LivoxPcapRecorder + ts-rewriting …
jeff-hykin Jun 13, 2026
1088fe3
fastlio2: rip out the global_map output (Python)
jeff-hykin Jun 13, 2026
c64047d
fastlio2: rip out the global_map output (C++)
jeff-hykin Jun 13, 2026
4725bcb
fastlio2: bump mid360 acc_cov 0.5 -> 1.0 for reliable bounding
jeff-hykin Jun 14, 2026
9bb8315
refactor(pointlio): remove in-process pcap replay; use virtual_mid360…
jeff-hykin Jun 15, 2026
a24e29f
feat(pointlio): new pcap_to_db driven by the rust virtual_mid360 replay
jeff-hykin Jun 15, 2026
05aa9d9
refactor(pointlio): drop mount/init_pose offset — publish in the sens…
jeff-hykin Jun 15, 2026
e741ad8
virtual_mid360: over-the-wire pcap replay for fastlio (live SDK path)
jeff-hykin Jun 15, 2026
1c42128
fastlio2: remove the in-process pcap replay subsystem
jeff-hykin Jun 15, 2026
7bf679e
feat(virtual_mid360): make point/IMU multicast group configurable
jeff-hykin Jun 15, 2026
1118f23
pcap_to_db: orchestrate virtual_mid360 behind the --pcap interface
jeff-hykin Jun 15, 2026
b266301
chore(pointlio): trim verbose comments; fix two stale ones
jeff-hykin Jun 15, 2026
d7ffc6f
feat(virtual_mid360): read demo blueprint pcap from VIRTUAL_MID360_PC…
jeff-hykin Jun 15, 2026
0f48de5
refactor(virtual_mid360): require lidar_ip/host_ip/lidar_netns (not L…
jeff-hykin Jun 15, 2026
5f27cfd
feat(virtual_mid360): tcpdump pcap recorder for raw Livox Mid-360 UDP
jeff-hykin Jun 15, 2026
3d077c4
fix(virtual_mid360): kill tcpdump via unconfined label when AppArmor …
jeff-hykin Jun 15, 2026
d987f87
mid360 recorder: scream loudly + narrow sudoers hint when tcpdump kil…
jeff-hykin Jun 15, 2026
ab4c19d
mid360 recorder: include long-term sudoers fix in the bash reaper's f…
jeff-hykin Jun 15, 2026
8376809
virtual_mid360: drop verbose default fn + rename single-char vars
jeff-hykin Jun 15, 2026
344560c
pointlio/timing: replace single-letter var names with meaningful ones
jeff-hykin Jun 15, 2026
bea9ec8
virtual_mid360: pull pointlio's latest + fix replay stop logic
jeff-hykin Jun 15, 2026
90079f3
virtual_mid360: drop stale handshake TODOs
jeff-hykin Jun 15, 2026
ec184cb
pointlio: fix FAST-LIO mislabels in prose/logs (module is Point-LIO)
jeff-hykin Jun 15, 2026
2e7c755
pointlio cpp: drop timing.hpp, rename FastLio->PointLio, body frame i…
jeff-hykin Jun 15, 2026
6fb4b09
pointlio: review fixes + required network IPs + frame-id rename
jeff-hykin Jun 15, 2026
4eadca8
pointlio cpp: bump fast-lio flake to 36a3eef (PointLio class rename)
jeff-hykin Jun 15, 2026
2a0f51c
pointlio: TF timestamp = odometry ts exactly (drop falsy-0.0 wall-clo…
jeff-hykin Jun 15, 2026
948c3f5
virtual_mid360: give terse rust names meaningful ones
jeff-hykin Jun 15, 2026
b4a455b
virtual_mid360/pcap_to_db: condense verbose comments
jeff-hykin Jun 15, 2026
76743e0
pointlio cpp: forward Point-LIO velocity in odometry twist (was zeroed)
jeff-hykin Jun 15, 2026
5fdaf56
virtual_mid360 demo: take addresses from env vars, no hardcoded IPs
jeff-hykin Jun 15, 2026
3ec490d
pointlio/virtual_mid360: strip remaining verbose/useless comments
jeff-hykin Jun 15, 2026
e06c29f
virtual_mid360: default Config from VIRTUAL_MID360_* env vars; slim t…
jeff-hykin Jun 15, 2026
95383f8
virtual_mid360: rename env vars VIRTUAL_MID360_* -> DIMOS_MID360_*
jeff-hykin Jun 15, 2026
0dd0071
pointlio: make host_ip fully optional (derive from lidar_ip's subnet)
jeff-hykin Jun 15, 2026
1f21def
clean
jeff-hykin Jun 15, 2026
9a12a30
-
jeff-hykin Jun 15, 2026
6f70e29
pointlio cpp: bump fast-lio pin to 46b1a9e (macOS libc++/apple-sdk-26…
jeff-hykin Jun 15, 2026
d234536
virtual_mid360: re-lock dimos-repo to repo root (stale 4-level path →…
jeff-hykin Jun 15, 2026
12c4d13
Merge branch 'jeff/feat/pointlio_native' of github.com:dimensionalOS/…
jeff-hykin Jun 15, 2026
be4c1f8
pcap_to_db: make netns opt-in; default to no-netns aliases on one host
jeff-hykin Jun 15, 2026
460168c
pointlio: human-readable filter config + raw_cloud bypass; expose pre…
jeff-hykin Jun 15, 2026
3a8fc55
pointlio cpp: unwrap multi-line printf/statements onto single lines
jeff-hykin Jun 15, 2026
e2a4d85
pointlio: drop last fastlio names (pointlio_debug + pointlio.hpp head…
jeff-hykin Jun 15, 2026
7ee0c31
pointlio module.py: tighten verbose config/comment lines
jeff-hykin Jun 15, 2026
226a330
-
jeff-hykin Jun 15, 2026
1e7bf28
pointlio cpp: fix statements swallowed into trailing // comments (P0)
jeff-hykin Jun 15, 2026
c4fa47b
pointlio cpp: uninit Livox SDK before clearing globals (fix shutdown …
jeff-hykin Jun 15, 2026
d7aa546
-
jeff-hykin Jun 15, 2026
7f49b00
Merge branch 'jeff/feat/pointlio_native' of github.com:dimensionalOS/…
jeff-hykin Jun 15, 2026
c35dd76
virtual_mid360: pull pointlio's latest (env-var config defaults, nami…
jeff-hykin Jun 15, 2026
b245098
rename LFS sample ruwik2_part3 -> mid360_shake_stairs
jeff-hykin Jun 15, 2026
c64c2ba
pointlio: rip out output-cloud downsampling + denoising
jeff-hykin Jun 15, 2026
773d02c
Merge branch 'main' into jeff/feat/pointlio_native
jeff-hykin Jun 15, 2026
de3b32f
fastlio2: rip out output-cloud downsampling + denoising (mirror point…
jeff-hykin Jun 15, 2026
6f33458
fastlio2: uninit Livox SDK before clearing globals (fix shutdown null…
jeff-hykin Jun 15, 2026
f91f4e9
Merge branch 'main' into jeff/feat/pointlio_native
jeff-hykin Jun 16, 2026
fc682b3
Merge remote-tracking branch 'origin/main' into jeff/feat/pointlio_na…
jeff-hykin Jun 16, 2026
24cb75a
fastlio2: trim verbose comments across the recorder files
jeff-hykin Jun 16, 2026
d6d979f
Merge remote-tracking branch 'origin/main' into jeff/feat/pointlio_na…
jeff-hykin Jun 16, 2026
45bffc3
fastlio2: condense verbose acc_cov / filter_size comments in mid360.yaml
jeff-hykin Jun 16, 2026
bf858c4
fastlio2: remove mount/init_pose machinery + timing.hpp (mirror point…
jeff-hykin Jun 16, 2026
94cf54f
fastlio2: drop // ---- section banners (mirror pointlio's single-line…
jeff-hykin Jun 16, 2026
aca9e56
fastlio2: tighten the last multi-line pcap_to_db stop-logic comment
jeff-hykin Jun 16, 2026
0f7be3a
merge
jeff-hykin Jun 17, 2026
2fbdb79
virtual_mid360: use tracing::error! instead of eprintln! for startup …
jeff-hykin Jun 17, 2026
6d0833a
virtual_mid360 recorder: default lidar_ip from DIMOS_MID360_LIDAR_IP …
jeff-hykin Jun 17, 2026
8b4c130
livox Mid360: honor DIMOS_MID360_LIDAR_IP env for lidar_ip (fallback …
jeff-hykin Jun 17, 2026
ca9d276
livox: share host_ip auto-detect between Mid360 driver and Point-LIO
jeff-hykin Jun 17, 2026
e9f8096
livox Mid360: host_ip defaults to None (machine-specific, auto-detect…
jeff-hykin Jun 17, 2026
027f214
clean
jeff-hykin Jun 17, 2026
28b01f8
fix pre-existing mypy 3.10 errors (Unpack import, numpy no-any-return)
jeff-hykin Jun 17, 2026
f31b1c8
feat(pointlio): pcap_to_db --config (pwd-relative) + --odom/lidar-str…
jeff-hykin Jun 17, 2026
9c4724f
cleanup
jeff-hykin Jun 17, 2026
a5256e9
macos fixes
jeff-hykin Jun 17, 2026
b4109a1
macos fix
jeff-hykin Jun 17, 2026
4d3f1c1
fix(pointlio): pcap_to_db doc paths + self-terminate on lidar-stream …
jeff-hykin Jun 17, 2026
723d6fa
fix(virtual_mid360): derive Serialize on Config for NativeConfig bound
jeff-hykin Jun 17, 2026
0953506
fix(pointlio): serialize Point-LIO EKF access across IMU + main threads
jeff-hykin Jun 17, 2026
da415cd
Merge remote-tracking branch 'origin/main' into jeff/feat/pointlio_na…
jeff-hykin Jun 17, 2026
4d3c79d
fix(virtual_mid360): use #[native_config] for Config (main dimos-modu…
jeff-hykin Jun 17, 2026
1ad8031
Merge remote-tracking branch 'origin/main' into jeff/feat/fastlio_rec…
jeff-hykin Jun 17, 2026
ec5594d
feat(pointlio): pcap_to_db writes a <db>.rrd quick-look
jeff-hykin Jun 17, 2026
35a8ad9
feat(map): map global registers clouds by per-frame pose; --go2 for o…
jeff-hykin Jun 17, 2026
55099fc
fastlio2: mirror pointlio's virtual_mid360 evolution (self-provision …
jeff-hykin Jun 17, 2026
c458de8
fix(pointlio): use z.max()-z.min() not np.ptp in pcap_to_db .rrd grad…
jeff-hykin Jun 17, 2026
39cf383
fastlio2: make host_ip/lidar_ip optional + env-driven (mirror pointlio)
jeff-hykin Jun 17, 2026
3fb1a54
feat(pointlio): recorder stamps lidar poses; pcap_to_db auto-stops by…
jeff-hykin Jun 17, 2026
10faefe
Merge branch 'main' into jeff/feat/pointlio_native
jeff-hykin Jun 17, 2026
6ffaad4
feat(map): add --carve/--no-carve toggle to map global, default off
leshy Jun 17, 2026
ec86efa
Revert "fix pre-existing mypy 3.10 errors (Unpack import, numpy no-an…
jeff-hykin Jun 17, 2026
4b22007
Revert "fix pre-existing mypy 3.10 errors (Unpack import, numpy no-an…
jeff-hykin Jun 17, 2026
62e71b7
Merge branch 'jeff/feat/pointlio_native' into jeff/feat/fastlio_record2
jeff-hykin Jun 18, 2026
721a290
fastlio2: move config from YAML files to FastLio2Config (literal-unio…
jeff-hykin Jun 18, 2026
e17e6b1
fastlio2 pcap_to_db: mirror pointlio rrd + span auto-stop; get_data d…
jeff-hykin Jun 18, 2026
9faea3b
fastlio2/pointlio: recorders inherit memory2 Recorder, override pose …
jeff-hykin Jun 18, 2026
c995b0a
fastlio2: fov_degree is an int in the generated YAML
jeff-hykin Jun 18, 2026
86d8f68
Merge remote-tracking branch 'origin/main' into jeff/feat/fastlio_rec…
jeff-hykin Jun 18, 2026
f2aeab4
pointlio: move config from default.yaml to PointLioConfig (literal-un…
jeff-hykin Jun 18, 2026
9b38016
memory2: add TfRecorder; fastlio/pointlio recorders inherit it via @p…
jeff-hykin Jun 18, 2026
9335298
fastlio2: publish sensor/body-frame lidar (get_body_cloud), like poin…
jeff-hykin Jun 18, 2026
b2eb4c7
fastlio2: reference dimos-module-fastlio2 body-cloud branch (drop fla…
jeff-hykin Jun 18, 2026
f5ca33f
fastlio2/pointlio: code-review cleanup
jeff-hykin Jun 18, 2026
e2ae0b8
fastlio2: honor filter_size + publish flags; drop virtual_now()
jeff-hykin Jun 18, 2026
ffd8628
fastlio2: remove scan_bodyframe_pub_en (always publish sensor/body fr…
jeff-hykin Jun 18, 2026
e5621fb
fastlio2 pcap_to_db: open the .rrd in rerun + get_data fallback for -…
jeff-hykin Jun 18, 2026
9f13b16
blueprints: use default FastLio2 config in nav; stash FlowBase mid360…
jeff-hykin Jun 18, 2026
244954f
fastlio2 pcap_to_db: FastLio2 tuning as direct CLI flags
jeff-hykin Jun 18, 2026
6bcaaa3
fastlio2: no YAML — pass FAST-LIO params as plain CLI args
jeff-hykin Jun 18, 2026
5e88c54
fastlio2 main.cpp: python-style formatting (de-align = and comments)
jeff-hykin Jun 18, 2026
d35224a
fastlio2 main.cpp: inline run_main_iter into the main loop
jeff-hykin Jun 18, 2026
404d604
fastlio2 main.cpp: run_main_iter as a free function (time as arg, not…
jeff-hykin Jun 18, 2026
fa1ee7b
fastlio2/pointlio recorders: record at msg.ts; drop ts-alignment mach…
jeff-hykin Jun 18, 2026
eb7891e
fastlio2: delete cpp/README.md; fix stale 'rendered to YAML' comment
jeff-hykin Jun 18, 2026
a2aa3ce
fastlio2: reword acc_cov comment (no jhist/go2 references)
jeff-hykin Jun 18, 2026
e30afd9
fastlio2/pointlio recorders: drop the force flag; always replace own …
jeff-hykin Jun 18, 2026
2eb1ac4
fastlio2/pointlio recorders: rename via ports + .remappings, drop _st…
jeff-hykin Jun 18, 2026
9183d7e
memory2: fold TfRecorder into base Recorder (all Recorders record tf)
jeff-hykin Jun 18, 2026
0ab7ab1
pointlio: drop YAML config, pass tuning as plain CLI args
jeff-hykin Jun 18, 2026
fba90db
memory2: add Recorder.stream_remapping config for per-stream renames
jeff-hykin Jun 18, 2026
43d25f5
fastlio/pointlio: consistent frame_id scheme, stamp cloud with sensor…
jeff-hykin Jun 18, 2026
33c7041
fastlio/pointlio: default sensor_frame_id to mid360_link
jeff-hykin Jun 18, 2026
c76339d
memory2/recorders: fix APPEND re-run duplicating tf + ignoring stream…
jeff-hykin Jun 18, 2026
1e3e9b1
fastlio main.cpp: match pointlio formatting; inline get_publish_ts + …
jeff-hykin Jun 18, 2026
c5a6ab9
memory2: fix mypy no-any-return in Recorder._resolve_pose
jeff-hykin Jun 18, 2026
0260030
pointlio: revert module to origin/main (keep PR #2498 fastlio-only)
jeff-hykin Jun 18, 2026
2678ba7
Merge commit 'c5a6ab944' into jeff/feat/pointlio_native
jeff-hykin Jun 18, 2026
44efe28
Merge branch 'jeff/feat/fastlio_record2' into jeff/feat/pointlio_native
jeff-hykin Jun 18, 2026
253cf7a
Revert "pointlio: revert module to origin/main (keep PR #2498 fastlio…
jeff-hykin Jun 18, 2026
493a9b8
Merge remote-tracking branch 'origin/main' into HEAD
jeff-hykin Jun 22, 2026
be522a1
Merge remote-tracking branch 'origin/main' into jeff/feat/pointlio_na…
jeff-hykin Jun 23, 2026
708f3bd
pointlio(pcap_to_db): mirror fastlio get_data fallback for --db/--pcap
jeff-hykin Jun 23, 2026
dba1e5a
recorder: async callbacks + async pose_setter_for
jeff-hykin Jun 24, 2026
af6a06b
recorder: require @pose_setter_for setters to be async
jeff-hykin Jun 24, 2026
2ddfe52
Merge origin/jeff/fix/pose_setter_for into jeff/feat/pointlio_native
jeff-hykin Jun 24, 2026
e005ee3
Merge branch 'main' into jeff/feat/pointlio_native
jeff-hykin Jun 24, 2026
2068d4f
Merge branch 'main' of github.com:dimensionalOS/dimos into jeff/feat/…
jeff-hykin Jun 26, 2026
aa6aed4
Merge branch 'main' into jeff/feat/pointlio_native
jeff-hykin Jun 26, 2026
71a2e04
docs(pointlio): add README for pcap_to_db replay script
jeff-hykin Jun 26, 2026
27122d7
macos unpushed edgecase
jeff-hykin Jun 26, 2026
c0aab03
docs(pointlio): expand pcap_to_db README with record/replay + example…
jeff-hykin Jun 26, 2026
6ee83be
-
jeff-hykin Jun 26, 2026
b670f4a
-
jeff-hykin Jun 26, 2026
d94e7c4
Merge remote-tracking branch 'origin/main' into jeff/feat/pointlio_na…
jeff-hykin Jun 30, 2026
5b43dbf
feat(pointlio): scale replay walltime to --rate + detect input overload
jeff-hykin Jun 30, 2026
1db67ce
feat(pointlio): reword overload warning to be action-oriented
jeff-hykin Jun 30, 2026
ff9ae75
feat(pointlio): gate frame/odom emission on data time for rate-indepe…
jeff-hykin Jul 2, 2026
f6f34e6
feat(pointlio): record a full tf tree in pcap_to_db replays
jeff-hykin Jul 2, 2026
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
133 changes: 110 additions & 23 deletions dimos/hardware/sensors/lidar/pointlio/cpp/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,26 @@ static std::atomic<bool> g_running{true};
static lcm::LCM* g_lcm = nullptr;
static PointLio* g_point_lio = nullptr;

// Sensor→system time anchor, locked once at the first published frame:
// g_ts_offset = system_now − first_frame_sensor_time. Every output is then
// stamped (frame_sensor_time + g_ts_offset), so timestamps follow the data's
// real cadence no matter how fast packets are fed. On live hardware sensor time
// already advances in real time, so the offset is effectively constant and this
// is indistinguishable from wall-clock — Point-LIO and downstream consumers
// behave identically online and offline.
static std::atomic<uint64_t> g_latest_pkt_ns{0}; // newest point-packet sensor ts (also the main loop's data clock)
static uint64_t g_publish_pkt_ns = 0; // sensor ts of the latest drained frame (main thread)
static double g_ts_offset = 0.0;
static bool g_ts_offset_set = false;

static double get_publish_ts() {
return std::chrono::duration<double>( std::chrono::system_clock::now().time_since_epoch()).count();
if (!g_ts_offset_set) {
const double system_now = std::chrono::duration<double>(
std::chrono::system_clock::now().time_since_epoch()).count();
g_ts_offset = system_now - static_cast<double>(g_publish_pkt_ns) / 1e9;
g_ts_offset_set = true;
}
return static_cast<double>(g_publish_pkt_ns) / 1e9 + g_ts_offset;
}

// Parse a comma-separated list of doubles (CLI vector args); empty on bad input.
Expand Down Expand Up @@ -203,6 +221,9 @@ static void on_point_cloud(const uint32_t /*handle*/, const uint8_t /*dev_type*/
g_frame_start_ns = ts_ns;
g_frame_has_timestamp = true;
}
if (ts_ns > g_latest_pkt_ns.load(std::memory_order_relaxed)) {
g_latest_pkt_ns.store(ts_ns, std::memory_order_relaxed);
}

if (data->data_type == DATA_TYPE_CARTESIAN_HIGH) {
auto* pts = reinterpret_cast<const LivoxLidarCartesianHighRawPoint*>(data->data);
Expand Down Expand Up @@ -429,10 +450,15 @@ int main(int argc, char** argv) {
g_point_lio = &point_lio;
if (debug) { printf("[pointlio] Point-LIO initialized.\n"); }

// Main-loop state. Body lives in `run_main_iter`, driven by the wall-paced
// main thread.
// Main-loop state. Body lives in `run_main_iter`. The emission bookmarks
// (last_emit/last_pc_publish/last_odom_publish) live on a DATA-time grid — they
// advance by how far the packet stream's sensor time has progressed, not by the
// wall clock — so a frame always spans one frame_interval of data regardless of
// replay speed. last_wall_emit is the one wall-time bookmark, used only by the
// overload check (which is inherently about real elapsed time).
auto frame_interval = std::chrono::microseconds( static_cast<int64_t>(1e6 / g_frequency));
std::optional<std::chrono::steady_clock::time_point> last_emit;
std::optional<std::chrono::steady_clock::time_point> last_wall_emit;
const double process_period_ms = 1000.0 / main_freq;

auto pc_interval = std::chrono::microseconds( static_cast<int64_t>(1e6 / pointcloud_freq));
Expand All @@ -441,35 +467,89 @@ int main(int argc, char** argv) {
std::optional<std::chrono::steady_clock::time_point> last_odom_publish;


auto run_main_iter = [&](std::chrono::steady_clock::time_point now) {
// Lazy-seed rate-limit bookmarks on the first iteration so they align
// with the wall clock.
if (!last_emit.has_value()) {
last_emit = now;
}
if (!last_pc_publish.has_value()) {
last_pc_publish = now;
}
if (!last_odom_publish.has_value()) {
last_odom_publish = now;
// data_now is a clock built from the newest packet's sensor time (see the call
// site). All emission gating advances on it; wall_now is read here and used only
// for the overload measurement below.
auto run_main_iter = [&](std::chrono::steady_clock::time_point data_now) {
const auto wall_now = std::chrono::steady_clock::now();
const bool have_data_clock = data_now.time_since_epoch().count() != 0;

// Seed the data-time bookmarks from the first packet — never from 0, or the
// grid would try to "replay" the entire sensor epoch to catch up.
if (have_data_clock && !last_emit.has_value()) {
last_emit = data_now;
last_wall_emit = wall_now;
last_pc_publish = data_now;
last_odom_publish = data_now;
}

// At frame rate: drain accumulated points into a CustomMsg and feed
// Point-LIO. Hold g_pc_mutex across the rate-limit check AND swap so the
// clock + accumulator are observed atomically (no packet slips between).
std::vector<custom_messages::CustomPoint> points;
uint64_t frame_start = 0;
{
uint64_t backlog_ns = 0;
double wall_elapsed_s = 0.0;
bool measured_backlog = false;
if (have_data_clock) {
std::lock_guard<std::mutex> lock(g_pc_mutex);
if (now - *last_emit >= frame_interval) {
if (data_now - *last_emit >= frame_interval) {
if (!g_accumulated_points.empty()) {
points.swap(g_accumulated_points);
frame_start = g_frame_start_ns;
// Sensor-time advanced since the previous drain (= span of data
// this single frame swallows). Skip the first drain, where
// g_publish_pkt_ns is still 0.
const uint64_t latest = g_latest_pkt_ns.load(std::memory_order_relaxed);
if (g_publish_pkt_ns != 0) {
backlog_ns = latest - g_publish_pkt_ns;
wall_elapsed_s = std::chrono::duration<double>(wall_now - *last_wall_emit).count();
measured_backlog = true;
}
g_publish_pkt_ns = latest;
g_frame_has_timestamp = false;
}
last_emit = now;
// Advance on a fixed data-time grid; snap forward only if the data
// clock leapt several frames (a stall) so we don't spin catching up.
*last_emit += frame_interval;
if (data_now - *last_emit >= frame_interval) { last_emit = data_now; }
last_wall_emit = wall_now;
}
}

// Overload guard. backlog_ns is how much sensor-time this frame had to
// ingest; wall_elapsed_s is the real time it covered. Their ratio is how
// much faster than real-time packets are arriving. Live hardware sits at
// ~1; a fast replay (or a flooded link) pushes it up, and once the feed
// outpaces the fixed-rate drain the kernel UDP buffer overflows and drops
// packets — gappy LiDAR/IMU input is exactly what makes Point-LIO diverge.
// Surface it (throttled) so the overload is observable instead of silent.
if (measured_backlog && wall_elapsed_s > 0.0) {
const double overload_warn_ratio = 1.5;
const int warn_throttle_sec = 10;
const double realtime_ratio = (static_cast<double>(backlog_ns) / 1e9) / wall_elapsed_s;

// Overload happens every 10 Hz frame while a fast feed lasts, so warning
// per-frame is pure noise. Instead track the worst frame in each throttle
// window and emit one line for it — collapsing hundreds of lines to a few
// while still surfacing the spikes (stall-induced pile-ups) that matter
// most. Epoch-init last_warn (not ::min(), which overflows wall_now - last_warn)
// so the first overload reports immediately.
static std::chrono::steady_clock::time_point last_warn{};
static double window_peak_ratio = 0.0;
if (realtime_ratio > window_peak_ratio) { window_peak_ratio = realtime_ratio; }
if (window_peak_ratio > overload_warn_ratio &&
wall_now - last_warn >= std::chrono::seconds(warn_throttle_sec)) {
fprintf(stderr,
"[pointlio] WARNING: high risk of odom drift - pointlio can't "
"process fast enough. Try reducing CPU load. (input up to %.1fx "
"real-time over last %ds)\n",
window_peak_ratio, warn_throttle_sec);
last_warn = wall_now;
window_peak_ratio = 0.0;
}
}

// Serialize EKF access against the SDK IMU callback (on_imu_data) for the
// rest of the iteration — feed_lidar/process/get_* all touch the estimator.
std::lock_guard<std::mutex> lio_lock(g_lio_mutex);
Expand All @@ -494,28 +574,30 @@ int main(int argc, char** argv) {
point_lio.process();

auto pose = point_lio.get_pose();
if (!pose.empty() && (pose[0] != 0.0 || pose[1] != 0.0 || pose[2] != 0.0)) {
if (have_data_clock && !pose.empty() && (pose[0] != 0.0 || pose[1] != 0.0 || pose[2] != 0.0)) {
double ts = get_publish_ts();

const bool lidar_due = !g_lidar_topic.empty() && now - *last_pc_publish >= pc_interval;
const bool lidar_due = !g_lidar_topic.empty() && data_now - *last_pc_publish >= pc_interval;

// get_body_cloud is the loop's costliest step, so build it only when
// a publish is due.
if (lidar_due) {
auto body_cloud = point_lio.get_body_cloud();
if (body_cloud && !body_cloud->empty()) {
publish_lidar(body_cloud, ts);
last_pc_publish = now;
*last_pc_publish += pc_interval;
if (data_now - *last_pc_publish >= pc_interval) { last_pc_publish = data_now; }
if (pointlio_debug) {
fprintf(stderr, "[pointlio] publish lidar: %zu points pose=(%.3f, %.3f, %.3f)\n", body_cloud->size(), pose[0], pose[1], pose[2]);
}
}
}

// Pose + covariance at odom_freq.
if (!g_odometry_topic.empty() && now - *last_odom_publish >= odom_interval) {
if (!g_odometry_topic.empty() && data_now - *last_odom_publish >= odom_interval) {
publish_odometry(point_lio.get_odometry(), ts);
last_odom_publish = now;
*last_odom_publish += odom_interval;
if (data_now - *last_odom_publish >= odom_interval) { last_odom_publish = data_now; }
if (pointlio_debug) {
fprintf(stderr, "[pointlio] publish odom: pose=(%.3f, %.3f, %.3f)\n", pose[0], pose[1], pose[2]);
}
Expand All @@ -540,7 +622,12 @@ int main(int argc, char** argv) {

while (g_running.load()) {
auto loop_start = std::chrono::high_resolution_clock::now();
run_main_iter(std::chrono::steady_clock::now());
// Data clock: the newest packet's sensor time. Advances in real time on live
// hardware, at --rate on replay, and freezes when the stream drains.
const uint64_t data_ns = g_latest_pkt_ns.load(std::memory_order_relaxed);
run_main_iter(std::chrono::steady_clock::time_point(
std::chrono::duration_cast<std::chrono::steady_clock::duration>(
std::chrono::nanoseconds(data_ns))));

lcm.handleTimeout(0);

Expand Down
95 changes: 95 additions & 0 deletions dimos/hardware/sensors/lidar/pointlio/replay_static_tf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Copyright 2026 Dimensional Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Static tf edges a Go2 + Mid-360 pcap replay needs on top of PointLio's odometry.

PointLio already rebroadcasts its odometry as the moving ``odom -> body`` tf edge,
so a replay only lacks the fixed edges: the ``map -> odom`` root (identity until a
downstream PGO overwrites it with the loop-closure correction), the ``body ->
base_link`` bridge (Point-LIO's body frame and the rig's base_link are the same
physical frame), and the Go2/Mid-360 sensor mounts. Publishing all of these lets a
recorder capture a fully connected ``map -> odom -> body(=base_link) -> front_camera
-> {mid360_link, camera_optical}`` tree.

The stock ``StaticTfPublisher`` stamps each cycle with wall-clock ``time.time()``.
Under a rate-scaled replay that clock diverges from PointLio's data-time odometry
stamps, so the static edges would only cover the wall duration of the run and leave
the tree disconnected across the rest of the data-time trajectory. Instead this
module re-stamps the fixed edges off the odometry stream, so they track the exact
data-time span PointLio produces (throttled so consecutive edges stay well inside a
tf buffer's match tolerance).
"""

from __future__ import annotations

from pydantic import Field

from dimos.core.module import Module, ModuleConfig
from dimos.core.stream import In
from dimos.msgs.geometry_msgs.Quaternion import Quaternion
from dimos.msgs.geometry_msgs.Transform import Transform
from dimos.msgs.geometry_msgs.Vector3 import Vector3
from dimos.msgs.nav_msgs.Odometry import Odometry
from dimos.navigation.cmu_nav.frames import FRAME_BODY, FRAME_MAP, FRAME_ODOM
from dimos.protocol.tf.static_tf_publisher import frames_to_edge_transforms
from dimos.robot.unitree.go2.go2_mid360_static_transforms import FRAMES

# The rig's base_link and Point-LIO's body frame are the same physical frame.
FRAME_BASE_LINK = "base_link"


def _identity_edge(parent: str, child: str) -> Transform:
return Transform(
translation=Vector3(0.0, 0.0, 0.0),
rotation=Quaternion(0.0, 0.0, 0.0, 1.0),
frame_id=parent,
child_frame_id=child,
)


def _replay_static_edges() -> list[Transform]:
return [
*frames_to_edge_transforms(FRAMES),
_identity_edge(FRAME_MAP, FRAME_ODOM),
_identity_edge(FRAME_BODY, FRAME_BASE_LINK),
]


class Go2Mid360ReplayStaticTfConfig(ModuleConfig):
# Min data-time gap between republishing the static edges; keep well under a tf
# buffer's match tolerance so any query lands next to a stamped edge.
republish_min_interval_s: float = Field(default=0.2, gt=0.0)


class Go2Mid360ReplayStaticTf(Module):

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

temporary, while waiting on static tf PR merge

"""Republishes the Go2/Mid-360 mount tree plus the two identity edges PointLio's
odom tf omits, stamped on the odometry data clock."""

config: Go2Mid360ReplayStaticTfConfig

odometry: In[Odometry]

_edges: list[Transform] = []
_last_emit_ts: float | None = None

async def handle_odometry(self, value: Odometry) -> None:
if not self._edges:
self._edges = _replay_static_edges()
if self._last_emit_ts is not None:
if value.ts - self._last_emit_ts < self.config.republish_min_interval_s:
return
self._last_emit_ts = value.ts
for edge in self._edges:
edge.ts = value.ts
self.tf.publish(*self._edges)
Loading
Loading