Industrial-AIOps

Governed OT data tap: 14 field protocols + cross-protocol RCA brain; audit + MOC gating.

OtherPythonv0.28.0

Industrial-AIOps

English · 中文

Ask an AI agent why the line stopped — and get an answer that cites its evidence.

A vendor-neutral, read-first data tap for the factory floor. It speaks 14 field protocols, correlates what it reads across them, and hands your agent an evidence-cited verdict instead of a guess. Every call is audited, and no reading ever phones home.

pip install "iaiops[opcua]"      # pick your protocol — or [all]
iaiops init                      # write ~/.iaiops/config.yaml
iaiops doctor                    # check the setup before you trust it

Prefer a container? The published image is cosign-signed and runs non-root. It speaks MCP over stdio, so keep stdin open and mount a volume for the audit store:

cosign verify --key deploy/margo/cosign.pub ghcr.io/industrial-aiops/iaiops:0.28.0-factory
docker run -i --rm -v iaiops-state:/home/iaiops/.iaiops \
  ghcr.io/industrial-aiops/iaiops:0.28.0-factory

For a hardened or air-gapped deployment (read-only rootfs, cap_drop: ALL, no-new-privileges, optional on-box LLM) use deploy/margo/compose.yaml and deploy/airgap/. The analysis engine needs no GPU and no model API — it is deterministic; an LLM is optional and only phrases the verdict.

What you get

ReadsOPC-UA (+ Historical Access, tag auto-discovery) · Modbus TCP/RTU · S7comm · Mitsubishi MC · Omron FINS · MTConnect · MQTT/Sparkplug B · EtherNet/IP · EtherCAT · PROFINET · SECS/GEM · HART-IP · BACnet/IP · IO-Link — plus read-only REST layers for BAS supervisors (Metasys / Niagara) and Ignition Gateway
Figures outdowntime root cause (the flagship copilot), alarm floods (ISA-18.2), broken dataflows, data trustworthiness, OEE, asset inventory, legacy PLC program explainer (ST/AWL/L5X)
Governsaudit · budget · risk-tier · undo — on every call, through one engine, from both MCP and CLI
Stays yoursno telemetry, no phone-home. Six tools can send data off-box by design (stream_publish, stream_publish_event, uns_publish, historian_push, mqtt_publish, rca_narrate) — IAIOPS_NO_EGRESS=1 withholds all six for an air-gapped posture

Ten per-industry editions ship in this package — fab · factory · process · building · water · warehouse · clinical · pharma · renewables · plcnext — each adding its own read-only advisory checks. Substation / utility telecontrol (IEC-104 · DNP3 · IEC-61850) ships separately as iaiops-energy.

Your first five minutes

Four commands. Only one of them touches a device, and it prints what it will send before it sends anything.

pip install "iaiops[modbus]"     # pick the protocol you actually have, or [all]
iaiops onboard status            # ← run this first. Contacts NOTHING.
iaiops doctor                    # config, secrets, reachability — and the version
iaiops readiness                 # every scenario, and what each gap needs

onboard status answers the smaller questions you have first: which journey is this site on — reading its devices directly, or subscribing to a UNS broker its data already flows through — which step of it are you on, and what is the one command that advances it? It is derived from your store and config.yaml every time. The one thing it keeps is a namespace audit verdict, and that counts only for the broker and topic filter it was taken on and always shows its age — so edit config.yaml by hand and the answer stays true.

readiness reads your config and local store and answers one question: which scenarios can this site run today, and what does each gap need? Every gap comes with the command that closes it, ranked by how much it unlocks. No agent, no cloud, no account, and nothing on the wire.

Then the path, in the order that matters — survey what is there, take a bounded sample, and only then explain it:

contacts a device?
Surveyiaiops scan plan → iaiops scan runpreview sends nothing; the run itemises every packet class it sent
Configureiaiops onboard draft → you merge it into config.yamlno — it reads the stored scan, and writes nothing
Tapiaiops collect run line1 --duration 7dyes — and it reports what it saw and what it missed
Declareiaiops tags export → a person fills in role → iaiops tags apply --by <you>no — the role column comes out empty on purpose
Explainiaiops oee measure --since … --until … · iaiops investigate open · iaiops diag rcano — all over collected history

See the whole thing run against a real device in about two minutes, including a genuine mid-run outage, with ./demo/oee-line/run_demo.sh — no hardware, no configuration, nothing written outside a temporary directory. demo/oee-line/ explains what each step is for and what the numbers do and do not claim.

Why read-first

OT is exactly where you want an agent on a tight leash. The read paths are the product; the few write paths are OT-dangerous, off by default, and gated by MOC discipline — dry-run, one-shot approval, undo capture, hash-chained audit.

Proving the analysis needs no model

The analysis layers cannot reach a language model. That is a guard, not a slogan: tests/test_brain_is_llm_free.py scans eight packages — brain, discovery, runtime, readiness, collect, knowledge, retain, connectors — for any import that could reach one, and an empty result is the guarantee. A model is used in exactly two places, and neither is load-bearing: rca_narrate rephrases a verdict that was already computed and already cited, and an agent front-end decides which tool to call. Remove both and the numbers are the same numbers.

That guard is static — it proves nothing can call a model. For a validation team the sentence they are asked to accept is the executed one, so it is executed:

iaiops verify determinism --out determinism-record.json

A pinned in-repo dataset goes through availability, production counts, the Six Big Losses, ISA-18.2 alarm load, control charts, the conservative baseline and the RCA copilot. Each result is canonically encoded and digested; the suite runs twice in this process and once in each of two fresh interpreters started at different PYTHONHASHSEED values — the arm that catches a set or dict iteration order reaching a result, which a single run never can. The socket API raises throughout, so a computation that reached for a device or a hostname fails here instead of quietly working on a machine that happened to be online. Afterwards the run is asked what it pulled in: a model library that was already loaded (an MCP server holds iaiops.core.llm for the opt-in narration tool) is recorded, not judged — only what the suite itself imported can condemn it.

The record separates result (identical every run — the part to sign) from context (when and where this run happened). Two good runs are not byte-identical records, and someone will diff them, so the halves are named rather than mixed.

This is the form the claim has to take to be usable: not "our model is accurate", which is not evidence in a GxP context, but a test case someone can write into an IQ/OQ protocol — remove the model, block the network, re-run the standard dataset, compare the hash — execute, and sign. verify_determinism is the same check from the MCP side; iaiops verify suite lists what it covers without running it.

How far it's actually been verified

Short version: verified against real protocol libraries, containers and in-process servers — not yet against real plant gear. We grade evidence rather than saying "tested", because a real container round-trip and a synthetic fixture are not the same claim.

RungWho judges our framesProtocols at this rung today
2a — real wire to a third-party serversomebody else's implementation of the specOPC-UA (Microsoft opc-plc; certificate trust enforced both ways) · Modbus TCP (pymodbus) · Modbus RTU (pymodbus over a socat PTY) · MQTT / Sparkplug B (a real mosquitto broker) · BACnet/IP (bacpypes3, two IPs on one subnet) · MTConnect (the Institute's own cppagent) · SECS/GEM (secsgem equipment side) · EtherNet/IP (identification)
2b — real wire to a server we wrote from the specthe third-party client parses our framesS7comm (pyS7) · Mitsubishi MC (pymcprotocol) · EtherNet/IP (tag layer — pycomm3, all three driver routes) · PROFINET-DCP
2c — real wire, but both ends are oursnobody independentOmron FINS · IO-Link · HART-IP (codec itself at rung 1, against hart-protocol)
mock only — protocol logic exercised, nothing on the wire—EtherCAT · BAS (Metasys / Niagara) · Ignition gateway
3 — real physical / vendor devicethe devicezero, for every protocol

Each row's detail — and, per protocol, what is still not covered — is in docs/VERIFICATION-RECORD.md. That file is the record; this table is its summary, and a test fails if the two disagree.

Per-protocol evidence — including what each test does not cover — is in docs/VERIFICATION-RECORD.md, one row per protocol, naming the test behind each claim. Every 待核实 is hardware-gated, not forgotten — each one names the equipment that would settle it.

🧪 测试与共创 / Beta testing & co-creation

我们在找现场测试伙伴。 软件里能验证的我们都验证了(真实 in-process 服务器、真实协议库、Docker 容器 loopback)——剩下的 待核实 清单只有真设备能回答:物理 Modbus-RTU(RS-485)、EtherCAT 从站、HART 网关、在线 BACnet 楼宇设备、在线 Metasys/Niagara BAS 控制器、在线 Ignition 网关、国产 PLC(汇川/信捷)、真机 PLCnext、真实变电站 RTU/IED、欧姆龙 FINS 真机、IO-Link 主站。如果你是 OT 工程师、系统集成商或工厂团队,手上有任何这类设备:装上 iaiops,对你的设备跑一遍 iaiops doctor,把结果告诉我们。经你验证的设备会署名写进支持矩阵;现场反馈的问题我们优先分诊;功能可以通过 GitHub Issues/Discussions 直接共创。

We're looking for field-testing partners. Everything software-verifiable has been verified; what's left on the honest 待核实 list only real equipment can answer — physical Modbus-RTU (RS-485), EtherCAT slaves, HART gateways, live BACnet HVAC, live Metasys/Niagara BAS controllers, live Ignition gateway, domestic PLCs (Inovance/Xinje), live PLCnext, substation RTUs/IEDs, live Omron FINS PLCs, IO-Link masters. If you're an OT engineer, integrator, or factory team with access to any of these: install iaiops, run iaiops doctor against your gear, and tell us what happened. Verified-equipment reports get credited in the support matrix, field-reported issues get fast triage, and features are co-designed in the open via GitHub Issues/Discussions.

👉 参与入口 | Start here: open an issue with the protocol and device model in the title, or email zhouwei008@gmail.com. Either reaches a person, and a report gets answered against the current release.


Consolidated capability matrix

ProtocolToolOperationR/Wrisk_tierReturns (key fields)
OPC-UAopcua_server_infoserver statusRlowstate, product_name, namespaces
OPC-UAopcua_browsebrowse node treeRlow[{node_id, browse_name, depth}]
OPC-UAopcua_read_noderead one nodeRlowvalue, datatype, source_timestamp, good
OPC-UAopcua_read_manybatch readRlow[{node_id, value, ...}]
OPC-UAopcua_subscribe_samplebounded sampleRlow{collected, samples[]}
OPC-UAopcua_read_alarmsalarm surfacingRlow{active_alarms[], active_count}
OPC-UAopcua_read_historyHistorical Access (HDA)Rlow{supported, count, values[]}
OPC-UAopcua_diagnose_connectionconnection triageRlow{verdict, checks[]}
OPC-UAopcua_discover_tagstag auto-discovery → semantic asset modelRlow{tag_count, assets[], naming_report}
OPC-UAopcua_health_summarythreshold classify (was health_summary¹)Rlow{overall, counts, offenders[]}
OPC-UAopcua_anomaly_scanstddev outliers (was anomaly_scan¹)Rlow{mean, stddev, outliers[]}
Modbusmodbus_read_holdingFC03Rlow{raw_registers, decoded[]}
Modbusmodbus_read_inputFC04Rlow{raw_registers, decoded[]}
Modbusmodbus_read_coilsFC01Rlow{bits[]}
Modbusmodbus_read_discreteFC02Rlow{bits[]}
Modbusmodbus_detect_byte_orderbyte/word-order auto-detectRlow{best_order, candidates[]}
Modbusmodbus_list_templatesvendor register templatesRlow{templates[]}
Modbusmodbus_apply_templatedecode block via templateRlow{values:{name: engineering_value}}
Modbusmodbus_health_summarythreshold classifyRlow{overall, counts, offenders[]}
S7comms7_cpu_infoCPU id + run/stopRlow{cpu_status, cpu_info}
S7comms7_read_arearead DB/M/I/QRlow{items:[{address, value}]}
S7comms7_read_dbread data blockRlow{items:[{address, value}]}
S7comms7_read_manybatch addressesRlow{items:[{address, value}]}
S7comms7_write_dbwrite data blockWhigh/MOC{before, written, _undo_id}
Mitsubishi MCmc_cpu_statusCPU typeRlow{cpu_type, cpu_code}
Mitsubishi MCmc_read_wordsword devicesRlow{words[]}
Mitsubishi MCmc_read_bitsbit devicesRlow{bits[]}
Mitsubishi MCmc_read_manyrandom readRlow{words[], dwords[]}
Mitsubishi MCmc_write_wordswrite wordsWhigh/MOC{before, written, _undo_id}
Omron FINSfins_cpu_infocontroller data read (0501)Rlow{controller_model, controller_version}
Omron FINSfins_cpu_statuscontroller status (0601)Rlow{run_mode, status}
Omron FINSfins_read_wordsmemory-area word read (DM/CIO/W/H/A/EM)Rlow{words[]}
Omron FINSfins_read_bitsmemory-area bit readRlow{bits[]}
Omron FINSfins_read_manybatch readsRlow{items[]}
Omron FINSfins_write_wordsmemory-area writeWhigh/MOC{before, written, _undo_id}
MTConnectmtconnect_probedevice modelRlow{devices:[{components:[{data_items}]}]}
MTConnectmtconnect_currentlatest valuesRlow{observations[]}
MTConnectmtconnect_samplebounded streamRlow{observations[]}
MTConnectmtconnect_assetsassetsRlow{assets[]}
MTConnectmtconnect_oee_snapshotOEE inputsRlow{availability, execution, verdict}
MQTT/Sparkplugmqtt_read_topicbounded readRlow{messages:[{topic, payload}]}
MQTT/Sparkplugsparkplug_subscribe_samplebounded SpB sample (full decode)Rlow{samples:[{sparkplug, payload:{metrics[]}}], seq_gap_count}
MQTT/Sparkplugsparkplug_decode_payloaddecode raw SpB payloadRlow{metrics:[{name, alias, datatype, value, is_historical}]}
MQTT/Sparkplugsparkplug_node_listnode discovery + stateRlow{nodes:[{group_id, edge_node_id, online, devices}], primary_hosts[]}
MQTT/Sparkpluguns_browsetopic-tree browseRlow{topics[], tree{}}
MQTT/Sparkpluguns_topic_auditUNS naming + sprawl governanceRlow{verdict, sprawl_findings, findings{casing_collisions[], scattered_leaves[], …}}
MQTT/Sparkpluguns_schema_driftSparkplug schema-drift (baseline vs current)Rlow{verdict (none/additive/breaking), node_changes[]}
MQTT/Sparkpluguns_live_auditlive UNS audit (bounded broker sample)Rlow{verdict, findings{}}
MQTT/Sparkplugsparkplug_live_schemalive NBIRTH schema snapshotRlow{nodes[], metrics[]}
MQTT/Sparkpluguns_live_driftlive drift vs stored baselineRlow{verdict, node_changes[]}
MQTT/Sparkplugmqtt_publishpublish/commandWhigh/MOC{published_bytes, applied}
EtherNet/IPeip_controller_infoLogix controller idRlow{controller:{vendor, product_name, revision, serial}}
EtherNet/IPeip_list_tagstag discoveryRlow{tag_count, tags:[{name, data_type, structure}]}
EtherNet/IPeip_read_tagread one tag/arrayRlow{tag, value, type, good}
EtherNet/IPeip_read_manybatch readRlow{items:[{tag, value, type}]}
EtherNet/IPeip_write_tagwrite tagWhigh/MOC{before, written, _undo_id}
Diagnosticsdiagnose_dataflowlocalize no-dataRlow{verdict, diagnosis, hops[]}
Diagnosticsalarm_bad_actorsISA-18.2 floodRlow{flood_verdict, top_offenders[]}
Diagnosticstag_healthoffender rankingRlow{overall, offenders[]}
Diagnosticshistorian_healthgap/flatlineRlow{verdict, gaps[]}
Diagnosticssubscription_healthsequenced-feed loss/reorder/overloadRlow{verdict, missed_count, overloaded_channels[]}
Diagnosticsdowntime_root_causeAI downtime RCA copilot (cited, advisory)Rlow{verdict, primary_cause, hypotheses:[{cause, confidence, evidence[]}]}
Diagnosticsdowntime_root_cause_liveRCA copilot that gathers its own live evidenceRlow{…downtime_root_cause…, collected_evidence}
Diagnosticslearn_cause_weightslearn per-site RCA cause weights from labeled incidentsRlow{cause_weights{}, rationale}
Diagnosticsdata_quality_scorecardfleet data-trust rollupRlow{fleet_score, fleet_status, issue_breakdown, worst_tags[], endpoints[]}
Diagnosticsdata_quality_fleet_rollupcross-endpoint fleet viewRlow{fleet_score, endpoints[]}
Diagnosticsheartbeat_healthheartbeat/watchdog livenessRlow{alive, distinct_transitions, longest_stall_s, reason}
Alarm (ISA-18.2)alarm_flood_analysisflood episodes / chattering / stale / summaryRlow{episodes[], chattering[], stale[], summary{}}
Alarm (ISA-18.2)alarm_rationalization_worksheetCSV-exportable rationalization rowsRlow{rows[], csv_path?}
Baselinebaseline_learnconservative change-log baseline (refuses thin history)Rlow{band{p1,p99,median,mad} | insufficient_data}
Baselinebaseline_checksilent-by-default violation checkRlow{status, violations[] (cited)}
Baselinebaseline_record_changerecord operator change (restarts learning)Rlow{recorded, change_point}
Baselinebaseline_statusno_baseline / learning / ok / violationRlow{status, window}
Historianhistorian_queryread history back out of sqlite/TDengine/IoTDBRlow{rows[], truncated}
Historianhistorian_coverageper-tag row counts + first/last tsRlow{tags:[{tag, rows, first, last}]}
PLC programplc_program_outlinestructure of exported ST/AWL/L5X programRlow{blocks[], call_graph, timers[]}
PLC programplc_program_xrefsymbol/address cross-reference (cited lines)Rlow{sites:[{kind, source_file, line, quote}]}
PLC programplc_program_sectionone named block's source (≤200 lines)Rlow{text, source_file}
Exportexport_dataexport local store → CSV/SQLite/ParquetRlow{path, row_count, preview[]}
Analyticsoee_computeOEE = A×P×QRlow{availability, performance, quality, oee, oee_pct}
Analyticsdowntime_eventsstoppage detect + categorizeRlow{event_count, total_downtime_s, by_category, events[]}
Analyticsoee_multidimOEE machine×part×shiftRlow{matrix[], worst_performers[], mean_oee}
Analyticsasset_inventoryactive fingerprintRlow{assets:[{protocol, vendor, model, firmware, reachable}]}
Analyticscross_protocol_asset_modelmerge discovered tags into one asset modelRlow{assets[], tag_count}
Analyticsadopt_alias_map / diff_alias_maptag alias-map adopt/diffRlow{aliases{}, changes[]}
Analyticsmonitor_changesbounded change-of-valueRlow{change_count, changes:[{value, previous, wall_clock}]}
EtherCATethercat_master_statemaster/WKC + slave countRlow{master_state, expected_working_counter, slaves_found, slaves_expected}
EtherCATethercat_slavesbus scanRlow{slave_count, slaves:[{index, name, vendor_id, product_code, state}]}
EtherCATethercat_slave_infoslave detailRlow{sync_managers[], fmmus[], object_dictionary[], input_bytes}
EtherCATethercat_read_sdoCoE SDO uploadRlow{index, byte_length, hex, as_uint}
EtherCATethercat_read_pdoinput PDO snapshotRlow{working_counter, input_hex, input_byte_length}
EtherCATethercat_write_sdoCoE SDO downloadWhigh/MOC{before, written, applied}
EtherCATethercat_set_stateAL-state transitionWhigh/MOC{before, requested, reached, applied}
PROFINETprofinet_discoverDCP IdentifyAll (segment-wide)Rlow{station_count, stations:[{name_of_station, mac, ip, vendor_id, device_roles[]}]}
PROFINETprofinet_identify_stationidentify by name-of-stationRlow{found, name_of_station, mac, ip, device_family}
PROFINETprofinet_station_paramstargeted DCP Get (by MAC)Rlow{found, name_of_station, ip, netmask, gateway}
PROFINETprofinet_asset_inventoryDCP asset registerRlow{asset_count, io_controller_count, assets[]}
PROFINETprofinet_dcp_setDCP Set (station name / IP suite)Whigh/MOC{before, applied, _undo_id}
SECS/GEMsecsgem_equipment_statusGEM link + identity (S1F1/F2)Rlow{communication_state, are_you_there}
SECS/GEMsecsgem_list_status_variablesSVID namelist (S1F11/F12)Rlow{count, status_variables[]}
SECS/GEMsecsgem_read_status_variablesSVID values (S1F3/F4)Rlow{svids, values[]}
SECS/GEMsecsgem_list_equipment_constantsECID namelist (S2F29/F30)Rlow{count, equipment_constants[]}
SECS/GEMsecsgem_read_equipment_constantsECID values (S2F13/F14)Rlow{ecids, values[]}
SECS/GEMsecsgem_list_alarmsalarm list (S5F5/F6)Rlow{count, alarms[]}
SECS/GEMsecsgem_list_process_programsPPID directory (S7F19/F20)Rlow{count, process_programs[]}
BACnet (building)bacnet_discoverWho-Is device discoveryRlow{device_count, devices:[{device_id, address}]}
BACnet (building)bacnet_object_lista device's objectsRlow{object_count, objects:[{object_type, instance}]}
BACnet (building)bacnet_read_propertyone object propertyRlow{object_type, instance, property, value}
BACnet (building)bacnet_read_pointsall present-values (HVAC snapshot)Rlow{point_count, points:[{object_type, instance, present_value}]}
BACnet (building)bacnet_cov_subscribebounded COV capture (always unsubscribes)Rlow{notifications[], terminated_reason}
BACnet (building)bacnet_read_trend_logTrendLog readRange (bounded)Rlow{records:[{timestamp, value}]}
BACnet (building)bacnet_write_propertypresent-value write (priority)Whigh/MOC{before, written, _undo_id}
HART-IP (process)hart_device_identitycmd 0 identityRlow{manufacturer, device_type, revision}
HART-IP (process)hart_primary_variablecmd 1 PVRlow{value, unit}
HART-IP (process)hart_dynamic_variablescmd 3 PV/SV/TV/QV + loop currentRlow{variables[], loop_current}
HART-IP (process)hart_burst_samplebounded burst-variable samplingRlow{samples[]}
IO-Linkiolink_master_infomaster identityRlow{vendor, product, serial}
IO-Linkiolink_ports≤32-port sweep (mode/status/device id)Rlow{ports[]}
IO-Linkiolink_device_infoper-port device identityRlow{vendor_id, device_id, product_name}
IO-Linkiolink_read_pdinprocess-data-in (raw hex + bytes)Rlow{hex, bytes[]}
IO-Linkiolink_read_isduISDU acyclic parameter readRlow{index, subindex, value}
IO-Linkiolink_scanmaster + all connected devicesRlow{master{}, devices[]}
BAS (Metasys/Niagara)bas_point_listsupervisory point directoryRlow{point_count, points:[{id, name, type}]}
BAS (Metasys/Niagara)bas_point_readread one supervisory pointRlow{point, value, unit, status}
BAS (Metasys/Niagara)bas_alarm_listactive controller alarmsRlow{alarm_count, alarms:[{id, priority, state}]}
BAS (Metasys/Niagara)bas_trend_readtrend/history samples (bounded)Rlow{records:[{timestamp, value}]}
BAS (Metasys/Niagara)bas_commandsupervisory command (default-OFF; life-safety object denylist refuses fire/smoke/egress/pressurization before any I/O)Whigh/MOC{before, written, _undo_id}
Ignitionignition_gateway_statusGateway + module healthRlow{state, version, modules:[{name, state}]}
Ignitionignition_tag_browsetag-tree browseRlow{tags[], tree{}}
Ignitionignition_tag_readcurrent tag valuesRlow{values:[{path, value, quality, timestamp}]}
Ignitionignition_alarm_statusactive alarmsRlow{alarm_count, alarms:[{path, priority, state}]}
Ignitionignition_tag_historytag-history query (bounded)Rlow{rows:[{path, timestamp, value}]}
信创 / compliancecompliance_mapping《工控网络安全防护指南》↔ iaiopsRlow{pillars[], status_summary, controls:[{pillar, status, gap}]}
信创 / compliancecompliance_frameworks等保 2.0 + IEC 62443 FR1–6 crosswalkRlow{controls:[{crosswalk}]}
信创 / compliancecompliance_dengbao_levels等保 二级 baseline vs 三级 增量Rlow{pillars:[{l2, l3_delta, status}]}
信创 / compliancecompliance_reportdeliverable compliance report (md/html)Rlow{markdown | out_path}
信创 / compliancecompliance_evidence_bundleaudit-evidence zip (hash-chain verified)Rlow{bundle_path, manifest}
信创 / historianhistorian_pushpush telemetry to sqlite/TDengine/IoTDBR(→historian)low{sink, received, written, skipped_non_numeric}
Selfprotocols_supportedcapability mapRlow{protocols[], diagnostics[], analytics[]}

(The energy protocols — IEC-104 / DNP3 / IEC-61850 — moved to iaiops-energy in 0.8.0; their tool matrix lives in that repo.)

196 governed tools = 183 read + 10 MOC-gated device writes + historian_push (a write, to a historian rather than to a device: [WRITE][risk=low]) + the 2 deprecated aliases below. The device writes are (s7_write_db, mc_write_words, fins_write_words, mqtt_publish, eip_write_tag, ethercat_write_sdo, ethercat_set_state, profinet_dcp_set, bacnet_write_property, bas_command). The read side now includes two vendor-REST read-only layers above the field protocols — a BAS controller layer (Metasys/Niagara, building edition) and an Ignition Gateway MES/SCADA layer (factory edition). ¹ The 2 deprecated aliases are the two deprecated brain aliases health_summary / anomaly_scan, renamed to opcua_health_summary / opcua_anomaly_scan in 0.10.0 — the deprecated aliases are still registered and will be removed in a future release (target: 1.0.0). Read-only per-edition tools load ONLY under their edition (see per-edition tool modules below), so a bare protocol / single-edition surface is smaller than this line-wide total. The table above is representative, not exhaustive; run protocols_supported() (or iaiops protocols) for the live map.


Per-protocol reference

OPC-UA

  • Versions/variants: binary opc.tcp:// via asyncua (sync facade). Security: anonymous + username/password, plus application-certificate message security (Sign / SignAndEncrypt) — set client_cert + client_key (+ optional server_cert) and the client opens a signed/encrypted secure channel (no cert ⇒ the anonymous / username path is unchanged). Validated end-to-end against an in-process asyncua server (tests/test_opcua_security.py) for Basic256Sha256 in both Sign and SignAndEncrypt modes: server_cert pinning and client-side server-cert auto-discovery are exercised, and the test asserts the negotiated policy URI + message-security mode on the live encrypted channel (plus a negative test that anonymous is refused by a secure-only server).
  • Connection params: endpoint_url, username (password encrypted), security_mode, security_policy; for cert security client_cert / client_key / optional server_cert (PEM or DER paths; aliases certfile / keyfile).
  • Alarms & Conditions: opcua_alarm_events — bounded event subscription + ConditionRefresh, events carry the server's own timestamps (verified against an in-process asyncua server; third-party A&C servers 待核实). Untimed fallback: opcua_read_alarms browses alarm-like boolean nodes.
  • Not supported / planned (待核实): cert-security interop with third-party / vendor servers (KEPServerEX / Prosys / Siemens / real PLCs), the other policies (Aes128Sha256RsaOaep / Aes256Sha256RsaPss / Basic128Rsa15 / Basic256), strict server-side certificate-trust enforcement, and cert-based user identity (X509 identity token, distinct from channel security).

Modbus-TCP / Modbus-RTU

  • Versions/variants: Modbus-TCP and Modbus-RTU (serial RS-485/232) via pymodbus (+ pyserial). Read function codes FC01 (coils), FC02 (discrete), FC03 (holding), FC04 (input). Write FCs (FC05/06/15/16) = not implemented (read-only).
  • Connection params: TCP — host, port (502), unit_id. RTU — transport: rtu, serial_port (e.g. /dev/ttyUSB0), baudrate, unit_id. Registers are untyped 16-bit words → decode hint (uint16/int16/uint32/int32/float32/raw); modbus_detect_byte_order auto-detects the byte/word order (AB/BA · ABCD/DCBA/BADC/CDAB) from hint values — pure logic, no extra device load.
  • Vendor register templates (modbus_list_templates / modbus_apply_template): named register maps decoding a block into engineering values — energy meters (Eastron SDM630, Schneider PM5xxx, Carlo Gavazzi EM24), PV inverters (Huawei SUN2000, Growatt), Phoenix PLCnext process data, and water-industry templates (E+H Promag, Hach SC controller, generic dosing pump). Each template carries an explicit 待核实 caveat — no invented "verified" addresses.
  • Coverage: many domestic 国产 PLCs (汇川 Inovance / 信捷 Xinje / 和利时 Hollysys / 台达 Delta) and any Modbus vendor. RTU framing is live-verified over a real serial link (socat PTY + pymodbus RTU server); specific physical RS-485 devices stay 待核实.

S7comm (Siemens + 仿西门子 国产)

  • Versions/variants: pyS7 (pure-Python, ISO-on-TCP / RFC1006 — no native libsnap7). S7-300/400/1200/1500 and compatible clones. Memory areas DB / M (merker) / I / Q. No protocol auth (CPU gates via "Permit access with PUT/GET").
  • Connection params: host, port (102), rack, slot (0/1 for 1200/1500; 0/2 common for 300/400).
  • Write: s7_write_db = high risk_tier, MOC, dry-run default, captures BEFORE value + undo.
  • Not supported / planned: optimized/symbolic DB access on 1500 with "optimized block access" can require absolute-addressing config on the CPU.

Mitsubishi MC

  • Versions/variants: pymcprotocol — MC 3E frame (binary) only. 1E / 4E frames = not supported. PLC types Q / L / QnA / iQ-R / iQ-L. Devices: D/W/R (word), M/X/Y/B (bit).
  • Connection params: host, port (5007 default; set to the module's open MC port), plctype.
  • Write: mc_write_words = high/MOC/dry-run default, captures BEFORE + undo.

Omron FINS (CS/CJ/CP/NX-via-FINS)

  • Versions/variants: in-repo, stdlib-only FINS client (no third-party dependency — the iaiops[fins] extra pins nothing): 10-byte FINS header framing, FINS/UDP (default port 9600) and FINS/TCP (node-address handshake per Omron W342), SID matching, bounded response parsing, end-code table per W227/W342. Commands: 0101 memory-area read (words/bits over DM/CIO/W/H/A/EM), 0102 write, 0501 controller data read, 0601 controller status.
  • Connection params: host, port (9600), transport (udp default / tcp), FINS network/node/unit addressing.
  • Write: fins_write_words = high/MOC/dry-run default, captures BEFORE + undo; CLI double-confirm on --apply.
  • Validation: verified against an in-repo mock FINS UDP/TCP responder (tests/test_fins.py); live Omron PLC behaviour and banked-EM access stay 待核实.

IO-Link (master JSON integration — read-only)

  • Versions/variants: sensor-level visibility via the IO-Link master's HTTP/JSON interface (IO-Link consortium "JSON Integration"), both dialects selectable per endpoint via flavor: — iotcore (ifm IoT-Core POST envelope, default) and rest (plain-REST GET, Balluff/Turck-style). Reads: master identity, bounded ≤32-port sweep, per-port device identity, process-data-in (raw hex + bytes), ISDU acyclic parameter read. NO write tools. Bounded/size-capped HTTP (256 KiB response cap), schema-checked JSON with teaching errors. Reuses the MTConnect HTTP pin (iaiops[iolink] → requests).
  • Connection params: master host/URL, flavor, timeout_s. protocol: iolink.
  • Validation: in-process mock master in both flavors (tests/test_iolink.py); live master datapoint paths stay 待核实.

HART-IP (process instrumentation — read-only)

  • Versions/variants: HART universal commands over HART-IP UDP (default, port 5094) or TCP (transport: tcp, length-delimited framing) via an in-tree transport; the HART command codec is verified vs hart-protocol. Tools: hart_device_identity (cmd 0), hart_primary_variable (cmd 1), hart_dynamic_variables (cmd 3, PV/SV/TV/QV + loop current), hart_burst_sample (bounded sampling of burst-published variables). No write / device-specific commands exposed (OT-dangerous on live instruments).
  • Connection params: host (HART-IP server/gateway), port (5094), transport (udp default / tcp).
  • Validation: TCP transport loopback-verified (in-process HART-IP server, real long-frame ACK through the real codec path); live gateway behaviour and a true unsolicited burst subscription stay 待核实.

MTConnect (ALL CNC machine tools)

  • Versions/variants: agent REST + XML (requests + xml.etree), namespace-agnostic (parses MTConnect 1.x Devices/Streams/Assets schemas). Endpoints: /probe, /current, /sample, /assets. Read-only by specification. XML parsing is hardened (DTD/entity declarations rejected — XXE/billion-laughs defense).
  • Connection params: agent_url (e.g. http://host:5000).
  • Not supported / planned: MTConnect streaming (long-poll interval=); only bounded count= samples.

MQTT / Sparkplug B / UNS

  • Versions/variants: paho-mqtt — MQTT 3.1.1 & 5. Sparkplug B topic convention spBv1.0/{group}/{type}/{edge}/[device] (NBIRTH/DBIRTH/NDATA/DDATA/NDEATH/DDEATH/STATE). TLS + username/password supported.
  • Full Sparkplug B decode (no optional extra): payloads are protobuf-decoded with a vendored, byte-for-byte copy of the official Eclipse Tahu sparkplug_b.proto generated module (depends only on protobuf). Per metric you get name, alias (resolved to its name via the BIRTH model), datatype (Int8…Int64/UInt…/Float/Double/Boolean/String/DateTime/Text/UUID/DataSet/Bytes/File/Template/PropertySet…), value, timestamp, and the is_historical / is_null flags. A birth/death + seq model tracks node/device online state (NBIRTH/DBIRTH ↔ NDEATH/DDEATH), builds the alias→name map from BIRTH, applies NDATA/DDATA by alias, and flags seq gaps / out-of-order. Primary-host awareness: STATE/<host_id> topics surface in sparkplug_node_list. sparkplug_decode_payload decodes a single raw payload (base64/hex) offline.
  • A data SOURCE, not just a bus (new): MQTT/Sparkplug is collectable — iaiops collect run holds ONE subscription open for the run and samples from its last-value cache, so a plant whose data is already unified into a UNS can be tapped without touching a PLC. Refusals are the feature. MQTT is pushed, and the obvious cache answers with the last value forever after a publisher dies — availability then reads 100% on a stopped line, in every subscriber at once. So a point is a reading only when it has been seen at all (never-published is not zero), is fresher than stale_after_s, and — for Sparkplug — its node has not sent NDEATH/DDEATH. Anything else raises OTNoReadingError, which the collector records as a gap (collection was blind), never as downtime.
  • stale_after_s is REQUIRED to collect, and deliberately has no default: a value that stopped updating and one that is simply constant are identical on the wire, so only the site knows how often a point is published. Guessing it here would put a guess underneath every availability figure. Same discipline as running_when on a run_state tag.
  • The two guarantees are not equal, and the endpoint is told which it has: Sparkplug gives staleness and death (the broker publishes the node's Last Will); plain MQTT has no death signal at all, so only staleness is available there and a quiet topic is indistinguishable from a dead publisher.
  • Refs: a plain-MQTT ref is the topic itself; a Sparkplug ref is group/edge[/device]:metric — exactly the node id mqtt live-schema reports, so its output is usable as config refs without translation. Payloads decode as a bare scalar or a {"value": …} / {"v": …} JSON envelope; an unknown envelope is not guessed at by picking its only number.
  • Connection params: host/broker, port (1883 / 8883 TLS), topic, use_tls, username (password encrypted), stale_after_s (required for collection).
  • Validation: verified end-to-end against a real eclipse-mosquitto:2 broker through the full paho loop (tests/test_uns_tap_live.py, opt-in) — publish → read → publisher stops → the run records a gap instead of repeating the cache. Also exercised across a real network on three separate hosts (broker, edge node, reader) against a spec-correct Sparkplug node — alias-only NDATA, periodic re-BIRTH, and a genuine broker-issued Last Will after the publisher was kill -9'd on another machine (refusal in ~2s with stale_after_s deliberately set to 60, so only the death signal could have caused it). 待核实: the edge node is still our own implementation, not a commercial EoN (Ignition / Litmus / HighByte), and not a commercial broker (HiveMQ / EMQX).
  • Command: mqtt_publish = high/MOC/dry-run default. A transient publish has no automatic inverse (delivered is delivered); a retained one overwrites durable broker state, so it captures the prior retained payload and records an inverse.

EtherNet/IP (Rockwell / Allen-Bradley)

  • Supported: ControlLogix / CompactLogix (and GuardLogix) via CIP / EtherNet-IP using pycomm3 (pure-Python — no native deps). Tag-based, symbolic access: read/write tags by name (Conveyor.Speed, Array[3], Program:Main.X) and discover the controller's tag list at runtime (eip_list_tags, the headline feature). eip_controller_info reads the controller identity.
  • Connection params: host, slot (0 for CompactLogix; the CPU slot for a ControlLogix chassis), port (44818). protocol: ethernetip (alias eip).
  • Write: eip_write_tag = high risk_tier, MOC, dry-run default, captures BEFORE value + undo.
  • Not supported / planned: PLC-5 / SLC-500 (PCCC) and Micro800 are not supported = roadmap (Logix tag model only).

EtherCAT (pysoem / SOEM fieldbus master)

  • Supported: a real EtherCAT master via pysoem (the Python binding for the SOEM C stack). CoE SDO read (ethercat_read_sdo, acyclic mailbox upload) + SDO write (ethercat_write_sdo, download), input PDO read (ethercat_read_pdo, one bounded cyclic snapshot), bus scan / slave enumeration (ethercat_slaves, ethercat_slave_info — identity, SM/FMMU mapping, object-dictionary summary), master/working-counter state (ethercat_master_state), and AL-state transitions INIT↔PREOP↔SAFEOP↔OP (ethercat_set_state).

  • HARD REQUIREMENTS (no way around them): Linux, root or CAP_NET_RAW, a dedicated NIC cabled to the bus, and real EtherCAT slave hardware. pysoem is an OPTIONAL extra: pip install iaiops[ethercat] — the base package installs and imports without it, and every EtherCAT tool then degrades to a teaching error (never crashes, never imports pysoem at module load).

  • NOT supported: no software simulator exists (unlike OPC-UA / Modbus) — EtherCAT is hardware-only and not testable in mock-only CI; macOS is unsupported. EoE / FoE / SoE mailbox protocols and full PDO-mapping decode/expansion = roadmap.

  • Connection params: nic (the dedicated interface name, e.g. eth1; alias interface), optional expected_slaves (a sanity check vs the bus scan). protocol: ethercat.

  • Operations matrix:

    ToolOpR/WriskCapture/notes
    ethercat_master_statemaster + WKC state, slave countRlowexpected vs found
    ethercat_slavesbus scan / enumerateRlowindex/vendor/product/rev/addr/AL-state
    ethercat_slave_infoone-slave detailRlowSM/FMMU + OD summary
    ethercat_read_sdoCoE SDO uploadRlowhex + uint interpretation
    ethercat_read_pdoinput PDO snapshotRlowsingle cycle, never loops
    ethercat_write_sdoCoE SDO downloadWhigh/MOCbefore-value (SDO read-back) + undo
    ethercat_set_stateAL-state transitionWhigh/MOCbefore-state + undo; can start/stop motion
  • Write/state safety: ethercat_write_sdo (hex little-endian bytes) and ethercat_set_state are high risk_tier, MOC, dry-run by default, capture the BEFORE value/state for undo, and need a CLI double-confirm. Changing EtherCAT state can START or STOP machine motion — treat with extreme care. 未经授权勿对生产控制系统写入.

PROFINET (DCP discovery / identify + gated DCP Set)

  • Supported: layer-2 PROFINET-DCP via pnio-dcp — profinet_discover (DCP IdentifyAll: one broadcast surfaces every station on the segment — name-of-station, MAC, IP, vendor/device id, role — closer to passive discovery than a per-device fingerprint), profinet_identify_station (by name-of-station), profinet_station_params (targeted DCP Get by MAC → name + IP suite), and profinet_asset_inventory (a register with IO-controller vs IO-device role decoding).
  • Write: profinet_dcp_set re-addresses one station (name-of-station and/or IP suite, by MAC) — high risk_tier, MOC, dry-run default, captures the BEFORE addressing + undo descriptor. Re-addressing a live station can disrupt its IO connection.
  • Scope (deliberate): no RT cyclic process data (that needs an IO-controller/IO-device stack and hard real-time — out of scope and unsafe to tap); the blink / factory-reset DCP services stay unexposed.
  • HARD REQUIREMENTS: raw-socket access (root / admin / CAP_NET_RAW) on the NIC on the PROFINET subnet. pnio-dcp is an OPTIONAL extra: pip install iaiops[profinet] — the base package installs/imports without it, and every tool then degrades to a teaching error.
  • Connection params: host — THIS machine's IP on the PROFINET subnet (the DCP broadcast goes out on it). protocol: profinet.
  • Preview caveat: validated against a mocked pnio-dcp DCP — not verified against live PROFINET devices yet.

Energy edition (electrical substation / utility telecontrol) → iaiops-energy

The energy vertical — IEC 60870-5-104 / DNP3 / IEC 61850 MMS read-only monitoring for substation RTUs/IEDs — moved to its own package in 0.8.0: iaiops-energy (pip install iaiops-energy), built on iaiops.core (shared governance / brain / runtime). Its protocol reference, support matrix, and validation status live in that repo.

Building edition (facility / HVAC / 厂务)

The building vertical adds BACnet/IP (ASHRAE 135) — the dominant building-automation protocol for HVAC, lighting, metering, and facility plant. Install with pip install iaiops[building] and expose with IAIOPS_MCP=building (bundle: bacnet + modbus + opcua + iolink).

  • BACnet/IP (BAC0 over bacpypes3): bacnet_discover (Who-Is device discovery), bacnet_object_list (a device's objects), bacnet_read_property (one object property), bacnet_read_points (present-value of all analog/binary/multistate points — the HVAC snapshot), bacnet_cov_subscribe (bounded change-of-value capture — capped by count AND wall-clock, always unsubscribes), bacnet_read_trend_log (TrendLog buffered records via one bounded readRange). Config: host = THIS machine's BACnet/IP interface (ip or ip/mask) / port (47808).
  • Write: bacnet_write_property (present-value at a BACnet priority 1..16, or relinquish) = high risk_tier, MOC, dry-run default, BEFORE-value read-back + undo. Overriding a live building-control point can move real HVAC/plant.
  • Validation: the read path is verified against a real bacpypes3 virtual BACnet/IP device through the actual async BAC0 stack (tests/test_bacnet_live.py); COV / trend-log / writes on live HVAC gear stay 待核实.

Water treatment edition (水处理)

IAIOPS_MCP=water (or iaiops-mcp-water, pip install iaiops[water]) exposes modbus + opcua + hart + the brain — the protocol set waterworks / wastewater plants actually run. Adds water-domain tag semantics (溶解氧 DO / ORP / 余氯 chlorine / 氨氮 ammonia / TSS/MLSS / 跨膜压差 TMP / UV / 加药 dosing / 曝气 aeration) and water-industry Modbus templates (E+H Promag, Hach SC controller, generic dosing pump — all with explicit 待核实 caveats).

Warehouse / intralogistics edition (仓储 / 物料搬运)

IAIOPS_MCP=warehouse (or iaiops-mcp-warehouse, pip install iaiops[warehouse]) exposes eip + profinet + modbus + opcua + sparkplug + the brain — conveyor & sorter drives over EtherNet/IP (Rockwell) and Profinet (Siemens), VFD / energy meters over Modbus (conveyor_vfd / agv_battery templates), WMS/WCS gateways over OPC-UA, and AMR/IoT telemetry over MQTT-Sparkplug. Edition tools (read-only, advisory): line_bottleneck (Theory-of-Constraints throughput bottleneck across stations) + sortation_health. PdM (pdm_forecast), downtime_triage and OEE are reused as-is.

Clinical-facility edition (医疗设施)

IAIOPS_MCP=clinical (or iaiops-mcp-clinical, pip install iaiops[clinical]) exposes bacnet + modbus + opcua + the brain — hospital facilities as a distinct patient-safety vertical over the building brain. Edition tools (read-only, advisory): isolation_room_check (负压/正压 isolation-room pressurization), medical_gas_check (medical-gas alarm-panel safety), or_environment_check (OR temperature / humidity / pressure envelope). BACnet BMS + Modbus gas-alarm panels + OPC-UA plant SCADA.

Pharmaceutical-manufacturing edition (制药)

IAIOPS_MCP=pharma (or iaiops-mcp-pharma, pip install iaiops[pharma]) exposes bacnet + modbus + hart + opcua + the brain. No new protocol — that is the point: no field protocol is specific to pharma. Cleanrooms run BACnet, purified-water systems run Modbus and HART, filling and lyophilization run S7, DCS and bioreactors run OPC-UA, and all of it was already here. What pharma needed was semantics: the water edition's indicators are municipal (DO, ORP, chlorine, turbidity) and the clinical edition grades one room's pressure, where Annex 1 inspects the cascade.

Edition tools (read-only, advisory): cleanroom_pressure_cascade (EU GMP Annex 1, door by door — adjacency is declared, never inferred from a room list), cleanroom_particle_check, pharma_water_check (USP <645> stage-1 procedure: the non-temperature-compensated reading, the measured temperature rounded down to the tabulated step, and exceeding stage 1 reported as proceed to Stage 2 rather than as a failure).

No compendial limit tables are shipped. The particle limits, the stage-1 conductivity table and the TOC limit belong to the site's qualified specification at its compendial revision. A transcription nobody in this repository can verify would end up deciding whether a batch environment passed — and the error that hurts is the flattering one, since a limit set too loose reads as "in specification". Limits are passed in and cited back; anything not declared is reported no_limit / not_graded and named, never counted as passing. Known gaps are listed in the edition's skill: no PI historian connector, S7 without hardware verification, no GxP (Annex 11 / Part 11) crosswalk yet, and LIMS / QMS deliberately out of scope — they run REST and databases, not f

Installation

Source-derived launch command. Check the maintainer’s required arguments and credentials before running:

bash
uvx iaiops

Set up in your AI client

Merge this template into ~/Library/Application Support/Claude/claude_desktop_config.json. Keep existing servers. Add any arguments, credentials, and permissions required by the maintainer; this template has not been install-tested.

json
{
  "mcpServers": {
    "io-github-industrial-aiops-iaiops": {
      "command": "uvx",
      "args": [
        "iaiops"
      ]
    }
  }
}

Restart Claude Desktop completely for changes to take effect. Confirm the server appears connected in the client’s tool list, then try a read-only example from its documentation.

Claude Desktop setup reference

Package

iaiopspypi

Compatible MCP Clients

Industrial-AIOps works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.

  • Claude Desktop~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.
  • Cursor~/.cursor/mcp.jsonRestart Cursor for changes to take effect.
  • VS Code.vscode/mcp.jsonReload VS Code window for changes to take effect.
  • Windsurf~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect.
  • Claude Code.mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.

Learn More