IQVizyon Metal
  • Rust 78.6%
  • TypeScript 18.4%
  • Python 1.8%
  • CSS 0.6%
  • Shell 0.3%
  • Other 0.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-25 14:49:09 +03:00
.cargo smoke tests for siemens and dmg mori 2026-08-21 17:58:17 +03:00
.cargo-husky/hooks fixing warnings 2026-08-24 15:55:50 +03:00
.github/workflows tool fix 2026-09-15 16:10:42 +03:00
apps siemens snap7 holdup fixed 2026-09-16 12:50:46 +03:00
docs/client metal client guide 2026-09-11 14:36:33 +03:00
examples dmg mori added 2026-08-18 15:28:14 +03:00
node/iqvmetal siemsn nomenclature contrastive identities split 2026-08-24 17:45:16 +03:00
python/iqvmetal tool fix 2026-09-15 16:10:42 +03:00
scripts updated publish hook 2026-09-01 16:06:02 +03:00
src runner update 2026-09-25 14:49:09 +03:00
vendor readding version 2026-09-01 19:11:10 +03:00
.gitignore - abstracted some key parts into seperate compoment / hooks. 2026-09-08 15:01:50 +03:00
.gitmodules readding version 2026-09-01 19:11:10 +03:00
build.rs Implemented the Siemens native S7 refactor. 2026-08-28 15:20:43 +03:00
Cargo.lock tool fix 2026-09-15 16:10:42 +03:00
Cargo.toml tool fix 2026-09-15 16:10:42 +03:00
config.json add MTConnect device to sample config 2026-08-14 13:00:52 +03:00
pyproject.toml tool fix 2026-09-15 16:10:42 +03:00
README.md simple change 2026-09-09 14:59:38 +03:00

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)

  1. Loads / creates config.json (METAL_CONFIG overrides path).
  2. Connects MQTT (MQTT_HOST / MQTT_PORT / MQTT_CLIENT_ID).
  3. Subscribes the startup {company_name}/config, publishes {company_name}/<machine-id>, and acknowledges on {company_name}/config/ack by default.
  4. 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 → exit 1; optional APIs use SKIP (do not fail the suite).
  • Still issue with automatic restart and graceful handling of subworkers. these need proper handling, rather than os.exit()