From 34113a7e1456ce41f04648ef04ba49da4c83577d Mon Sep 17 00:00:00 2001 From: kelsoprotein-lab Date: Tue, 21 Apr 2026 15:44:28 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(slave-app):=20=E6=96=B0=E5=BB=BA?= =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E6=A1=86=E6=94=AF=E6=8C=81=20TLS=20=E2=80=94?= =?UTF-8?q?=20Transport::TcpTls=20+=20=E6=8C=81=E4=B9=85=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ui-shared/project.rs: TcpSpec 新增 #[serde(default)] tls: Option TlsSpec 字段对齐 SlaveTlsConfig(cert/key/ca/pkcs12/password/require_client_cert) schema_version 不动,旧 .modbusproj 文件 default=None 自然兼容 - egui/app.rs: - SlaveApp 加 new_use_tls / new_cert_file / new_key_file / new_pkcs12_file / new_pkcs12_password / new_ca_file / new_require_client_cert 表单字段 - 新建对话框:☐ 启用 TLS 折叠区,PEM (cert+key) 或 PKCS#12 二选一, 可选 CA + mTLS require_client_cert - allocate_connection 标签前缀 TLS:// vs TCP, spawn_create_tcp 接受 Option 并构造 Transport::TcpTls, create_tcp_connection 校验 TLS 字段非空 - load_project / build_project 双向同步 TLS 配置 - ui-shared 单元测试新增 slave_tls_roundtrip_and_legacy_compat --- crates/modbussim-egui/src/app.rs | 172 +++++++++++++++++++--- crates/modbussim-ui-shared/src/project.rs | 59 ++++++++ 2 files changed, 213 insertions(+), 18 deletions(-) diff --git a/crates/modbussim-egui/src/app.rs b/crates/modbussim-egui/src/app.rs index a99c612..a1b9ff9 100644 --- a/crates/modbussim-egui/src/app.rs +++ b/crates/modbussim-egui/src/app.rs @@ -10,12 +10,13 @@ use modbussim_core::log_collector::LogCollector; use modbussim_core::log_entry::LogEntry; use modbussim_core::register::{decode_value, DataType, Endian, RegisterDef, RegisterType}; use modbussim_core::slave::{ConnectionState, SlaveConnection, SlaveDevice}; -use modbussim_core::transport::Transport; +use modbussim_core::transport::{SlaveTlsConfig, Transport}; use modbussim_ui_shared::format::{format_u16, U16Format}; use modbussim_ui_shared::icons; use modbussim_ui_shared::log_panel::{self, LogPanelAction, LogPanelState}; use modbussim_ui_shared::project::{ - deserialize_slave, serialize_slave, SlaveConnectionSave, SlaveDeviceSave, SlaveProject, TcpSpec, + deserialize_slave, serialize_slave, SlaveConnectionSave, SlaveDeviceSave, SlaveProject, + TcpSpec, TlsSpec, }; use modbussim_ui_shared::theme::{self, Flavor}; use modbussim_ui_shared::ui as uikit; @@ -281,6 +282,15 @@ pub struct SlaveApp { new_host: String, new_port: String, show_new_tcp_dialog: bool, + // —— 新建连接的 TLS 表单字段 —— + new_use_tls: bool, + /// PEM cert 路径(与 pkcs12_file 互斥;同时填则 PKCS#12 优先) + new_cert_file: String, + new_key_file: String, + new_ca_file: String, + new_require_client_cert: bool, + new_pkcs12_file: String, + new_pkcs12_password: String, /// 删除连接二次确认状态:(conn_id, 首次点击时刻)。 /// 3 秒内同一连接再次点删除按钮 → 真删;否则按钮 label 自动恢复。 pending_delete: Option<(String, std::time::Instant)>, @@ -499,6 +509,13 @@ impl SlaveApp { new_host: "0.0.0.0".to_string(), new_port: "5502".to_string(), show_new_tcp_dialog: false, + new_use_tls: false, + new_cert_file: String::new(), + new_key_file: String::new(), + new_ca_file: String::new(), + new_require_client_cert: false, + new_pkcs12_file: String::new(), + new_pkcs12_password: String::new(), pending_delete: None, last_error: None, conn_snapshot: Vec::new(), @@ -1065,17 +1082,38 @@ impl SlaveApp { fn allocate_connection(&self) -> (String, String) { let n = self.next_conn_seq.fetch_add(1, Ordering::Relaxed); let id = format!("slave_{}", n); - let label = format!("TCP {}:{}", self.new_host.trim(), self.new_port.trim()); + let proto = if self.new_use_tls { "TLS" } else { "TCP" }; + let label = format!( + "{} {}:{}", + proto, + self.new_host.trim(), + self.new_port.trim() + ); (id, label) } - fn spawn_create_tcp(&self, id: String, label: String, host: String, port: u16) { + fn spawn_create_tcp( + &self, + id: String, + label: String, + host: String, + port: u16, + tls: Option, + ) { let connections = self.connections.clone(); let tx = self.events_tx.clone(); self.rt.spawn(async move { let log_collector = Arc::new(LogCollector::new()); - let connection = SlaveConnection::new(Transport::Tcp { host, port }) - .with_log_collector(log_collector.clone()); + let transport = if tls.is_some() { + Transport::TcpTls { host, port } + } else { + Transport::Tcp { host, port } + }; + let mut connection = + SlaveConnection::new(transport).with_log_collector(log_collector.clone()); + if let Some(cfg) = tls { + connection = connection.with_tls_config(cfg); + } let device = SlaveDevice::with_default_registers(1, "从站 1", 20000); let device_snap = DeviceSnapshot { slave_id: device.slave_id, @@ -1110,8 +1148,29 @@ impl SlaveApp { return; } }; + let tls = if self.new_use_tls { + let has_pem = + !self.new_cert_file.trim().is_empty() && !self.new_key_file.trim().is_empty(); + let has_pkcs12 = !self.new_pkcs12_file.trim().is_empty(); + if !has_pem && !has_pkcs12 { + self.last_error = + Some("启用 TLS 需要填写 cert+key(PEM)或 pkcs12 文件路径".to_string()); + return; + } + Some(SlaveTlsConfig { + enabled: true, + cert_file: self.new_cert_file.trim().to_string(), + key_file: self.new_key_file.trim().to_string(), + ca_file: self.new_ca_file.trim().to_string(), + require_client_cert: self.new_require_client_cert, + pkcs12_file: self.new_pkcs12_file.trim().to_string(), + pkcs12_password: self.new_pkcs12_password.clone(), + }) + } else { + None + }; let (id, label) = self.allocate_connection(); - self.spawn_create_tcp(id, label, host, port); + self.spawn_create_tcp(id, label, host, port, tls); ctx.request_repaint(); } @@ -1291,19 +1350,38 @@ impl SlaveApp { fn build_project(&self) -> SlaveProject { let mut proj = SlaveProject::new(); for snap in &self.conn_snapshot { - let (host, port) = self + // 同时取出 host/port 与可选 TLS 配置,单次 try_read 完成 + let (host, port, tls) = self .connections .try_read() .ok() .and_then(|list| { list.iter().find(|e| e.id == snap.id).and_then(|e| { - e.connection.try_read().ok().map(|c| match &c.transport { - Transport::Tcp { host, port } => (host.clone(), *port), - _ => ("0.0.0.0".to_string(), 502), + e.connection.try_read().ok().map(|c| { + let (h, p) = match &c.transport { + Transport::Tcp { host, port } + | Transport::TcpTls { host, port } => (host.clone(), *port), + _ => ("0.0.0.0".to_string(), 502), + }; + let tls = if matches!(c.transport, Transport::TcpTls { .. }) + && c.tls_config.enabled + { + Some(TlsSpec { + cert_file: c.tls_config.cert_file.clone(), + key_file: c.tls_config.key_file.clone(), + ca_file: c.tls_config.ca_file.clone(), + require_client_cert: c.tls_config.require_client_cert, + pkcs12_file: c.tls_config.pkcs12_file.clone(), + pkcs12_password: c.tls_config.pkcs12_password.clone(), + }) + } else { + None + }; + (h, p, tls) }) }) }) - .unwrap_or_else(|| ("0.0.0.0".to_string(), 502)); + .unwrap_or_else(|| ("0.0.0.0".to_string(), 502, None)); let devices: Vec = snap .devices @@ -1321,7 +1399,7 @@ impl SlaveApp { proj.connections.push(SlaveConnectionSave { label: snap.label.clone(), - tcp: TcpSpec { host, port }, + tcp: TcpSpec { host, port, tls }, devices, }); } @@ -1394,11 +1472,30 @@ impl SlaveApp { let id = format!("slave_{}", next_seq.fetch_add(1, Ordering::Relaxed)); let label = c.label.clone(); let log_collector = Arc::new(LogCollector::new()); - let connection = SlaveConnection::new(Transport::Tcp { - host: c.tcp.host.clone(), - port: c.tcp.port, - }) - .with_log_collector(log_collector.clone()); + let transport = if c.tcp.tls.is_some() { + Transport::TcpTls { + host: c.tcp.host.clone(), + port: c.tcp.port, + } + } else { + Transport::Tcp { + host: c.tcp.host.clone(), + port: c.tcp.port, + } + }; + let mut connection = + SlaveConnection::new(transport).with_log_collector(log_collector.clone()); + if let Some(tls) = c.tcp.tls.as_ref() { + connection = connection.with_tls_config(SlaveTlsConfig { + enabled: true, + cert_file: tls.cert_file.clone(), + key_file: tls.key_file.clone(), + ca_file: tls.ca_file.clone(), + require_client_cert: tls.require_client_cert, + pkcs12_file: tls.pkcs12_file.clone(), + pkcs12_password: tls.pkcs12_password.clone(), + }); + } let mut device_snapshots = Vec::new(); for ds in &c.devices { @@ -3420,6 +3517,45 @@ impl eframe::App for SlaveApp { ui.end_row(); }); ui.add_space(4.0); + ui.checkbox(&mut self.new_use_tls, "启用 TLS"); + if self.new_use_tls { + ui.add_space(2.0); + egui::Grid::new("new_tcp_tls_form") + .num_columns(2) + .spacing([8.0, 4.0]) + .show(ui, |ui| { + ui.label("Cert (PEM)"); + ui.text_edit_singleline(&mut self.new_cert_file); + ui.end_row(); + ui.label("Key (PEM)"); + ui.text_edit_singleline(&mut self.new_key_file); + ui.end_row(); + ui.label("PKCS#12"); + ui.text_edit_singleline(&mut self.new_pkcs12_file); + ui.end_row(); + ui.label("PKCS#12 密码"); + ui.add( + egui::TextEdit::singleline( + &mut self.new_pkcs12_password, + ) + .password(true), + ); + ui.end_row(); + ui.label("CA (可选)"); + ui.text_edit_singleline(&mut self.new_ca_file); + ui.end_row(); + }); + ui.checkbox( + &mut self.new_require_client_cert, + "要求客户端证书 (mTLS)", + ); + theme::text::crumb( + ui, + self.flavor, + "PEM (cert+key) 与 PKCS#12 二选一;两者都填则 PKCS#12 优先", + ); + ui.add_space(4.0); + } ui.horizontal(|ui| { if uikit::primary_button(ui, self.flavor, "创建").clicked() { diff --git a/crates/modbussim-ui-shared/src/project.rs b/crates/modbussim-ui-shared/src/project.rs index 56e6176..c09208d 100644 --- a/crates/modbussim-ui-shared/src/project.rs +++ b/crates/modbussim-ui-shared/src/project.rs @@ -19,6 +19,27 @@ pub enum EguiProjectType { pub struct TcpSpec { pub host: String, pub port: u16, + /// 可选 TLS 配置;旧项目文件无此字段时按 None(明文 TCP)解析。 + #[serde(default)] + pub tls: Option, +} + +/// 子站 TLS 持久化字段,与 `modbussim_core::transport::SlaveTlsConfig` +/// 字段一一对应。任何字段缺失视为空字符串/false,便于演进。 +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct TlsSpec { + #[serde(default)] + pub cert_file: String, + #[serde(default)] + pub key_file: String, + #[serde(default)] + pub ca_file: String, + #[serde(default)] + pub require_client_cert: bool, + #[serde(default)] + pub pkcs12_file: String, + #[serde(default)] + pub pkcs12_password: String, } // --- Slave --- @@ -165,6 +186,7 @@ mod tests { tcp: TcpSpec { host: "0.0.0.0".into(), port: 502, + tls: None, }, devices: vec![SlaveDeviceSave { slave_id: 1, @@ -186,6 +208,7 @@ mod tests { tcp: TcpSpec { host: "127.0.0.1".into(), port: 5502, + tls: None, }, slave_id: 1, timeout_ms: 3000, @@ -201,6 +224,42 @@ mod tests { assert_eq!(q.connections[0].poll.as_ref().unwrap().qty, 10); } + #[test] + fn slave_tls_roundtrip_and_legacy_compat() { + let mut p = SlaveProject::new(); + p.connections.push(SlaveConnectionSave { + label: "TLS 0.0.0.0:8502".into(), + tcp: TcpSpec { + host: "0.0.0.0".into(), + port: 8502, + tls: Some(TlsSpec { + cert_file: "/etc/cert.pem".into(), + key_file: "/etc/key.pem".into(), + ca_file: String::new(), + require_client_cert: false, + pkcs12_file: String::new(), + pkcs12_password: String::new(), + }), + }, + devices: Vec::new(), + }); + let json = serialize_slave(&p).unwrap(); + let q = deserialize_slave(&json).unwrap(); + let tls = q.connections[0].tcp.tls.clone(); + assert_eq!(tls.unwrap().cert_file, "/etc/cert.pem"); + + // 旧文件(无 tls 字段)应当照常解析、tls=None + let legacy = r#"{ + "schema_version": 2, + "type": "slave", + "connections": [ + {"label":"L","tcp":{"host":"0.0.0.0","port":502},"devices":[]} + ] + }"#; + let parsed = deserialize_slave(legacy).unwrap(); + assert!(parsed.connections[0].tcp.tls.is_none()); + } + #[test] fn wrong_type_rejected() { let master = MasterProject::new(); From 1388716d67d4c0defb6fe4dc1caa065229587e86 Mon Sep 17 00:00:00 2001 From: kelsoprotein-lab Date: Wed, 22 Apr 2026 00:16:03 +0800 Subject: [PATCH 2/3] =?UTF-8?q?docs(spec):=20slave=20=E7=A9=BA=E7=8A=B6?= =?UTF-8?q?=E6=80=81=20Hero=20=E4=B8=89=E8=89=B2=20dancing=20strings=20?= =?UTF-8?q?=E5=8A=A8=E7=94=BB=E8=AE=BE=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 Selection::None 空状态内联一块 Hero 画布(参考 egui demo 的 dancing_strings),三条弦按 Holding/Coil/DiscreteInput 的 accent/ success/warn 三色分派,振幅由 LogCollector 的 1s 滚动 TX/RX 计数 驱动,有 Error 级日志时三弦暂时转 danger 红。模块独立放进 modbussim-ui-shared 方便 Master 端未来复用,app.rs 不再继续膨胀。 Co-Authored-By: Claude Opus 4.7 (1M context) --- ...ave-welcome-hero-dancing-strings-design.md | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-22-slave-welcome-hero-dancing-strings-design.md diff --git a/docs/superpowers/specs/2026-04-22-slave-welcome-hero-dancing-strings-design.md b/docs/superpowers/specs/2026-04-22-slave-welcome-hero-dancing-strings-design.md new file mode 100644 index 0000000..d0769c4 --- /dev/null +++ b/docs/superpowers/specs/2026-04-22-slave-welcome-hero-dancing-strings-design.md @@ -0,0 +1,136 @@ +# Slave 端空状态 Hero · 三色 Dancing Strings 动画 + +> 规划日期:2026-04-22 · 分支:`feat/slave-tls-ui-2026-04` + +## Context + +ModbusSim Slave 端 UI (`crates/modbussim-egui`) 目前 `Selection::None` 空状态只有一行大标题 + 一段提示文案(`app.rs:2250-2259`)。当用户还没建任何连接、或临时切走选中项时,CentralPanel 大片留白显得单薄。项目已经有脉动状态灯(`app.rs:3660-3688`,commit `37769f8`)作为生命体征的"微动",但占屏非常小。 + +参考 egui 官方 demo 的 [`dancing_strings.rs`](https://github.com/emilk/egui/blob/main/crates/egui_demo_lib/src/demo/dancing_strings.rs),可以在空状态加一块 Hero 画布,既让首屏更有"活着"的质感,又能把 Slave 运行时的 TX/RX 节奏直观视觉化——"没在忙"时轻轻起伏,"在忙"时琴弦跃动,出错瞬间三条变红。目标是纯视觉强化,不引入新的业务数据通道、不动现有 theme/log_panel。 + +## 最终方案 + +### 1. 模块边界 + +新增 `crates/modbussim-ui-shared/src/hero_anim.rs`,对外: + +```rust +pub struct HeroPulseFeed { + pub amp: f32, // 0.0..=1.0 + pub has_error: bool, + pub disabled: bool, // 预留开关,当前恒 false +} + +pub fn show_welcome_hero(ui: &mut egui::Ui, feed: HeroPulseFeed); +``` + +- 模块不持有业务状态,只负责渲染 + 调用 `ctx.request_repaint_after`。 +- 放在 `ui-shared` 而非 `modbussim-egui` 本地:未来 Master 端 (`modbusmaster-egui`) 可直接复用。 +- `app.rs` `Selection::None` 分支(L2250-2259)整段替换为 `show_welcome_hero(ui, self.hero_pulse.feed())`。 + +### 2. 心跳采样 + +在 `SlaveApp` 加字段 `hero_pulse: HeroPulseState`: + +```rust +struct HeroPulseState { + ring: [u32; 10], // 1s 窗口 × 10 × 100ms bucket + err_ring: [u32; 10], + cursor: usize, + last_tick: std::time::Instant, + last_seen_log_id: u64, +} +``` + +- 每帧 `update()` 开头调用 `hero_pulse.tick(&log_collector)`: + 1. `Instant::now() - last_tick >= 100ms` 时游标前推、旧 bucket 清零。 + 2. 从 `LogCollector` 拉取 `last_seen_log_id` 之后的**原始入账**条目(不受 UI 过滤影响)——需在 `LogCollector` 加一个 `total_ingested_since(id: u64) -> impl Iterator` 访问器(3-5 行)。 + 3. 每条记录累加到当前 bucket;`level == Error` 同步计入 `err_ring`。 +- `feed()` 返回: + - `amp = (sum(ring) as f32 / SATURATION_RATE).clamp(0.0, 1.0)`,`SATURATION_RATE` 取 40(约每 100ms 4 条消息即满振)。 + - `has_error = sum(err_ring) > 0`。 + +不引入新 channel、不改 `UiEvent`、不计时响应延迟。 + +### 3. 绘制与布局 + +`show_welcome_hero` 内部: + +``` +vertical_centered { + heading("ModbusSim · Slave") + add_space(12) + Frame::canvas { + allocate_exact_size(vec2(avail_w.min(640), 180)) + draw 3 strings: mode ∈ {2, 3, 5} + } + add_space(8) + muted_label("从左侧创建或选中一个连接 · 或按 ⌘N 新建") +} +``` + +- 坐标变换:`RectTransform::from_to(Rect{x:0..=1, y:-1..=1}, canvas_rect)`(沿用 demo)。 +- 琴弦公式(在 demo 基础上乘入 `gain`): + + ```rust + let gain = 0.15 + 0.85 * feed.amp; // 底噪 15%,满载 100% + let base = (time * SPEED * mode).sin() / mode; + let y = gain * base * (t * TAU/2.0 * mode).sin(); + ``` + + `SPEED = 1.5`,`n = 120` 采样点,`thickness = 10.0 / mode`。 +- 颜色:默认 `[Theme::accent(), Theme::success(), Theme::warn()]`;`feed.has_error` 时三条各自 `lerp(color, Theme::danger(), 0.6)`。 +- **不用** Window(demo 用的是 Window,这里内联 CentralPanel);**不用** `PathStroke::new_uv` 的渐变(与语义三色冲突)。 + +### 4. 重绘节流 + +- `feed.amp >= 0.3` → `request_repaint_after(16ms)`(~60fps) +- `feed.amp < 0.3` → `request_repaint_after(50ms)`(~20fps,与状态栏脉动节奏对齐) +- `ui.ctx().input(|i| !i.focused)` → `request_repaint_after(100ms)` +- `feed.disabled == true` → 直接 `return`,画纯文本欢迎屏 + +### 5. 需修改/新增文件 + +| 文件 | 改动 | +|---|---| +| `crates/modbussim-ui-shared/src/hero_anim.rs` | 新建 | +| `crates/modbussim-ui-shared/src/lib.rs` | `pub mod hero_anim;` | +| `crates/modbussim-ui-shared/src/log_panel.rs` | `LogCollector::total_ingested_since()` + `next_log_id()` | +| `crates/modbussim-egui/src/app.rs` | `SlaveApp` 加 `hero_pulse`;`update()` 前置 tick;`Selection::None` 分支改调 `show_welcome_hero` | + +**不改**:`theme.rs`、`value_panel.rs`、任何 Master 端 crate。 + +### 6. 测试 · 验证 + +1. **单测**(`hero_pulse_state`,不依赖 egui): + - 注入 10 条日志 → `amp ≈ 1.0` + - 静置 1.1s → `amp == 0.0` + - 注入 Error 级日志 → `has_error == true`;1s 后清零 + - 游标轮转正确性(bucket 越界) +2. **手动视觉验证**: + - `cargo run -p modbussim-egui` → `Selection::None` 可见三色动画 + - 建连接开始 TX/RX → 振幅明显上拉 + - 人为触发解析错误 → 三弦短暂转红 + - 窗口失焦 → CPU 降档(用 Activity Monitor 观察) +3. **回归**:`cargo fmt --all`、`cargo clippy --workspace -- -D warnings`、`cargo test --workspace`。 + +### 7. 风险 / 不做清单 + +已识别: +- **CPU**:三条 120 点 path/frame,60fps,egui 可轻松承受;节流已覆盖。 +- **色盲友好度**:蓝/绿/橙 + 错误红在三色色盲下辨识度一般。本次不加开关,后续可做 `accessibility::color_blind_mode`。 +- **数据源范围**:只统计 `LogCollector` 入账条目(所有 TX/RX + Log);不引入独立计数器以免重复维护。 + +YAGNI(明确不做): +- 动画开关偏好 UI +- 声音效果 +- Master 端接入(API 预留,本次不调) +- 寄存器值→振幅的逐寄存器映射 + +### 8. 关键文件路径(索引) + +- `crates/modbussim-egui/src/app.rs:2250-2259` — `Selection::None` 当前空状态 +- `crates/modbussim-egui/src/app.rs:3660-3688` — 既有脉动状态灯(风格参照) +- `crates/modbussim-ui-shared/src/log_panel.rs` — `LogCollector` 持有者 +- `crates/modbussim-ui-shared/src/theme.rs:66-385` — `accent/success/warn/danger` 色板 +- 参考:egui `crates/egui_demo_lib/src/demo/dancing_strings.rs` From 8c30dfdc3d956b7bc83e571178a28861ff1cb2ef Mon Sep 17 00:00:00 2001 From: kelsoprotein-lab Date: Wed, 22 Apr 2026 00:16:03 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix(master-app):=20=E8=A1=A5=E9=BD=90=20Tcp?= =?UTF-8?q?Spec=20{=20tls:=20None=20}=20=E5=AD=97=E6=AE=B5=20=E2=80=94=20C?= =?UTF-8?q?I=20E0063=20=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TcpSpec 在 ui-shared 加了 tls: Option 字段后, master crate 的 save 路径字面量未同步更新,导致 CI 编译失败。 Master 暂不暴露 TLS UI,save 时一律写 None; 旧 .modbusproj 读取由 #[serde(default)] 自动兼容。 --- crates/modbusmaster-egui/src/app.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/modbusmaster-egui/src/app.rs b/crates/modbusmaster-egui/src/app.rs index 361478a..03742c9 100644 --- a/crates/modbusmaster-egui/src/app.rs +++ b/crates/modbusmaster-egui/src/app.rs @@ -612,7 +612,11 @@ impl MasterApp { proj.connections.push(MasterConnectionSave { label: s.label.clone(), - tcp: TcpSpec { host, port }, + tcp: TcpSpec { + host, + port, + tls: None, + }, slave_id: s.slave_id, timeout_ms, poll,