A pure Go library for communicating with industrial PLCs (Programmable Logic Controllers) across multiple vendors and protocols. plcio provides a unified Driver interface for reading tags, writing values, discovering devices, and browsing symbol tables across Allen-Bradley (Logix, SLC 500, PLC-5, MicroLogix), Siemens, Beckhoff, and Omron PLCs.
BETA — Hardware-tested reads and writes: Allen-Bradley ControlLogix L7 and Micro820, Siemens S7-1200, and Beckhoff TwinCAT 3 on a CX. SLC 500 and MicroLogix are implemented, pending lab verification of this release. PLC-5, Omron FINS, Omron EIP, and the EtherNet/IP adapter are implemented, pending lab verification. See the hardware verification log for tested paths and the remaining lab checklist.
| Family | Models | Protocol | Tag Discovery | Tested On |
|---|---|---|---|---|
| Allen-Bradley Logix | ControlLogix, CompactLogix | EtherNet/IP (CIP) | Automatic | ControlLogix L7 |
| Allen-Bradley Micro800 | Micro820, Micro850 | EtherNet/IP (CIP) | Automatic | Micro820 |
| Allen-Bradley SLC 500 | SLC 5/03, 5/04, 5/05 | PCCC over EtherNet/IP | Automatic (file directory) | Pending lab verification |
| Allen-Bradley PLC-5 | PLC-5/20E, 5/40E, 5/80E | PCCC over EtherNet/IP | Manual (address-based) | Pending lab verification |
| Allen-Bradley MicroLogix | 1100, 1200, 1400, 1500 | PCCC over EtherNet/IP | Automatic (file directory) | Pending lab verification |
| Siemens S7 | S7-300, S7-400, S7-1200, S7-1500 | S7comm (port 102) | Manual (address-based) | S7-1200 |
| Beckhoff TwinCAT | CX series, TwinCAT 2/3 | ADS (port 48898) | Automatic | CX, TwinCAT 3 |
| Omron (FINS) | CS1, CJ1/2, CP1, CV | FINS TCP/UDP (port 9600) | Manual (address-based) | Pending lab verification |
| Omron (EIP) | NJ, NX Series | EtherNet/IP (CIP) | Automatic (no UDT members) | Pending lab verification |
go get github.com/yatesdr/plcioRequires Go 1.24 or later. No external dependencies — plcio is implemented entirely in the Go standard library.
Every PLC uses the same driver.Driver interface, so your application code is vendor-agnostic:
package main
import (
"fmt"
"log"
"github.com/yatesdr/plcio/driver"
)
func main() {
// Create a driver from configuration
cfg := &driver.PLCConfig{
Name: "myPLC",
Address: "192.168.1.10",
Family: driver.FamilyLogix,
Enabled: true,
}
drv, err := driver.Create(cfg)
if err != nil {
log.Fatal(err)
}
// Connect
if err := drv.Connect(); err != nil {
log.Fatal(err)
}
defer drv.Close()
// Read tags
results, err := drv.Read([]driver.TagRequest{
{Name: "MyTag"},
{Name: "AnotherTag"},
})
for _, tv := range results {
if tv.Error != nil {
fmt.Printf("%s: ERROR %v\n", tv.Name, tv.Error)
} else {
fmt.Printf("%s = %v\n", tv.Name, tv.Value)
}
}
if err != nil {
log.Printf("read incomplete: %v", err)
return
}
// Write a value
if err := drv.Write("MyTag", 42); err != nil {
log.Fatal(err)
}
}cfg := &driver.PLCConfig{
Name: "logix",
Address: "192.168.1.10",
Family: driver.FamilyLogix,
Slot: 0, // CPU slot (0 for CompactLogix, varies for ControlLogix)
Enabled: true,
}
drv, _ := driver.Create(cfg)
drv.Connect()
defer drv.Close()
// Read tags by symbolic name
results, _ := drv.Read([]driver.TagRequest{
{Name: "Program:MainProgram.Counter"},
{Name: "MyDINT"},
{Name: "MyUDT"},
})
// Discover all tags
if drv.SupportsDiscovery() {
tags, _ := drv.AllTags()
for _, t := range tags {
fmt.Printf(" %s (%s)\n", t.Name, t.TypeName)
}
}cfg := &driver.PLCConfig{
Name: "micro820",
Address: "192.168.1.20",
Family: driver.FamilyMicro800,
Enabled: true,
}cfg := &driver.PLCConfig{
Name: "slc500",
Address: "192.168.1.15",
Family: driver.FamilySLC500, // or FamilyPLC5, FamilyMicroLogix
Enabled: true,
}
drv, _ := driver.Create(cfg)
drv.Connect()
defer drv.Close()
// Read data table addresses (no type hints needed)
results, _ := drv.Read([]driver.TagRequest{
{Name: "N7:0"}, // Integer
{Name: "F8:5"}, // Float
{Name: "B3:0/5"}, // Single bit
{Name: "T4:0.ACC"}, // Timer accumulated value
})
// Write a value
drv.Write("N7:0", 42)PCCC PLCs use file-based data table addresses instead of symbolic tag names. SLC 500 and MicroLogix support automatic data table discovery via the file directory; PLC-5 requires manual address configuration. See Allen-Bradley PCCC for the full address reference and usage details.
cfg := &driver.PLCConfig{
Name: "s7plc",
Address: "192.168.1.30",
Family: driver.FamilyS7,
Slot: 2, // Slot 2 for S7-300/400, Slot 0 for S7-1200/1500
Enabled: true,
}
drv, _ := driver.Create(cfg)
drv.Connect()
defer drv.Close()
// S7 uses address-based tags with type hints
results, _ := drv.Read([]driver.TagRequest{
{Name: "DB1.0", TypeHint: "DINT"},
{Name: "DB1.4", TypeHint: "REAL"},
{Name: "M100.0", TypeHint: "BOOL"},
})cfg := &driver.PLCConfig{
Name: "beckhoff",
Address: "192.168.5.212:48898",
Family: driver.FamilyBeckhoff,
AmsNetId: "5.45.219.226.1.1", // Target AMS identity, independent of TCP host
AmsPort: 851, // TwinCAT 3 runtime (801 for TC2)
Enabled: true,
}
drv, _ := driver.Create(cfg)
drv.Connect()
defer drv.Close()
// Read by symbol name
results, _ := drv.Read([]driver.TagRequest{
{Name: "MAIN.test_struct"}, // map[string]any with typed member values
{Name: "MAIN.test_2d_dint_array_style1"}, // flat []int64
{Name: "MAIN.test_struct.my_dint"},
})Successful unified reads use int64, uint64, float64, bool, string, typed primitive slices and record maps. Integers retain their precision. Check per-tag errors and process successful results even when Read returns a top-level connection error. ADS resolves schemas automatically; driver.Describer is optional inspection. See Beckhoff details and compatibility changes.
cfg := &driver.PLCConfig{
Name: "omron_fins",
Address: "192.168.1.50",
Family: driver.FamilyOmron,
Protocol: "fins",
FinsPort: 9600,
FinsNetwork: 0,
FinsNode: 0, // Usually last octet of PLC IP
FinsUnit: 0,
Enabled: true,
}
drv, _ := driver.Create(cfg)
drv.Connect()
defer drv.Close()
// FINS uses memory area addresses with type hints
results, _ := drv.Read([]driver.TagRequest{
{Name: "DM100", TypeHint: "INT"},
{Name: "DM200", TypeHint: "DINT"},
{Name: "CIO50", TypeHint: "WORD"},
{Name: "HR0", TypeHint: "INT"},
})cfg := &driver.PLCConfig{
Name: "omron_nj",
Address: "192.168.1.60",
Family: driver.FamilyOmron,
Protocol: "eip",
Enabled: true,
}
drv, _ := driver.Create(cfg)
drv.Connect()
defer drv.Close()
// EIP uses symbolic tag names (case-sensitive)
results, _ := drv.Read([]driver.TagRequest{
{Name: "MyVariable"},
{Name: "Counter1"},
})Discover PLCs on your network across all supported protocols:
import "github.com/yatesdr/plcio/driver"
// Discover all PLC types on the local network
devices := driver.DiscoverAll(
"255.255.255.255", // Broadcast address
"192.168.1.0/24", // Subnet to scan
500*time.Millisecond, // Timeout per device
20, // Concurrent scan workers
)
for _, dev := range devices {
fmt.Printf("[%s] %s at %s:%d (%s)\n",
dev.Family, dev.ProductName, dev.IP, dev.Port, dev.Vendor)
}driver.DiscoverAllWithReport takes the same arguments and also returns per-protocol
failures (for example a refused broadcast or an invalid CIDR) instead of dropping them.
- Unified interface — One
Driverinterface works across all PLC families - Zero dependencies — Pure Go standard library, no CGO
- Tag discovery — Browse and enumerate tags on supported PLCs
- Network discovery — Find PLCs on your network via EIP broadcast, S7 port scan, TwinCAT UDP Get Info (reports the real AMS NetID), and FINS scan
- Batch reads — Efficient multi-tag reads with automatic protocol-level batching
- Structure decoding — Automatic UDT/struct member unpacking (Logix, ADS)
- Per-tag errors — Individual tag failures don't fail the entire batch
- Connection detection —
driver.IsConnectionLost/IsLikelyConnectionErrorclassify connection loss by error identity - Keep-alive — Automatic connection maintenance for protocols that need it
Driver implementations are safe for concurrent use from multiple goroutines. Operations on a single driver are serialized; use separate drivers for parallel I/O.
In addition to the scanner-side drivers above, plcio includes an adapter-side package (plcio/eipadapter, implemented, pending lab verification) that lets your Go program be scanned by a PLC over EtherNet/IP. Typical use: smart sensors, vision systems, or bench fixtures that feed data into a PLC's I/O scan.
import "github.com/yatesdr/plcio/eipadapter"
input := eipadapter.NewAssembly(101, eipadapter.AssemblyInput, 16)
adp, _ := eipadapter.New(eipadapter.Config{
Identity: eipadapter.Identity{VendorID: 0x1337, DeviceType: 0x000C, ProductName: "MyDevice", SerialNumber: 1, RevMajor: 1, State: 0x03},
Assemblies: []*eipadapter.Assembly{input},
})
go adp.Serve(ctx)
input.SetBytes(0, myDataBytes) // produced cyclically at the negotiated RPISee EtherNet/IP Adapter for the full guide. This package is not safety-rated — see Safety & Intended Use.
Detailed documentation for each PLC family and feature:
- Allen-Bradley (Logix & Micro800)
- Allen-Bradley (SLC 500, PLC-5 & MicroLogix)
- Siemens S7
- Beckhoff TwinCAT (ADS)
- Omron (FINS & EIP)
- EtherNet/IP Adapter (be-a-device)
- Network Discovery
- API Reference
- Candidate Compatibility
- Implementation Audit and Promotion Gates
- Safety & Intended Use
- Troubleshooting
Tested = verified on hardware (ControlLogix L7, Micro820, S7-1200, Beckhoff CX with TwinCAT 3). Implemented = implemented and covered by protocol-level tests, pending lab verification.
| Feature | Logix | Micro800 | SLC 500 | PLC-5 | MicroLogix | S7 | Beckhoff | Omron FINS | Omron EIP |
|---|---|---|---|---|---|---|---|---|---|
| Connect / Read / Write | Tested | Tested | Implemented | Implemented | Implemented | Tested | Tested | Implemented | Implemented |
| Batch Reads | Tested | N/A | Implemented | Implemented | Implemented | Tested | Tested | Implemented | Implemented |
| Tag Discovery | Tested | Tested | Implemented | N/A | Implemented | N/A | Tested | N/A | Implemented |
| UDT/Struct Decode | Tested | Tested | N/A | N/A | N/A | N/A | Tested (published layouts) | N/A | No |
| Device Info | Tested | Tested | Implemented | Implemented | Implemented | Tested | Tested | Implemented | Implemented |
| Keep-alive | Tested | Tested | Implemented | Implemented | Implemented | Tested | Tested | Implemented | Implemented |
| Network Discovery | Tested | Tested | Implemented | Implemented | Implemented | Tested | Tested | Implemented | Implemented |
The EtherNet/IP adapter (eipadapter) is implemented, pending lab verification.
plcio was built from the ground up in pure Go, but the protocol implementations would not have been possible without the research, documentation, and reference code provided by several outstanding open-source projects:
-
pylogix — Invaluable reference for Allen-Bradley EtherNet/IP and CIP implementation details including Forward Open connection parameters, tag discovery, and template decoding. Many protocol constants and sequencing details were validated against pylogix's well-tested codebase.
-
pycomm3 — Reference for CIP structure attribute handling, template size computation, and PCCC protocol constants. The approach to UDT member decoding was informed by pycomm3's implementation, and the PCCC file type codes and command framing were validated against its SLC/PLC-5 driver.
-
libplctag — Essential resource for Omron EIP/CIP support. GitHub issues and source code provided critical insight into Omron NJ/NX symbol object attributes and CIP vendor-specific behavior. Also a valuable general reference for multi-vendor PLC protocol details.
-
rust-eip — Reference for EtherNet/IP session management and CIP message framing patterns, helpful for validating our EIP transport layer implementation.
-
gos7 — Go implementation of the S7comm protocol that served as a reference for S7 connection setup, PDU negotiation, and data block addressing.
-
Wireshark — Protocol captures with Wireshark's EtherNet/IP, S7comm, and ADS dissectors were used extensively to validate packet structures and debug protocol-level issues across all PLC families.
-
Omron W506 Manual — The NJ/NX-series CPU Unit Built-in EtherNet/IP Port User's Manual provided essential protocol details for Omron EIP tag discovery and symbol access.
Thank you to the maintainers and contributors of these projects for making industrial protocol communication more accessible.
plcio is provided "AS IS" without warranty of any kind. PLCs frequently control industrial equipment that can cause serious injury or death if operated improperly. This library is intended for monitoring and data collection only. See Safety & Intended Use for critical safety information before using this library in any industrial environment.
Licensed under the Apache License, Version 2.0. See LICENSE for the full text.