Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
169 changes: 169 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
1 change: 1 addition & 0 deletions README.id.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions src/catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/runtime/nodes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ mod sort;
mod split;
mod status;
mod switch;
mod sysinfo;
mod template;
mod trigger;

Expand Down Expand Up @@ -57,6 +58,7 @@ pub fn build_node(node_type: &str, config: &Value) -> Result<Box<dyn Node>> {
"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}"),
Expand Down
77 changes: 77 additions & 0 deletions src/runtime/nodes/sysinfo.rs
Original file line number Diff line number Diff line change
@@ -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<Self> {
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<String> {
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<Self>, 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;
}
}
}