From ffb08c6881d8b11aab3ec96cb25c2e75341a4aa9 Mon Sep 17 00:00:00 2001 From: ashaffah Date: Mon, 29 Jun 2026 11:46:47 +0700 Subject: [PATCH] feat(nodes): add sysinfo node (host CPU / memory / IP) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A source node that periodically emits the host's CPU usage, memory (used/total/percent), and local IP — the runtime equivalent of an edge agent's "resources" topic, which sandboxed function nodes cannot read. Uses the sysinfo crate (system feature only) for CPU/memory; the local IP is resolved via a UDP-connect trick (no dependency, no packet sent). Verified live: emits real cpu/memory/ip. --- Cargo.lock | 169 +++++++++++++++++++++++++++++++++++ Cargo.toml | 5 ++ README.id.md | 1 + README.md | 1 + src/catalog.rs | 7 ++ src/runtime/nodes/mod.rs | 2 + src/runtime/nodes/sysinfo.rs | 77 ++++++++++++++++ 7 files changed, 262 insertions(+) create mode 100644 src/runtime/nodes/sysinfo.rs diff --git a/Cargo.lock b/Cargo.lock index d79d643..4d2b45a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -764,6 +764,7 @@ dependencies = [ "serde", "serde_json", "sqlx", + "sysinfo", "tokio", "tokio-modbus", "tokio-serial", @@ -1511,6 +1512,15 @@ dependencies = [ "spin 0.5.2", ] +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1614,6 +1624,25 @@ dependencies = [ "libc", ] +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "objc2-io-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +dependencies = [ + "libc", + "objc2-core-foundation", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -2746,6 +2775,20 @@ dependencies = [ "syn", ] +[[package]] +name = "sysinfo" +version = "0.39.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8bd2130a9b60bee2581bf82cfe89ee836424d1f37dcfa4ce21509611684673" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "windows", +] + [[package]] name = "tag_ptr" version = "0.1.0" @@ -3410,6 +3453,22 @@ dependencies = [ "wasite", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.11" @@ -3419,12 +3478,113 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -3483,6 +3643,15 @@ dependencies = [ "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" diff --git a/Cargo.toml b/Cargo.toml index 88475f1..cc53324 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,11 @@ rumqttc = { version = "0.25", features = ["websocket", "url"] } rhai = { version = "1", features = ["sync", "serde"] } regex = "1" +# Host CPU / memory for the sysinfo node. "system" feature only (no disk / +# network / component) to keep the footprint small. Cross-compiles to the +# musl/armv7/windows targets. +sysinfo = { version = "0.39", default-features = false, features = ["system"] } + # Function node alternative languages: # boa_engine = pure-Rust JavaScript engine (cross-compiles cleanly to # musl/armv7/windows; no C toolchain needed). diff --git a/README.id.md b/README.id.md index 4548e3e..59d5cb8 100644 --- a/README.id.md +++ b/README.id.md @@ -36,6 +36,7 @@ axum + rust-embed + Svelte/xyflow untuk editornya. | `modbus-read` | master: baca holding/input/coils/discrete → emit | | `modbus-write` | master: tulis register/coil, atau float/uint 32-bit (2 register), dari payload | | `modbus-slave` | **server**: jadi device Modbus yang dipoll master eksternal | + | `sysinfo` | source: CPU%, memori, dan IP lokal host (snapshot resource) | | `serial-ascii` | baca baris ASCII dari serial (mis. timbangan) + kirim command; regex, byte-map, parity/stop/data bits | | `mqtt-in` / `mqtt-out` | subscribe / publish MQTT | - **Modbus transport**: TCP, RTU-over-TCP, RTU-serial (master); TCP + RTU-serial diff --git a/README.md b/README.md index ec4e13e..d80a416 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ rust-embed + Svelte/xyflow for the editor. | `modbus-read` | master: read holding/input/coils/discrete → emit | | `modbus-write` | master: write a register/coil, or a 32-bit float/uint over two registers, from the payload | | `modbus-slave` | **server**: act as a Modbus device polled by an external master | + | `sysinfo` | source: host CPU%, memory, and local IP (the resource snapshot) | | `serial-ascii` | read ASCII lines from a serial port (e.g. a scale) + write commands; regex, byte-map, parity/stop/data bits | | `mqtt-in` / `mqtt-out` | subscribe / publish MQTT | - **Modbus transports**: TCP, RTU-over-TCP, RTU-serial (master); TCP + RTU-serial diff --git a/src/catalog.rs b/src/catalog.rs index 328f539..b7338cd 100644 --- a/src/catalog.rs +++ b/src/catalog.rs @@ -318,6 +318,13 @@ pub fn catalog() -> Value { "type": "textarea", "default": "" } ] }, + { + "type": "sysinfo", "label": "System Info", "category": "input", + "color": "#9ecbd8", "inputs": 0, "outputs": 1, + "fields": [ + { "key": "interval_ms", "label": "Interval (ms)", "type": "number", "default": 5000 } + ] + }, mqtt_in, mqtt_out, mqtt_broker diff --git a/src/runtime/nodes/mod.rs b/src/runtime/nodes/mod.rs index 01c062a..dd85932 100644 --- a/src/runtime/nodes/mod.rs +++ b/src/runtime/nodes/mod.rs @@ -23,6 +23,7 @@ mod sort; mod split; mod status; mod switch; +mod sysinfo; mod template; mod trigger; @@ -57,6 +58,7 @@ pub fn build_node(node_type: &str, config: &Value) -> Result> { "modbus-write" => Box::new(modbus::ModbusWriteNode::from_config(config)?), "modbus-slave" => Box::new(modbus_slave::ModbusSlaveNode::from_config(config)?), "serial-ascii" => Box::new(serial_ascii::SerialAsciiNode::from_config(config)?), + "sysinfo" => Box::new(sysinfo::SysInfoNode::from_config(config)?), "mqtt-in" => Box::new(mqtt::MqttInNode::from_config(config)?), "mqtt-out" => Box::new(mqtt::MqttOutNode::from_config(config)?), other => bail!("unknown node type: {other}"), diff --git a/src/runtime/nodes/sysinfo.rs b/src/runtime/nodes/sysinfo.rs new file mode 100644 index 0000000..8437e3e --- /dev/null +++ b/src/runtime/nodes/sysinfo.rs @@ -0,0 +1,77 @@ +//! System info node: a source that periodically emits host CPU usage, memory, +//! and the local IP — the runtime equivalent of an edge agent's "resources". + +use std::time::Duration; + +use async_trait::async_trait; +use serde::Deserialize; +use serde_json::{Value, json}; + +use crate::model::Msg; +use crate::runtime::node::{Node, NodeCtx}; +use crate::runtime::nodes::parse_config; + +fn default_interval() -> u64 { + 5000 +} + +#[derive(Debug, Deserialize)] +pub struct SysInfoNode { + #[serde(default = "default_interval")] + interval_ms: u64, +} + +impl Default for SysInfoNode { + fn default() -> Self { + Self { + interval_ms: default_interval(), + } + } +} + +impl SysInfoNode { + pub fn from_config(config: &Value) -> anyhow::Result { + parse_config(config) + } +} + +/// Best-effort local IP: the source address the OS would use to reach the +/// internet. No packet is actually sent (UDP connect just selects a route). +fn local_ip() -> Option { + let sock = std::net::UdpSocket::bind("0.0.0.0:0").ok()?; + sock.connect("8.8.8.8:80").ok()?; + sock.local_addr().ok().map(|addr| addr.ip().to_string()) +} + +#[async_trait] +impl Node for SysInfoNode { + async fn run(self: Box, ctx: NodeCtx) -> anyhow::Result<()> { + let mut sys = ::sysinfo::System::new(); + // CPU usage is measured between two refreshes — prime it, then leave a + // short gap so the first reading is meaningful. + sys.refresh_cpu_usage(); + tokio::time::sleep(::sysinfo::MINIMUM_CPU_UPDATE_INTERVAL).await; + + let period = Duration::from_millis(self.interval_ms.max(500)); + let mut tick = tokio::time::interval(period); + loop { + tick.tick().await; + sys.refresh_cpu_usage(); + sys.refresh_memory(); + let used = sys.used_memory(); + let total = sys.total_memory(); + let percent = if total > 0 { + (used as f64 / total as f64 * 1000.0).round() / 10.0 + } else { + 0.0 + }; + let cpu = (sys.global_cpu_usage() as f64 * 10.0).round() / 10.0; + let payload = json!({ + "cpu": cpu, + "memory": { "used": used, "total": total, "percent": percent }, + "ip": local_ip(), + }); + ctx.emit(Msg::new(payload)).await; + } + } +}