Skip to content

Repository files navigation

plcio

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.

Supported PLC Families

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

Installation

go get github.com/yatesdr/plcio

Requires Go 1.24 or later. No external dependencies — plcio is implemented entirely in the Go standard library.

Quick Start

Unified Driver Interface

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)
    }
}

Allen-Bradley (ControlLogix / CompactLogix)

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)
    }
}

Allen-Bradley Micro800

cfg := &driver.PLCConfig{
    Name:    "micro820",
    Address: "192.168.1.20",
    Family:  driver.FamilyMicro800,
    Enabled: true,
}

Allen-Bradley SLC 500 / PLC-5 / MicroLogix (PCCC)

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.

Siemens S7

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"},
})

Beckhoff TwinCAT

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.

Omron (FINS)

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"},
})

Omron (EIP) — pending lab verification

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"},
})

Network Discovery

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.

Key Features

  • Unified interface — One Driver interface 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 / IsLikelyConnectionError classify connection loss by error identity
  • Keep-alive — Automatic connection maintenance for protocols that need it

Concurrency

Driver implementations are safe for concurrent use from multiple goroutines. Operations on a single driver are serialized; use separate drivers for parallel I/O.

Adapter Mode

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 RPI

See EtherNet/IP Adapter for the full guide. This package is not safety-rated — see Safety & Intended Use.

Documentation

Detailed documentation for each PLC family and feature:

Support Status

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.

Acknowledgements

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.

Disclaimer

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.

License

Licensed under the Apache License, Version 2.0. See LICENSE for the full text.

About

A go library for interfacing with Allen Bradley Logix, Siemens S7, Beckhoff ADS, and Omron FINS / EIP Industrial PLC's

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages