minot-cli 0.5.0__tar.gz → 0.5.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. {minot_cli-0.5.0 → minot_cli-0.5.2}/Cargo.lock +15 -8
  2. {minot_cli-0.5.0 → minot_cli-0.5.2}/Cargo.toml +14 -15
  3. {minot_cli-0.5.0 → minot_cli-0.5.2}/PKG-INFO +1 -1
  4. {minot_cli-0.5.0 → minot_cli-0.5.2}/minot/Cargo.toml +29 -5
  5. {minot_cli-0.5.0 → minot_cli-0.5.2}/minot/src/app.rs +1 -1
  6. {minot_cli-0.5.0 → minot_cli-0.5.2}/minot/src/coord.rs +2 -1
  7. {minot_cli-0.5.0 → minot_cli-0.5.2}/minot/src/minot.rs +46 -5
  8. {minot_cli-0.5.0 → minot_cli-0.5.2}/minot/src/runner.rs +39 -7
  9. {minot_cli-0.5.0 → minot_cli-0.5.2}/mt_sea/Cargo.toml +1 -1
  10. {minot_cli-0.5.0 → minot_cli-0.5.2}/mt_sea/src/client.rs +320 -71
  11. {minot_cli-0.5.0 → minot_cli-0.5.2}/mt_wind/Cargo.toml +2 -2
  12. {minot_cli-0.5.0 → minot_cli-0.5.2}/ratpub/Cargo.toml +2 -2
  13. {minot_cli-0.5.0 → minot_cli-0.5.2}/minot/README.md +0 -0
  14. {minot_cli-0.5.0 → minot_cli-0.5.2}/minot/build.rs +0 -0
  15. {minot_cli-0.5.0 → minot_cli-0.5.2}/minot/src/event.rs +0 -0
  16. {minot_cli-0.5.0 → minot_cli-0.5.2}/minot/src/handler.rs +0 -0
  17. {minot_cli-0.5.0 → minot_cli-0.5.2}/minot/src/tui.rs +0 -0
  18. {minot_cli-0.5.0 → minot_cli-0.5.2}/minot/src/ui.rs +0 -0
  19. {minot_cli-0.5.0 → minot_cli-0.5.2}/mt_bagread/Cargo.toml +0 -0
  20. {minot_cli-0.5.0 → minot_cli-0.5.2}/mt_bagread/README.md +0 -0
  21. {minot_cli-0.5.0 → minot_cli-0.5.2}/mt_bagread/src/lib.rs +0 -0
  22. {minot_cli-0.5.0 → minot_cli-0.5.2}/mt_bagread/src/qos.rs +0 -0
  23. {minot_cli-0.5.0 → minot_cli-0.5.2}/mt_bagread/test/metadata-v5.yml +0 -0
  24. {minot_cli-0.5.0 → minot_cli-0.5.2}/mt_bagread/test/metadata-v9.yml +0 -0
  25. {minot_cli-0.5.0 → minot_cli-0.5.2}/mt_mtc/Cargo.toml +0 -0
  26. {minot_cli-0.5.0 → minot_cli-0.5.2}/mt_mtc/README.md +0 -0
  27. {minot_cli-0.5.0 → minot_cli-0.5.2}/mt_mtc/src/lib.rs +0 -0
  28. {minot_cli-0.5.0 → minot_cli-0.5.2}/mt_net/Cargo.toml +0 -0
  29. {minot_cli-0.5.0 → minot_cli-0.5.2}/mt_net/README.md +0 -0
  30. {minot_cli-0.5.0 → minot_cli-0.5.2}/mt_net/src/lib.rs +0 -0
  31. {minot_cli-0.5.0 → minot_cli-0.5.2}/mt_sea/README.md +0 -0
  32. {minot_cli-0.5.0 → minot_cli-0.5.2}/mt_sea/src/coordinator.rs +0 -0
  33. {minot_cli-0.5.0 → minot_cli-0.5.2}/mt_sea/src/lib.rs +0 -0
  34. {minot_cli-0.5.0 → minot_cli-0.5.2}/mt_sea/src/net.rs +0 -0
  35. {minot_cli-0.5.0 → minot_cli-0.5.2}/mt_sea/src/ship.rs +0 -0
  36. {minot_cli-0.5.0 → minot_cli-0.5.2}/mt_wind/README.md +0 -0
  37. {minot_cli-0.5.0 → minot_cli-0.5.2}/mt_wind/src/lib.rs +0 -0
  38. {minot_cli-0.5.0 → minot_cli-0.5.2}/mt_wind/src/ratpub.rs +0 -0
  39. {minot_cli-0.5.0 → minot_cli-0.5.2}/mt_wind/src/ros1.rs +0 -0
  40. {minot_cli-0.5.0 → minot_cli-0.5.2}/mt_wind/src/ros2.rs +0 -0
  41. {minot_cli-0.5.0 → minot_cli-0.5.2}/mt_wind/src/ros2_r2r.rs +0 -0
  42. {minot_cli-0.5.0 → minot_cli-0.5.2}/pyproject.toml +0 -0
  43. {minot_cli-0.5.0 → minot_cli-0.5.2}/ratpub/README.md +0 -0
  44. {minot_cli-0.5.0 → minot_cli-0.5.2}/ratpub/examples/pub.rs +0 -0
  45. {minot_cli-0.5.0 → minot_cli-0.5.2}/ratpub/examples/smol.rs +0 -0
  46. {minot_cli-0.5.0 → minot_cli-0.5.2}/ratpub/examples/sub.rs +0 -0
  47. {minot_cli-0.5.0 → minot_cli-0.5.2}/ratpub/src/lib.rs +0 -0
@@ -527,6 +527,12 @@ dependencies = [
527
527
  "serde",
528
528
  ]
529
529
 
530
+ [[package]]
531
+ name = "bytesize"
532
+ version = "2.3.1"
533
+ source = "registry+https://github.com/rust-lang/crates.io-index"
534
+ checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3"
535
+
530
536
  [[package]]
531
537
  name = "camino"
532
538
  version = "1.2.2"
@@ -2487,9 +2493,10 @@ dependencies = [
2487
2493
 
2488
2494
  [[package]]
2489
2495
  name = "minot"
2490
- version = "0.5.0"
2496
+ version = "0.5.2"
2491
2497
  dependencies = [
2492
2498
  "anyhow",
2499
+ "bytesize",
2493
2500
  "chrono",
2494
2501
  "clap",
2495
2502
  "clap_complete",
@@ -2583,7 +2590,7 @@ dependencies = [
2583
2590
 
2584
2591
  [[package]]
2585
2592
  name = "mt_bagread"
2586
- version = "0.5.0"
2593
+ version = "0.5.2"
2587
2594
  dependencies = [
2588
2595
  "anyhow",
2589
2596
  "camino",
@@ -2603,7 +2610,7 @@ dependencies = [
2603
2610
 
2604
2611
  [[package]]
2605
2612
  name = "mt_mtc"
2606
- version = "0.5.0"
2613
+ version = "0.5.2"
2607
2614
  dependencies = [
2608
2615
  "anyhow",
2609
2616
  "ariadne",
@@ -2616,7 +2623,7 @@ dependencies = [
2616
2623
 
2617
2624
  [[package]]
2618
2625
  name = "mt_net"
2619
- version = "0.5.0"
2626
+ version = "0.5.2"
2620
2627
  dependencies = [
2621
2628
  "anyhow",
2622
2629
  "rkyv 0.8.15",
@@ -2626,7 +2633,7 @@ dependencies = [
2626
2633
 
2627
2634
  [[package]]
2628
2635
  name = "mt_rat"
2629
- version = "0.5.0"
2636
+ version = "0.5.2"
2630
2637
  dependencies = [
2631
2638
  "anyhow",
2632
2639
  "async-trait",
@@ -2641,7 +2648,7 @@ dependencies = [
2641
2648
 
2642
2649
  [[package]]
2643
2650
  name = "mt_sea"
2644
- version = "0.5.0"
2651
+ version = "0.5.2"
2645
2652
  dependencies = [
2646
2653
  "anyhow",
2647
2654
  "async-trait",
@@ -2657,7 +2664,7 @@ dependencies = [
2657
2664
 
2658
2665
  [[package]]
2659
2666
  name = "mt_wind"
2660
- version = "0.5.0"
2667
+ version = "0.5.2"
2661
2668
  dependencies = [
2662
2669
  "anyhow",
2663
2670
  "async-trait",
@@ -3781,7 +3788,7 @@ dependencies = [
3781
3788
 
3782
3789
  [[package]]
3783
3790
  name = "ratpub"
3784
- version = "0.5.0"
3791
+ version = "0.5.2"
3785
3792
  dependencies = [
3786
3793
  "anyhow",
3787
3794
  "async-trait",
@@ -4,15 +4,13 @@ members = ["minot", "ratpub", "mt_bagread", "mt_mtc", "mt_sea", "mt_wind", "mt_n
4
4
  resolver = "3"
5
5
 
6
6
  [workspace.package]
7
- version = "0.5.0"
7
+ version = "0.5.2"
8
8
  keywords = ["robotics"]
9
9
  license = "MIT OR Apache-2.0"
10
10
  repository = "https://codeberg.org/stelzo/minot"
11
11
  edition = "2024"
12
12
  authors = ["Christopher Sieh <stelzo@steado.de>"]
13
- categories = [
14
- "science::robotics",
15
- ]
13
+ categories = ["science::robotics"]
16
14
  documentation = "https://uos.github.io/minot"
17
15
  homepage = "https://uos.github.io/minot"
18
16
  rust-version = "1.85"
@@ -30,12 +28,12 @@ exclude = [
30
28
  ]
31
29
 
32
30
  [workspace.dependencies]
33
- mt_sea = { path = "mt_sea", version = "0.5.0", default-features = false }
34
- ratpub = { path = "ratpub", version = "0.5.0" }
35
- mt_bagread = { path = "mt_bagread", version = "0.5.0" }
36
- mt_net = { path = "mt_net", version = "0.5.0" }
37
- mt_wind = { path = "mt_wind", version = "0.5.0" }
38
- mt_mtc = { path = "mt_mtc", version = "0.5.0" }
31
+ mt_sea = { path = "mt_sea", version = "0.5.2" }
32
+ ratpub = { path = "ratpub", version = "0.5.2" }
33
+ mt_bagread = { path = "mt_bagread", version = "0.5.2" }
34
+ mt_net = { path = "mt_net", version = "0.5.2" }
35
+ mt_wind = { path = "mt_wind", version = "0.5.2" }
36
+ mt_mtc = { path = "mt_mtc", version = "0.5.2" }
39
37
 
40
38
  # Tools
41
39
  anyhow = "1.0"
@@ -51,7 +49,7 @@ futures = "0.3.31"
51
49
  ratatui = "0.29.0"
52
50
  rosbag2-rs = "0.2.1"
53
51
  chrono = "0.4.40"
54
- tui-logger = "0.16.0" # out of date but needed for 1.85 compatibility
52
+ tui-logger = "0.16.0"
55
53
  regex = "1.12"
56
54
  once_cell = "1.21"
57
55
  rust_decimal = "1.37.1"
@@ -64,6 +62,7 @@ serde_yml = "0.0.12"
64
62
  cdr = "0.2.4"
65
63
  rand = "0.8"
66
64
  cdr-encoding = "0.10"
65
+ bytesize = "2.3"
67
66
 
68
67
  # IO
69
68
  futures-lite = "2.6"
@@ -97,10 +96,10 @@ ros2-interfaces-jazzy-rkyv = { version = "0.0.5", features = [
97
96
  roslibrust = { version = "0.18", features = ["ros1", "macro"] }
98
97
 
99
98
  [profile.release]
100
- lto = "fat" # Link Time Optimization
101
- codegen-units = 1 # Slower compile times but compiler sees all generated code and can optimize over everything across modules. Also decreases binary size.
102
- strip = "symbols" # Significantly reduce binary size
103
- opt-level = "s" # Optimize for size but keep some performance
99
+ lto = "fat" # Link Time Optimization
100
+ codegen-units = 1 # Slower compile times but compiler sees all generated code and can optimize over everything across modules. Also decreases binary size.
101
+ strip = "symbols" # Significantly reduce binary size
102
+ opt-level = "s" # Optimize for size but keep some performance
104
103
  debug = 0
105
104
 
106
105
  [profile.colcon]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: minot-cli
3
- Version: 0.5.0
3
+ Version: 0.5.2
4
4
  Classifier: Intended Audience :: Developers
5
5
  Classifier: Intended Audience :: Science/Research
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -38,6 +38,7 @@ clap_complete.workspace = true
38
38
  regex.workspace = true
39
39
  once_cell.workspace = true
40
40
  rust_decimal.workspace = true
41
+ bytesize.workspace = true
41
42
 
42
43
  [[bin]]
43
44
  name = "minot"
@@ -47,12 +48,11 @@ path = "src/minot.rs"
47
48
  name = "minot-coord"
48
49
  path = "src/coord.rs"
49
50
 
50
- # Note: All following features only embed existing nodes that can be built and run separately through the network.
51
51
  [features]
52
- default = ["embed-coord", "shared-memory"]
52
+ default = ["embed-coord"]
53
53
 
54
- # Enable shared memory for zero-copy transfer of large messages (same-machine only)
55
- shared-memory = ["mt_sea/shm", "mt_wind?/shared-memory"]
54
+ # Experimental: Enable shared memory for zero-copy transfer of large messages (same-machine only)
55
+ shm = ["mt_sea/shm", "mt_wind?/shm"]
56
56
 
57
57
  # Use the official C API for ROS2 publishing (r2r crate). Enable for maximum compatibility at the cost of needing an actively sourced ROS2 installation.
58
58
  embed-ros2-c = ["embed-ros2-c-turbine"]
@@ -77,6 +77,30 @@ embed-ros1-turbine = ["dep:mt_wind", "mt_wind/ros1-native"]
77
77
  embed-ratpub-turbine = ["dep:mt_wind", "mt_wind/ratpub"]
78
78
 
79
79
  [package.metadata.docs.rs]
80
- features = ["embed-coord", "embed-ratpub", "embed-ros1-native", "embed-ros2-c", "embed-ros2-c-humble", "embed-ros2-native"]
80
+ features = [
81
+ "embed-coord",
82
+ "embed-ratpub",
83
+ "embed-ros1-native",
84
+ "embed-ros2-c",
85
+ "embed-ros2-c-humble",
86
+ "embed-ros2-native",
87
+ ]
81
88
  default-target = "x86_64-unknown-linux-gnu"
82
89
  rustdoc-args = ["--cfg", "docsrs"]
90
+
91
+ [package.metadata.deb]
92
+ name = "minot"
93
+ section = "science"
94
+ priority = "optional"
95
+ assets = [
96
+ [
97
+ "target/release/minot",
98
+ "usr/bin/minot",
99
+ "755",
100
+ ],
101
+ [
102
+ "target/release/minot-coord",
103
+ "usr/bin/minot-coord",
104
+ "755",
105
+ ],
106
+ ]
@@ -1892,7 +1892,7 @@ impl App {
1892
1892
  if wind_data.is_empty() {
1893
1893
  if bagmsgs.end_of_bag {
1894
1894
  // Truly no more data in bagfile - abort remaining actions
1895
- info!(
1895
+ warn!(
1896
1896
  "reached end of bagfile with no more matching messages"
1897
1897
  );
1898
1898
  return;
@@ -700,7 +700,8 @@ pub fn get_clients(eval: &Evaluated) -> anyhow::Result<HashSet<String>> {
700
700
  pub async fn main() -> anyhow::Result<()> {
701
701
  mt_sea::init_logging();
702
702
 
703
- let filepath = Args::parse().file;
703
+ let args = Args::parse();
704
+ let filepath = args.file;
704
705
 
705
706
  let eval = if let Some(fp) = filepath {
706
707
  let rules_file = std::fs::canonicalize(&fp)?;
@@ -39,12 +39,32 @@ const EMBED_RATPUB_TURBINE_NAME: &'static str = "embedded_ratpub_turbine";
39
39
  #[derive(Debug, Clone, Subcommand)]
40
40
  pub enum ServeCommand {}
41
41
 
42
+ #[cfg(feature = "shm")]
43
+ use bytesize::ByteSize;
44
+
42
45
  #[derive(Parser, Debug)]
43
46
  #[command(version, about, author, long_about = None)]
44
47
  /// Minot — A versatile toolset for debugging and verifying stateful robot perception software.
45
48
  pub(crate) struct Args {
46
49
  #[command(subcommand)]
47
50
  pub command: Commands,
51
+
52
+ /// Disable POSIX shared memory for zero-copy transfer.
53
+ /// SHM is enabled by default for better performance with large messages.
54
+ #[cfg(feature = "shm")]
55
+ #[arg(long, global = true)]
56
+ pub no_shm: bool,
57
+
58
+ /// Size of the shared memory buffer pool (e.g., "7mb", "64MiB", "1gb").
59
+ /// Default is 7MB which is within typical Linux /dev/shm limits.
60
+ #[cfg(feature = "shm")]
61
+ #[arg(long, global = true, default_value = "7mb", value_parser = parse_bytesize)]
62
+ pub shm_size: ByteSize,
63
+ }
64
+
65
+ #[cfg(feature = "shm")]
66
+ fn parse_bytesize(s: &str) -> Result<ByteSize, String> {
67
+ s.parse::<ByteSize>().map_err(|e| e.to_string())
48
68
  }
49
69
 
50
70
  #[derive(Debug, Clone, Parser)]
@@ -115,8 +135,11 @@ async fn tui(path: PathBuf) -> Result<(), Box<dyn std::error::Error>> {
115
135
  Ok(log::LevelFilter::Info)
116
136
  }?;
117
137
  tui_logger::init_logger(log_level).unwrap();
118
- // tui_logger::set_env_filter_from_string(&log_level);
119
138
  tui_logger::set_default_level(log_level);
139
+ tui_logger::set_level_for_target("zenoh", log::LevelFilter::Warn);
140
+ tui_logger::set_level_for_target("zenoh_transport", log::LevelFilter::Warn);
141
+ tui_logger::set_level_for_target("zenoh_link", log::LevelFilter::Warn);
142
+ tui_logger::set_level_for_target("zenoh_protocol", log::LevelFilter::Warn);
120
143
 
121
144
  #[cfg(feature = "embed-coord")]
122
145
  {
@@ -671,9 +694,11 @@ struct StdioLogger {
671
694
 
672
695
  impl log::Log for StdioLogger {
673
696
  fn enabled(&self, metadata: &log::Metadata) -> bool {
674
- // We can filter levels here, but it's easier
675
- // to just set the global `log::set_max_level`.
676
- // This logger will respect that global level.
697
+ // Filter out verbose zenoh logs (only show warnings and errors)
698
+ let target = metadata.target();
699
+ if target.starts_with("zenoh") && metadata.level() > log::Level::Warn {
700
+ return false;
701
+ }
677
702
  metadata.level() <= log::max_level()
678
703
  }
679
704
 
@@ -704,7 +729,12 @@ fn init_stdio_logger(
704
729
  ) -> Result<(), log::SetLoggerError> {
705
730
  let logger = StdioLogger { sender };
706
731
  log::set_boxed_logger(Box::new(logger))?;
707
- log::set_max_level(level);
732
+ // Respect RUST_LOG if set, otherwise use the provided default
733
+ let effective_level = std::env::var("RUST_LOG")
734
+ .ok()
735
+ .and_then(|s| s.parse().ok())
736
+ .unwrap_or(level);
737
+ log::set_max_level(effective_level);
708
738
  Ok(())
709
739
  }
710
740
 
@@ -1346,6 +1376,17 @@ fn start_logging_thread(rx: std::sync::mpsc::Receiver<String>) {
1346
1376
  async fn main() -> Result<(), Box<dyn std::error::Error>> {
1347
1377
  let args = Args::parse();
1348
1378
 
1379
+ // Set SHM environment variables for mt_sea to read
1380
+ // SHM is enabled by default, --no-shm disables it
1381
+ // SAFETY: We're setting env vars at the start of main before spawning any threads
1382
+ #[cfg(feature = "shm")]
1383
+ unsafe {
1384
+ if args.no_shm {
1385
+ std::env::set_var("MINOT_SHM_DISABLED", "1");
1386
+ }
1387
+ std::env::set_var("MINOT_SHM_SIZE", args.shm_size.as_u64().to_string());
1388
+ }
1389
+
1349
1390
  match args.command {
1350
1391
  Commands::Tui(tui_args) => tui(tui_args.file).await,
1351
1392
  Commands::Serve => serve().await,
@@ -1,5 +1,5 @@
1
1
  use anyhow::anyhow;
2
- use log::{info, warn};
2
+ use log::{debug, error, info, warn};
3
3
  use serde::{Deserialize, Serialize};
4
4
  use std::path::PathBuf;
5
5
  use std::process::Stdio;
@@ -8,6 +8,36 @@ use tokio::process::Command;
8
8
 
9
9
  type BoxError = Box<dyn std::error::Error>;
10
10
 
11
+ /// Initialize a stderr logger for the runner process with a minimal format
12
+ /// that pelorus can parse: `[LEVEL] target: message`
13
+ fn init_runner_logger() {
14
+ env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info,zenoh=warn"))
15
+ .format(|buf, record| {
16
+ use std::io::Write;
17
+ writeln!(
18
+ buf,
19
+ "[{}] {}: {}",
20
+ record.level(),
21
+ record.target(),
22
+ record.args()
23
+ )
24
+ })
25
+ .target(env_logger::Target::Stderr)
26
+ .init();
27
+ }
28
+
29
+ /// Re-emit a log record received from serve's JSON protocol at the correct level.
30
+ fn forward_serve_log(level: &str, target: &str, message: &str) {
31
+ match level {
32
+ "ERROR" => error!(target: target, "{}", message),
33
+ "WARN" => warn!(target: target, "{}", message),
34
+ "INFO" => info!(target: target, "{}", message),
35
+ "DEBUG" => debug!(target: target, "{}", message),
36
+ "TRACE" => log::trace!(target: target, "{}", message),
37
+ _ => info!(target: target, "{}", message),
38
+ }
39
+ }
40
+
11
41
  #[derive(Serialize, Debug)]
12
42
  #[serde(tag = "type")]
13
43
  #[allow(dead_code)]
@@ -58,6 +88,8 @@ struct ServerResponse {
58
88
  }
59
89
 
60
90
  pub async fn run(file_path: PathBuf, minot_path: PathBuf, sync: bool) -> Result<(), BoxError> {
91
+ init_runner_logger();
92
+
61
93
  let file_content = std::fs::read_to_string(&file_path)
62
94
  .map_err(|e| format!("Failed to read file {:?}: {}", file_path, e))?;
63
95
 
@@ -172,17 +204,17 @@ pub async fn run(file_path: PathBuf, minot_path: PathBuf, sync: bool) -> Result<
172
204
  message,
173
205
  target,
174
206
  } => {
175
- println!("[{}] {}: {}", level, target, message);
207
+ forward_serve_log(&level, &target, &message);
176
208
  }
177
209
  ServerMessage::CommandResponse(resp) => {
178
- info!("Received command response: {:?}", resp);
210
+ debug!("Received command response: {:?}", resp);
179
211
  }
180
212
  ServerMessage::ServerReady => {
181
213
  warn!("Received unexpected ServerReady");
182
214
  }
183
215
  }
184
216
  } else {
185
- print!("{}", line);
217
+ eprint!("{}", line);
186
218
  }
187
219
  }
188
220
 
@@ -222,18 +254,18 @@ where
222
254
  message,
223
255
  target,
224
256
  } => {
225
- println!("[{}] {}: {}", level, target, message);
257
+ forward_serve_log(level, target, message);
226
258
  }
227
259
  _ => {
228
260
  if predicate(&msg) {
229
261
  return Ok(msg);
230
262
  } else {
231
- warn!("Ignored unexpected message while waiting: {:?}", msg);
263
+ debug!("Ignored unexpected message while waiting: {:?}", msg);
232
264
  }
233
265
  }
234
266
  }
235
267
  } else {
236
- print!("{}", line);
268
+ eprint!("{}", line);
237
269
  }
238
270
  }
239
271
  }
@@ -15,7 +15,7 @@ exclude.workspace = true
15
15
  readme = "README.md"
16
16
 
17
17
  [features]
18
- default = ["shm"]
18
+ default = []
19
19
  shm = ["zenoh/shared-memory"]
20
20
 
21
21
  [dependencies]
@@ -6,6 +6,8 @@ use mt_net::COMPARE_NODE_NAME;
6
6
  use rkyv::{api::high::from_bytes, to_bytes, util::AlignedVec};
7
7
  use zenoh::Wait;
8
8
 
9
+ #[cfg(feature = "shm")]
10
+ use std::sync::RwLock as StdRwLock;
9
11
  #[cfg(feature = "shm")]
10
12
  use zenoh::shm::{
11
13
  BlockOn, GarbageCollect, PosixShmProviderBackend, ShmProvider, ShmProviderBuilder,
@@ -20,13 +22,28 @@ pub type CoordSender = tokio::sync::broadcast::Sender<(Packet, Option<std::net::
20
22
  pub type RecvBuffer = HashMap<u32, Vec<(Vec<u8>, VariableType, String)>>;
21
23
 
22
24
  #[cfg(feature = "shm")]
23
- /// SHM buffer pool size (64 MB)
24
- const SHM_BUFFER_SIZE: usize = 64 * 1024 * 1024;
25
+ /// Within typical Linux /dev/shm limits
26
+ const DEFAULT_SHM_BUFFER_SIZE: usize = 7 * 1024 * 1024;
25
27
 
26
28
  #[cfg(feature = "shm")]
27
- /// Messages larger than this use SHM, smaller use network (8 KB threshold)
28
29
  const SHM_SIZE_THRESHOLD: usize = 8 * 1024;
29
30
 
31
+ #[cfg(feature = "shm")]
32
+ fn is_shm_enabled() -> bool {
33
+ !std::env::var("MINOT_SHM_DISABLED")
34
+ .map(|v| v == "1" || v.to_lowercase() == "true")
35
+ .unwrap_or(false)
36
+ }
37
+
38
+ /// Get SHM buffer size from environment variable, falling back to default
39
+ #[cfg(feature = "shm")]
40
+ fn get_shm_buffer_size() -> usize {
41
+ std::env::var("MINOT_SHM_SIZE")
42
+ .ok()
43
+ .and_then(|v| v.parse::<usize>().ok())
44
+ .unwrap_or(DEFAULT_SHM_BUFFER_SIZE)
45
+ }
46
+
30
47
  /// Copy bytes into an aligned buffer for rkyv deserialization
31
48
  fn align_bytes(bytes: &[u8]) -> AlignedVec {
32
49
  let mut aligned = AlignedVec::with_capacity(bytes.len());
@@ -34,6 +51,13 @@ fn align_bytes(bytes: &[u8]) -> AlignedVec {
34
51
  aligned
35
52
  }
36
53
 
54
+ /// SHM state with dynamic resizing capability
55
+ #[cfg(feature = "shm")]
56
+ struct ShmState {
57
+ provider: std::sync::Arc<ShmProvider<PosixShmProviderBackend>>,
58
+ capacity: usize,
59
+ }
60
+
37
61
  pub struct Client {
38
62
  pub coordinator_receive: std::sync::Arc<std::sync::RwLock<Option<CoordSender>>>,
39
63
  pub coordinator_send:
@@ -42,14 +66,14 @@ pub struct Client {
42
66
  rm_rules_on_disconnect: bool,
43
67
  pub updated_raw_recv: tokio::sync::broadcast::Sender<u32>,
44
68
  pub raw_recv_buff: std::sync::Arc<std::sync::RwLock<RecvBuffer>>,
45
- /// Dedicated channel for wind packets - uses MPSC to avoid race conditions.
46
- /// Wind packets are forwarded here from the coordinator receiver and buffered until consumed.
47
69
  pub wind_receiver: std::sync::Arc<tokio::sync::Mutex<tokio::sync::mpsc::Receiver<Packet>>>,
48
70
  wind_sender: tokio::sync::mpsc::Sender<Packet>,
49
71
  session: std::sync::Arc<zenoh::Session>,
50
72
  domain_id: u16,
51
73
  #[cfg(feature = "shm")]
52
- shm_provider: std::sync::Arc<ShmProvider<PosixShmProviderBackend>>,
74
+ shm_state: StdRwLock<Option<ShmState>>,
75
+ #[cfg(feature = "shm")]
76
+ shm_initialized: std::sync::atomic::AtomicBool,
53
77
  }
54
78
 
55
79
  impl std::fmt::Debug for Client {
@@ -62,7 +86,279 @@ impl std::fmt::Debug for Client {
62
86
  }
63
87
  }
64
88
 
89
+ #[cfg(feature = "shm")]
90
+ fn create_shm_provider(size: usize) -> Result<ShmState, String> {
91
+ match ShmProviderBuilder::default_backend(size).wait() {
92
+ Ok(provider) => Ok(ShmState {
93
+ provider: std::sync::Arc::new(provider),
94
+ capacity: size,
95
+ }),
96
+ Err(e) => Err(e.to_string()),
97
+ }
98
+ }
99
+
100
+ #[cfg(feature = "shm")]
101
+ fn format_shm_error(err_str: &str, requested_size: usize) -> String {
102
+ #[cfg(target_os = "linux")]
103
+ {
104
+ let shm_mb = requested_size / (1024 * 1024);
105
+ if err_str.contains("ENOMEM")
106
+ || err_str.contains("Cannot allocate")
107
+ || err_str.contains("No space")
108
+ {
109
+ format!(
110
+ "Insufficient shared memory. Requested {} bytes.\n\
111
+ Possible fixes:\n\
112
+ - Increase /dev/shm size: `sudo mount -o remount,size={}M /dev/shm`\n\
113
+ - Or set ulimit: `ulimit -l unlimited` (may require root)\n\
114
+ - Or add to /etc/fstab: `tmpfs /dev/shm tmpfs defaults,size={}M 0 0`\n\
115
+ - Or disable SHM: `--no-shm` or `MINOT_SHM_DISABLED=1`",
116
+ requested_size,
117
+ shm_mb.max(16),
118
+ shm_mb.max(16)
119
+ )
120
+ } else if err_str.contains("EACCES") || err_str.contains("Permission denied") {
121
+ "Permission denied.\n\
122
+ Possible fixes:\n\
123
+ - Check /dev/shm permissions: `ls -la /dev/shm`\n\
124
+ - Ensure user has write access to /dev/shm\n\
125
+ - Or disable SHM: `--no-shm` or `MINOT_SHM_DISABLED=1`"
126
+ .to_string()
127
+ } else {
128
+ format!(
129
+ "Error: {}\nTo disable SHM: `--no-shm` or `MINOT_SHM_DISABLED=1`",
130
+ err_str
131
+ )
132
+ }
133
+ }
134
+
135
+ #[cfg(target_os = "macos")]
136
+ {
137
+ if err_str.contains("ENOMEM")
138
+ || err_str.contains("Cannot allocate")
139
+ || err_str.contains("No space")
140
+ {
141
+ format!(
142
+ "Insufficient shared memory. Requested {} bytes.\n\
143
+ Possible fixes:\n\
144
+ - Increase shmmax: `sudo sysctl -w kern.sysv.shmmax={}`\n\
145
+ - Increase shmall: `sudo sysctl -w kern.sysv.shmall={}`\n\
146
+ - Or disable SHM: `--no-shm` or `MINOT_SHM_DISABLED=1`\n\
147
+ Note: macOS has stricter SHM limits than Linux.",
148
+ requested_size,
149
+ requested_size,
150
+ requested_size / 4096
151
+ )
152
+ } else if err_str.contains("EACCES") || err_str.contains("Permission denied") {
153
+ "Permission denied.\n\
154
+ Possible fixes:\n\
155
+ - Check System Preferences > Security & Privacy settings\n\
156
+ - Or disable SHM: `--no-shm` or `MINOT_SHM_DISABLED=1`"
157
+ .to_string()
158
+ } else {
159
+ format!(
160
+ "Error: {}\nTo disable SHM: `--no-shm` or `MINOT_SHM_DISABLED=1`",
161
+ err_str
162
+ )
163
+ }
164
+ }
165
+ }
166
+
65
167
  impl Client {
168
+ /// Get or create SHM provider, initializing on first call
169
+ #[cfg(feature = "shm")]
170
+ fn get_or_init_shm(&self) -> Option<std::sync::Arc<ShmProvider<PosixShmProviderBackend>>> {
171
+ if !is_shm_enabled() {
172
+ return None;
173
+ }
174
+
175
+ // Fast path: already initialized
176
+ if self
177
+ .shm_initialized
178
+ .load(std::sync::atomic::Ordering::Acquire)
179
+ {
180
+ return self
181
+ .shm_state
182
+ .read()
183
+ .ok()?
184
+ .as_ref()
185
+ .map(|s| s.provider.clone());
186
+ }
187
+
188
+ // Slow path: initialize
189
+ let mut state = self.shm_state.write().ok()?;
190
+ if state.is_none() {
191
+ let initial_size = get_shm_buffer_size();
192
+ match create_shm_provider(initial_size) {
193
+ Ok(shm_state) => {
194
+ info!(
195
+ "SHM enabled with initial buffer size: {} bytes",
196
+ initial_size
197
+ );
198
+ *state = Some(shm_state);
199
+ self.shm_initialized
200
+ .store(true, std::sync::atomic::Ordering::Release);
201
+ }
202
+ Err(e) => {
203
+ warn!(
204
+ "Failed to create SHM provider: {}\nFalling back to network transport.",
205
+ format_shm_error(&e, initial_size)
206
+ );
207
+ return None;
208
+ }
209
+ }
210
+ }
211
+ state.as_ref().map(|s| s.provider.clone())
212
+ }
213
+
214
+ /// Try to grow the SHM pool to accommodate a larger message
215
+ #[cfg(feature = "shm")]
216
+ fn try_grow_shm(
217
+ &self,
218
+ required_size: usize,
219
+ ) -> Option<std::sync::Arc<ShmProvider<PosixShmProviderBackend>>> {
220
+ let mut state = self.shm_state.write().ok()?;
221
+
222
+ let current_capacity = state.as_ref().map(|s| s.capacity).unwrap_or(0);
223
+
224
+ // Double the required size (like Vec growth strategy)
225
+ let new_capacity = (required_size * 2).max(current_capacity * 2);
226
+
227
+ debug!(
228
+ "Growing SHM pool from {} to {} bytes",
229
+ current_capacity, new_capacity
230
+ );
231
+
232
+ match create_shm_provider(new_capacity) {
233
+ Ok(new_state) => {
234
+ info!("SHM pool grown to {} bytes", new_capacity);
235
+ *state = Some(new_state);
236
+ state.as_ref().map(|s| s.provider.clone())
237
+ }
238
+ Err(e) => {
239
+ warn!(
240
+ "Cannot grow SHM pool to {} bytes: {}\nFalling back to network transport for large messages.",
241
+ new_capacity,
242
+ format_shm_error(&e, new_capacity)
243
+ );
244
+ None
245
+ }
246
+ }
247
+ }
248
+
249
+ /// Get current SHM capacity
250
+ #[cfg(feature = "shm")]
251
+ fn shm_capacity(&self) -> usize {
252
+ self.shm_state
253
+ .read()
254
+ .ok()
255
+ .and_then(|s| s.as_ref().map(|state| state.capacity))
256
+ .unwrap_or(0)
257
+ }
258
+
259
+ /// Try to send via SHM with automatic pool growth. Returns:
260
+ /// - Some(Ok(())) if sent successfully via SHM
261
+ /// - Some(Err(e)) if there was an error during sending
262
+ /// - None if SHM is unavailable and caller should fall back to network
263
+ #[cfg(feature = "shm")]
264
+ #[allow(clippy::too_many_arguments)]
265
+ async fn try_shm_send(
266
+ &self,
267
+ total_len: usize,
268
+ id_bytes: &[u8; 4],
269
+ variable_type: VariableType,
270
+ padded_name: &[u8; 64],
271
+ data: &[u8],
272
+ data_key: &str,
273
+ id: u32,
274
+ ) -> Option<anyhow::Result<()>> {
275
+ // Get or initialize SHM provider
276
+ let mut shm_provider = self.get_or_init_shm()?;
277
+
278
+ // Try allocation, growing pool if needed (up to 2 attempts)
279
+ for attempt in 0..2 {
280
+ let shm_result = shm_provider
281
+ .alloc(total_len)
282
+ .with_policy::<BlockOn<GarbageCollect>>()
283
+ .await;
284
+
285
+ match shm_result {
286
+ Ok(mut shm_buf) => {
287
+ // Copy data into SHM buffer
288
+ shm_buf[0..4].copy_from_slice(id_bytes);
289
+ shm_buf[4] = variable_type.into();
290
+ shm_buf[5..69].copy_from_slice(padded_name);
291
+ shm_buf[69..total_len].copy_from_slice(data);
292
+
293
+ let shm_immut: zenoh::shm::ZShm = shm_buf.into();
294
+ debug!("Sending {} bytes via SHM to {}", total_len, data_key);
295
+
296
+ // Send with retry loop
297
+ loop {
298
+ let replies =
299
+ match self.session.get(data_key).payload(shm_immut.clone()).wait() {
300
+ Ok(r) => r,
301
+ Err(e) => {
302
+ return Some(Err(anyhow!("Failed to send data query: {}", e)));
303
+ }
304
+ };
305
+
306
+ match replies.recv_async().await {
307
+ Ok(reply) => match reply.result() {
308
+ Ok(_sample) => {
309
+ debug!(
310
+ "Sent data id {} to {} via SHM (ACK received)",
311
+ id, data_key
312
+ );
313
+ return Some(Ok(()));
314
+ }
315
+ Err(err) => {
316
+ let err_payload = err.payload().to_bytes();
317
+ let err_msg = String::from_utf8_lossy(&err_payload);
318
+ warn!("Receiver error for id {}: {}", id, err_msg);
319
+ tokio::task::yield_now().await;
320
+ continue;
321
+ }
322
+ },
323
+ Err(_) => {
324
+ tokio::task::yield_now().await;
325
+ continue;
326
+ }
327
+ }
328
+ }
329
+ }
330
+ Err(_) if attempt == 0 => {
331
+ // First attempt failed - try to grow the pool
332
+ let current_capacity = self.shm_capacity();
333
+ if total_len > current_capacity {
334
+ debug!(
335
+ "Message ({} bytes) exceeds current SHM capacity ({} bytes), growing pool",
336
+ total_len, current_capacity
337
+ );
338
+ if let Some(new_provider) = self.try_grow_shm(total_len) {
339
+ shm_provider = new_provider;
340
+ continue; // Retry with larger pool
341
+ }
342
+ }
343
+ // Couldn't grow, fall back to network
344
+ warn!(
345
+ "SHM allocation failed for {} bytes, falling back to network transport",
346
+ total_len
347
+ );
348
+ return None;
349
+ }
350
+ Err(_) => {
351
+ // Second attempt also failed
352
+ warn!(
353
+ "SHM allocation failed after pool growth, falling back to network transport"
354
+ );
355
+ return None;
356
+ }
357
+ }
358
+ }
359
+ None
360
+ }
361
+
66
362
  pub async fn init(ship_kind: ShipKind, rm_rules_on_disconnect: bool) -> Self {
67
363
  let domain_id = get_domain_id();
68
364
  if domain_id > 0 {
@@ -76,14 +372,6 @@ impl Client {
76
372
  .expect("Failed to open Zenoh session");
77
373
  let session = std::sync::Arc::new(session);
78
374
 
79
- #[cfg(feature = "shm")]
80
- let shm_provider = {
81
- let provider = ShmProviderBuilder::default_backend(SHM_BUFFER_SIZE)
82
- .wait()
83
- .expect("Failed to create SHM provider");
84
- std::sync::Arc::new(provider)
85
- };
86
-
87
375
  let (updated_raw_recv, _) = tokio::sync::broadcast::channel(100);
88
376
  let raw_recv_buff: std::sync::Arc<std::sync::RwLock<RecvBuffer>> =
89
377
  std::sync::Arc::new(std::sync::RwLock::new(HashMap::new()));
@@ -218,7 +506,9 @@ impl Client {
218
506
  session,
219
507
  domain_id,
220
508
  #[cfg(feature = "shm")]
221
- shm_provider,
509
+ shm_state: StdRwLock::new(None),
510
+ #[cfg(feature = "shm")]
511
+ shm_initialized: std::sync::atomic::AtomicBool::new(false),
222
512
  }
223
513
  }
224
514
 
@@ -441,63 +731,22 @@ impl Client {
441
731
 
442
732
  #[cfg(feature = "shm")]
443
733
  if total_len >= SHM_SIZE_THRESHOLD {
444
- // Try to allocate SHM buffer for zero-copy transfer
445
- let shm_result = self
446
- .shm_provider
447
- .alloc(total_len)
448
- .with_policy::<BlockOn<GarbageCollect>>()
449
- .await;
450
-
451
- if let Ok(mut shm_buf) = shm_result {
452
- // Copy data into SHM buffer
453
- shm_buf[0..4].copy_from_slice(&id_bytes);
454
- shm_buf[4] = variable_type.into();
455
- shm_buf[5..69].copy_from_slice(&padded_name);
456
- shm_buf[69..total_len].copy_from_slice(&data);
457
-
458
- // Convert to immutable SHM buffer (can be cloned for retries)
459
- let shm_immut: zenoh::shm::ZShm = shm_buf.into();
460
-
461
- debug!("Sending {} bytes via SHM to {}", total_len, data_key);
462
-
463
- // Use query with SHM payload
464
- loop {
465
- let replies = self
466
- .session
467
- .get(&data_key)
468
- .payload(shm_immut.clone())
469
- .wait()
470
- .map_err(|e| anyhow!("Failed to send data query: {}", e))?;
471
-
472
- match replies.recv_async().await {
473
- Ok(reply) => match reply.result() {
474
- Ok(_sample) => {
475
- debug!(
476
- "Sent data id {} to {} via SHM (ACK received)",
477
- id, data_key
478
- );
479
- return Ok(());
480
- }
481
- Err(err) => {
482
- let err_payload = err.payload().to_bytes();
483
- let err_msg = String::from_utf8_lossy(&err_payload);
484
- warn!("Receiver error for id {}: {}", id, err_msg);
485
- tokio::task::yield_now().await;
486
- continue;
487
- }
488
- },
489
- Err(_) => {
490
- tokio::task::yield_now().await;
491
- continue;
492
- }
493
- }
494
- }
734
+ // Try SHM transfer with dynamic pool growth
735
+ if let Some(result) = self
736
+ .try_shm_send(
737
+ total_len,
738
+ &id_bytes,
739
+ variable_type,
740
+ &padded_name,
741
+ &data,
742
+ &data_key,
743
+ id,
744
+ )
745
+ .await
746
+ {
747
+ return result;
495
748
  }
496
- // Fall through to network transfer if SHM alloc failed
497
- debug!(
498
- "SHM allocation failed, falling back to network for {} bytes",
499
- total_len
500
- );
749
+ // If try_shm_send returns None, fall through to network
501
750
  }
502
751
 
503
752
  // Network transfer (small messages, SHM disabled, or SHM fallback)
@@ -35,8 +35,8 @@ cdr-encoding.workspace = true
35
35
  ratpub = { workspace = true, optional = true }
36
36
 
37
37
  [features]
38
- default = ["ratpub", "shared-memory"]
39
- shared-memory = ["mt_sea/shm", "ratpub?/shared-memory"]
38
+ default = ["ratpub"]
39
+ shm = ["mt_sea/shm", "ratpub?/shm"]
40
40
  humble = ["mt_bagread/humble"]
41
41
  ros2-c = ["dep:r2r", "dep:ros2-interfaces-jazzy-serde"]
42
42
  ros2-native = ["dep:ros2-client", "dep:ros2-interfaces-jazzy-serde"]
@@ -28,8 +28,8 @@ ros2-interfaces-jazzy-rkyv.workspace = true
28
28
  futures-lite = { workspace = true, optional = true }
29
29
 
30
30
  [features]
31
- default = ["shared-memory"]
32
- shared-memory = ["mt_sea/shm"]
31
+ default = []
32
+ shm = ["mt_sea/shm"]
33
33
  smol = ["dep:futures-lite"]
34
34
 
35
35
  [[example]]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes