- Rust 78.6%
- TypeScript 18.4%
- Python 1.8%
- CSS 0.6%
- Shell 0.3%
- Other 0.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .cargo | ||
| .cargo-husky/hooks | ||
| .github/workflows | ||
| apps | ||
| docs/client | ||
| examples | ||
| node/iqvmetal | ||
| python/iqvmetal | ||
| scripts | ||
| src | ||
| vendor | ||
| .gitignore | ||
| .gitmodules | ||
| build.rs | ||
| Cargo.lock | ||
| Cargo.toml | ||
| config.json | ||
| pyproject.toml | ||
| README.md | ||
metal
Rust driver for talking to CNC controllers and reporting machine data.
Drivers
| Kind | SDK | Platform notes |
|---|---|---|
| Fanuc | FOCAS (fwlib32) |
Linux + Windows |
| HEIDENHAIN | LSV2 (TCP) | Linux + Windows; read-only telemetry currently |
| Mitsubishi | EZSocket (EZSocketNc COM) |
Windows only (COM) |
| Mazak | MTConnect 1.3 (HTTP/XML) | Linux + Windows |
| Haas | MTConnect 1.2 (HTTP/XML) | Linux + Windows; NGC agent on port 8082 |
| Siemens | OPC UA (Access MyMachine) | Linux + Windows; SINUMERIK 828D / 840D sl / ONE on port 4840 |
| Siemens S7 | Snap7 (S7 DB mirror) | Linux + Windows; TCP 102; requires a configured read-only PLC data-block mapping |
| DMG MORI | OPC UA (Machine Data Connector) | Linux + Windows; CELOS / MDC on port 4840 |
Development is typically done on Linux and cross-compiled to 32-bit Windows (i686-pc-windows-gnu), which is the intended runtime on the shop floor.
# service (MQTT)
cargo win-build # create 32bit windows binary
METAL_CONFIG=config.json metal.exe
# Siemens Snap7 smoke (live PLC; exact addresses are required)
MACHINE_HOST=192.168.1.80 SNAP7_MAPPING_JSON='{"rack":0,"slot":2,"profile":"custom","channel":1,"db":0,"start":0,"size":0,"fields":[{"field":"status_status_code","db":21,"byte":35,"offset":0,"data_type":"bool","bit":0}]}' smoke.exe
# service (MQTT)
cargo win-build # create 32-bit Windows binary
$env:METAL_CONFIG = "config.json"
.\metal.exe
# Siemens Snap7 smoke
$env:MACHINE_HOST = "192.168.1.80"
$env:SNAP7_MAPPING_JSON = '{"rack":0,"slot":2,"profile":"sinumerik_840d_sl","channel":1,"db":0,"start":0,"size":0,"fields":[]}'
$env:SNAP7_SMOKE_SAMPLES = "10"
$env:SNAP7_EXPECT_JSON = '{"status_status_code":"run"}'
.\smoke.exe
Python usage:
import json
from iqvmetal import DmgMori, Fanuc, Haas, Heidenhain, Mazak, Mitsubishi, SiemensOpcUa, SiemensSnap7
fanuc = Fanuc("192.168.1.10")
print(json.loads(fanuc.sample_json()))
mitsubishi = Mitsubishi("192.168.1.20", system_type="m800m")
print(mitsubishi.serial())
heidenhain = Heidenhain("192.168.1.30")
print(json.loads(heidenhain.sample_json()))
mazak = Mazak("192.168.1.40", device_selector="MAZAK-M79MC510639")
print(json.loads(mazak.sample_json()))
haas = Haas("192.168.1.50")
print(json.loads(haas.sample_json()))
siemens = SiemensOpcUa("192.168.1.60", username="OpcUaClient", password="OpcUaClient")
print(json.loads(siemens.sample_json()))
snap7 = SiemensSnap7("192.168.1.80", json.dumps({
"rack": 0, "slot": 1, "db": 100, "start": 0, "size": 8,
"fields": [{"field": "spindle_speed", "offset": 0, "data_type": "u32"}],
}))
print(json.loads(snap7.sample_json()))
dmgmori = DmgMori("192.168.1.70")
print(json.loads(dmgmori.sample_json()))
Agent (default metal binary)
- Loads / creates
config.json(METAL_CONFIGoverrides path). - Connects MQTT (
MQTT_HOST/MQTT_PORT/MQTT_CLIENT_ID). - Subscribes the startup
{company_name}/config, publishes{company_name}/<machine-id>, and acknowledges on{company_name}/config/ackby default. - Reconciles the full desired CNC fleet: it starts new workers, stops removed workers, and restarts only machines whose endpoint settings changed.
# broker must be reachable
MQTT_HOST=localhost MQTT_PORT=1883 cargo run --release
# push a full desired fleet snapshot
mosquitto_pub -h localhost -t amarjay/config -m '{"company_name":"amarjay","machines":[{"id":"lathe-01","host":"192.168.1.10","port":8193,"kind":"fanuc"},{"id":"mill-02","host":"192.168.1.20","port":683,"kind":"mitsubishi"},{"id":"tnc-01","host":"192.168.1.30","port":19000,"kind":"heidenhain"},{"id":"mazak-01","host":"192.168.1.40","port":5000,"kind":"mazak","mtconnect_device":"MAZAK-M79MC510639"},{"id":"haas-01","host":"192.168.1.50","port":8082,"kind":"haas"},{"id":"sinumerik-01","host":"192.168.1.60","port":4840,"kind":"siemens","opcua_username":"OpcUaClient","opcua_password":"OpcUaClient"},{"id":"nlx-01","host":"192.168.1.70","port":4840,"kind":"dmgmori"}]}'
Siemens S7 / Snap7
kind: "siemens_snap7" is deliberately separate from kind: "siemens" (SINUMERIK OPC UA). An explicit mapping or controller profile always uses classic Snap7 and reads the configured DB addresses. With no mapping, the native S7 driver may use S7CommPlus typed-symbol discovery on S7-1200/1500 controllers.
The snap7 object below supports exact per-field DB addresses. Built-in sinumerik_840d_sl, sinumerik_828d, and sinumerik_808d profiles decode the documented channel program-state byte. When override_encoding is binary, they also map the documented channel feed and rapid-override bytes. Gray-coded override percentages remain machine-specific and are therefore not guessed. Explicit fields override profile fields with the same destination. Classic access cannot safely infer member names or types from arbitrary DB bytes; S7-1200/1500 classic access may also require PUT/GET and non-optimized block access.
Snap7 is a required native dependency, vendored as vendor/snap7, linked during every build, and staged next to Metal (snap7.dll on Windows or libsnap7.so on Linux), just like FOCAS.
The checked-in Windows library is 32-bit and uses the Snap7 __stdcall ABI. Metal selects that ABI automatically for i686-pc-windows-gnu; do not substitute a 64-bit snap7.dll in a 32-bit Metal deployment.
{
"id": "s7-mill-01",
"host": "192.168.1.80",
"kind": "siemens_snap7",
"snap7": {
"rack": 0,
"slot": 1,
"profile": "custom",
"channel": 1,
"override_encoding": "unknown",
"db": 0,
"start": 0,
"size": 0,
"fields": [
{ "field": "status_status_code", "db": 21, "byte": 35, "offset": 0, "data_type": "bool", "bit": 0 },
{ "field": "spindle_speed", "db": 50, "byte": 12, "offset": 0, "data_type": "real" },
{ "field": "position_x", "db": 60, "byte": 6, "offset": 0, "data_type": "real", "scale": "0.001" },
{ "field": "program_name", "db": 90, "byte": 10, "offset": 0, "data_type": "string", "length": 22 }
]
}
}
Supported data_type values are bool, u16, i16, u32, i32, real, and string. byte is an absolute offset in the field's db; legacy fields without db and byte still use the top-level DB range and relative offset. Multi-byte values are decoded in Siemens big-endian format. An S7 string length includes its two-byte S7 STRING header. Numeric scale is represented as a JSON string to retain exact, configuration-safe comparisons.
Versioning releases
Cargo installs a pre-commit hook through cargo-husky. It opens a
cross-platform Python TUI to choose a patch, minor, major, or custom SemVer
version, then keeps the Rust crate, Python package, and Metal Manager manifests
in sync. Use
METAL_SKIP_VERSION_BUMP=1 git commit ... for a commit that should not create
a release version. Run python -m pip install -r scripts/requirements-dev.txt and
cargo test --no-run once after cloning to install and use the hook.
After committing a release version, push a matching v<version> tag. The
Windows workflows include that version in their run names, while release asset
names remain stable for the updater and proxy. Before a normal branch push, the
pre-push hook asks whether to create and push that tag; confirm to trigger a
new release. It never prompts again when the hook itself pushes the tag.
Prerequisites
Vendor libraries (git submodules)
git submodule update --init --recursive
Profiles
| Profile | Purpose |
|---|---|
release |
Optimized (opt-level=3, thin LTO, strip debuginfo) |
testing |
Dev-based, opt-level=1, full debug info — for quicker Windows test builds |
Deploy / runtime files
build.rs stages vendor runtime files next to the binary after every build
(skips files that are already up to date). Disable with METAL_SKIP_RUNTIME_COPY=1.
Build is only supported on Windows(32-bit) and Linux(32-bit)
Windows (cargo win-build)
target/i686-pc-windows-gnu/release/
metal.exe
smoke.exe ← standalone CNC connectivity, browse, and telemetry diagnostics
Fwlib32.dll (+ all vendor/fwlib/*.dll)
EZSocketNc.dll
EZNcAut*.dll
snap7.dll ← required Siemens S7 / Snap7 runtime
CommServer\ ← ncapi32.dll etc. (required by Open2)
Parameter\
The published Windows release ZIP includes both metal.exe and smoke.exe.
Ship the whole release folder so the executables share the required DLLs and runtime directories.
For Mitsubishi connections, EZSocketNc.dll must be registered as a COM server.
Register it once during deployment from an elevated Command Prompt. The service
does not need, and should not attempt, to register the DLL on every startup.
Use 32-bit regsvr32 for this 32-bit executable, even on 64-bit Windows:
regsvr32.exe "EZSocketNc.dll"
Repeat registration only after replacing or moving the DLL. The registered DLL
path, metal.exe, and the full CommServer\ runtime must remain deployed.
Linux (cargo build --release)
target/release/
metal
libfwlib32-linux-x64.so.1.0.5
libfwlib32.so -> …
libfwlib32.so.1 -> …
libsnap7.so ← required Siemens S7 / Snap7 runtime
The binary is linked with $ORIGIN rpath so it finds FOCAS next to itself.
Run
All modes are selected with environment variables. Defaults are shown below.
Environment variables
Siemens Snap7 smoke
| Variable | Default | Description |
|---|---|---|
MACHINE_HOST |
(required) | PLC IP address or hostname |
SNAP7_MAPPING_JSON |
(required) | Exact Snap7 configuration, including profile and/or per-field DB addresses |
SNAP7_SMOKE_SAMPLES |
5 |
Number of live samples; minimum 2 |
SNAP7_SMOKE_INTERVAL_MS |
500 |
Delay between samples |
SNAP7_EXPECT_JSON |
(none) | Normalized values that every sample must match |
SNAP7_REQUIRE_CHANGE |
(none) | Comma-separated fields which must change during the smoke run |
SNAP7_REQUIRE_VALUES_JSON |
(none) | Field-to-array values which must all be observed, e.g. {"status_status_code":["run","stop"]} |
SNAP7_FLOAT_TOLERANCE |
0.000001 |
Numeric comparison tolerance for expected values |
MQTT service and demo
| Variable | Default | Description |
|---|---|---|
MQTT_HOST |
localhost |
Broker host |
MQTT_PORT |
1883 |
Broker port |
MQTT_CLIENT_ID |
metal (service), metal-mqtt-smoke (demo) |
Client id; make it unique per running instance |
MQTT_POLL_MS |
200 ms (service), 100 ms (demo) |
Service telemetry publish interval. Metal republishes the latest completed CNC snapshot at this cadence; sampled_at_unix_ms identifies the source snapshot. In the smoke demo, this remains the MQTT event wait. Service rejects too-small/invalid values and uses 200 ms. |
COMPANY_NAME |
company_name |
MQTT smoke-demo namespace only; the service reads company_name from config.json |
METAL_CONFIG |
config.json in the working directory |
Local persisted fleet configuration path |
The service reads its MQTT settings once at startup. Restart it after changing these environment variables.
Smoke Tests
Connects and exercises identity, position, timers, spindle/servo reads, etc. Prints a pass/fail summary.
Fanuc (windows)
set MACHINE_KIND=fanuc
set MACHINE_HOST=192.168.1.10
set MACHINE_PORT=8193
smoke.exe
mitsubishi (windows)
set MACHINE_KIND=mitsubishi
set MACHINE_HOST=192.168.1.20
set MACHINE_PORT=683
set MACHINE_TYPE=m800m
smoke.exe
MACHINE_PORT is used for TCP (default 683). Deploy the full EzSocket tree — Open2 needs CommServer\ (e.g. M700\ncapi32.dll), not only EZSocketNc.dll.
mqtt transport (linux)
Does not need a CNC. Subscribes to config, acks changes, and publishes one
synthetic DataTemplate per machine ID to /<machine-id>. By default it
uses smoke-01 and smoke-02;.
# start a broker first (e.g. mosquitto). fails without it
MQTT_HOST=localhost \
MQTT_PORT=1883 \
metal.exe
Fleet configuration
<company_name>/config carries the entire desired machine fleet by default.
Set company_name in the local config.json before starting Metal, because it
selects the initial MQTT subscription namespace. It cannot change at runtime;
restart Metal after changing it. Each machine needs a unique,
stable id; it is included as machine_id in every JSON report and forms the
telemetry topic <company_name>/<machine-id>. IDs must be a single MQTT
topic segment: no /, +, or #.
{
"company_name": "amarjay",
"machines": [
{ "id": "lathe-01", "host": "192.168.1.10", "port": 8193, "kind": "fanuc" },
{
"id": "mill-02",
"host": "192.168.1.20",
"port": 683,
"kind": "mitsubishi"
}
]
}
An empty machines list deliberately stops all CNC workers.
<company_name>/config/ack returns JSON with an acceptance result. An update
is accepted only after it has been persisted and applied by the worker
supervisor. Malformed or invalid configurations are rejected with an error. The older
single-machine JSON shape remains readable from existing local config files,
but fleet snapshots are recommended for MQTT updates.
example config publish
mosquitto_pub -h localhost -p 1883 -t amarjay/config -m '{"company_name":"amarjay","machines":[{"id":"mill-02","host":"192.168.1.20","port":683,"kind":"mitsubishi"}]}'
Operational behavior
Each configured machine has an independent driver thread, so an unavailable CNC
does not stop collection from the rest of the fleet. The driver emits a
/data report with connection: "reconnecting" before attempting a connection.
On failure, it emits connection: "disconnected", status_status_code: "stop",
and an error string, then waits three seconds before trying again. This is a
fixed delay, not exponential backoff; the effective retry period is the failed
connection attempt duration plus three seconds. Fanuc attempts may themselves
take up to ten seconds.
The MQTT broker has different behavior: if its connection closes, the MQTT thread logs its exit and does not currently reconnect. CNC drivers continue sampling, but configuration updates and telemetry publishing stop until the Metal process is restarted. The in-memory telemetry channel holds up to 256 reports; once full, newer stale reports are dropped rather than blocking CNC driver.
Windows startup and logs
Run Metal with its working directory set to the deployment folder so the default
config.json resolves beside metal.exe. Metal creates and appends its own
diagnostic files; no shell redirection is required:
logs\metal.log parent: config, MQTT, supervision, panics
logs\worker-<machine-id>.log one file per CNC: driver and EzSocket/FOCAS errors
Records are emitted by tracing-subscriber and include a timestamp, severity,
process role, and thread metadata. Set RUST_LOG (for example,
RUST_LOG=metal=debug) to adjust verbosity; the default is info. Worker logs
are intentionally separate, so simultaneous Mitsubishi
Open2 failures do not overwrite or interleave each other. Copy the complete
logs\ directory when reporting a fault.
To start automatically, create a Windows Task Scheduler task that runs the
launcher at logon (or at system startup) and set its Start in directory to
the Metal deployment directory. Register EZSocketNc.dll during installation,
not from that scheduled task.
Notes
- Bitness: the Windows binary is 32-bit (
i686). Pair it with 32-bit FOCAS / EZSocket DLLs from the vendor trees. - EzSocket on non-Windows: the API surface compiles, but every call returns
UnsupportedPlatform. - Smoke exit codes: hard
FAIL→ exit1; optional APIs useSKIP(do not fail the suite). - Still issue with automatic restart and graceful handling of subworkers. these need proper handling, rather than os.exit()