This guide covers how to use WarLink's underlying drivers in your own Go applications. Each PLC driver can be imported and used independently.
All PLC drivers implement a common interface defined in driver/interface.go:
type Driver interface {
// Connection management
Connect() error
Close() error
IsConnected() bool
// Identification
Family() config.PLCFamily
ConnectionMode() string
GetDeviceInfo() (*DeviceInfo, error)
// Tag discovery (not all families support this)
SupportsDiscovery() bool
AllTags() ([]TagInfo, error)
Programs() ([]string, error)
// Read/Write operations
Read(requests []TagRequest) ([]*TagValue, error)
Write(tag string, value interface{}) error
// Maintenance
Keepalive() error
IsConnectionError(err error) bool
}type TagRequest struct {
Name string // Tag name or address
TypeHint string // Optional type hint (e.g., "INT", "REAL", "DINT")
}type TagValue struct {
Name string // Tag name
DataType uint16 // Native type code (family-specific)
Family string // PLC family identifier
Value interface{} // Decoded Go value
Bytes []byte // Raw bytes in native byte order
Count int // Element count (1 for scalar)
Error error // Per-tag error (nil if successful)
}type TagInfo struct {
Name string // Tag name
TypeCode uint16 // Native type code
Instance uint32 // CIP instance ID
Dimensions []uint32 // Array dimensions
TypeName string // Human-readable type name
Writable bool // Whether the tag can be written
}type DeviceInfo struct {
Family config.PLCFamily
Vendor string
Model string
Version string
SerialNumber string
Description string
}Package: warlink/logix
Supports ControlLogix, CompactLogix, and Micro800 series PLCs via EtherNet/IP.
package main
import (
"fmt"
"log"
"warlink/logix"
)
func main() {
// Connect to PLC
client, err := logix.Connect("192.168.1.100", logix.WithSlot(0))
if err != nil {
log.Fatal(err)
}
defer client.Close()
// Read a tag
value, err := client.Read("Program:MainProgram.Counter")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Counter = %v\n", value.Value)
// Write a tag
err = client.Write("Program:MainProgram.SetPoint", 100.5)
if err != nil {
log.Fatal(err)
}
}// ControlLogix with specific slot
client, _ := logix.Connect("192.168.1.100", logix.WithSlot(2))
// Micro800 (no slot routing, unconnected messaging)
client, _ := logix.Connect("192.168.1.101", logix.WithMicro800())
// Custom route path (for routing through gateways)
client, _ := logix.Connect("192.168.1.100", logix.WithRoutePath([]byte{0x01, 0x00, 0x02, 0x00}))
// Skip Forward Open (use unconnected messaging only)
client, _ := logix.Connect("192.168.1.100", logix.WithoutConnection())// Get all tags
tags, err := client.AllTags()
if err != nil {
log.Fatal(err)
}
for _, tag := range tags {
fmt.Printf("%s: %s (type=0x%04X)\n", tag.Name, tag.TypeName, tag.TypeCode)
}
// Get program list
programs, err := client.Programs()
for _, prog := range programs {
fmt.Println("Program:", prog)
}// Read multiple tags efficiently (batched into single request)
tags := []string{"Tag1", "Tag2", "Tag3", "Program:Main.Counter"}
values, err := client.ReadMultiple(tags)
if err != nil {
log.Fatal(err)
}
for _, val := range values {
if val.Error != nil {
fmt.Printf("%s: error %v\n", val.Name, val.Error)
} else {
fmt.Printf("%s = %v\n", val.Name, val.Value)
}
}// Discover PLCs on the network
devices, err := logix.Discover("255.255.255.255", 3*time.Second)
if err != nil {
log.Fatal(err)
}
for _, dev := range devices {
fmt.Printf("Found: %s at %s\n", dev.ProductName, dev.IP)
}// Read a UDT - returns as map[string]interface{}
value, err := client.Read("MyUDT")
if err != nil {
log.Fatal(err)
}
// Access as map
if m, ok := value.Value.(map[string]interface{}); ok {
fmt.Printf("Temperature: %v\n", m["Temperature"])
fmt.Printf("Running: %v\n", m["Running"])
}
// Get template for a UDT type
template, err := client.GetTemplate(value.DataType)
if err == nil {
for _, member := range template.Members {
fmt.Printf(" %s: %s\n", member.Name, logix.TypeName(member.Type))
}
}Package: warlink/s7
Supports S7-300, S7-400, S7-1200, and S7-1500 PLCs via native S7comm protocol implementation.
package main
import (
"fmt"
"log"
"warlink/s7"
)
func main() {
// Connect to S7-1500 (rack 0, slot 0)
client, err := s7.Connect("192.168.1.102", s7.WithRackSlot(0, 0))
if err != nil {
log.Fatal(err)
}
defer client.Close()
// Read addresses with type hints
values, err := client.ReadWithTypes([]s7.TagRequest{
{Address: "DB1.DBD0", TypeHint: "DINT"},
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Value = %v\n", values[0].GoValue())
// Write a REAL to DB1 at byte offset 4
err = client.Write("DB1.DBD4", float32(72.5))
if err != nil {
log.Fatal(err)
}
}| Format | Example | Description |
|---|---|---|
DB<n>.DB<type><offset> |
DB1.DBD0 |
Data block DWORD at offset 0 |
DB<n>.DBX<offset>.<bit> |
DB1.DBX4.0 |
Data block bit |
I<offset> |
IB0 |
Input byte |
Q<offset> |
QB0 |
Output byte |
M<offset> |
MB0 |
Marker byte |
Type suffixes: X (bit), B (byte), W (word), D (dword)
// S7-300/400 (typically slot 2)
client, _ := s7.Connect("192.168.1.100", s7.WithRackSlot(0, 2))
// S7-1200/1500 (always slot 0)
client, _ := s7.Connect("192.168.1.100", s7.WithRackSlot(0, 0))
// With custom rack
client, _ := s7.Connect("192.168.1.100", s7.WithRackSlot(1, 2)) // Rack 1, Slot 2
// With timeout
client, _ := s7.Connect("192.168.1.100", s7.WithRackSlot(0, 0), s7.WithTimeout(5*time.Second))// Read multiple addresses (automatically batched by PDU size)
requests := []s7.TagRequest{
{Address: "DB1.DBD0", TypeHint: "DINT"},
{Address: "DB1.DBD4", TypeHint: "REAL"},
{Address: "DB1.DBD8", TypeHint: "DINT"},
}
values, err := client.ReadWithTypes(requests)
if err != nil {
log.Fatal(err)
}
for _, val := range values {
fmt.Printf("%s = %v\n", val.Name, val.GoValue())
}// Write using explicit type (for simple addresses like DB1.0)
err := client.WriteWithType("DB1.0", int32(42), "DINT")
// Write using address format (type inferred from address)
err = client.Write("DB1.DBD4", float32(72.5)) // DBD = DWORD/DINT/REAL// Read a STRING (S7 format: 2-byte header + chars)
values, err := client.ReadWithTypes([]s7.TagRequest{
{Address: "DB10.0", TypeHint: "STRING"},
})
fmt.Printf("String: %s\n", values[0].GoValue())Package: warlink/ads
Supports TwinCAT 2 and TwinCAT 3 PLCs via ADS protocol.
package main
import (
"fmt"
"log"
"warlink/ads"
)
func main() {
// Connect to TwinCAT 3
client, err := ads.Connect("192.168.1.103", "192.168.1.103.1.1", 851)
if err != nil {
log.Fatal(err)
}
defer client.Close()
// Read a variable
value, err := client.Read("MAIN.Counter")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Counter = %v\n", value.Value)
// Write a variable
err = client.Write("MAIN.SetPoint", float32(100.0))
if err != nil {
log.Fatal(err)
}
}// TwinCAT 3 (port 851)
client, _ := ads.Connect("192.168.1.103", "192.168.1.103.1.1", 851)
// TwinCAT 2 (port 801)
client, _ := ads.Connect("192.168.1.103", "192.168.1.103.1.1", 801)// Get all symbols
symbols, err := client.AllSymbols()
if err != nil {
log.Fatal(err)
}
for _, sym := range symbols {
fmt.Printf("%s: %s\n", sym.Name, sym.TypeName)
}// Read multiple symbols efficiently using SumUp Read
// This batches all reads into a single request (0xF080)
symbols := []string{"MAIN.Var1", "MAIN.Var2", "GVL.GlobalCounter"}
values, err := client.ReadMultiple(symbols)
if err != nil {
log.Fatal(err)
}
for _, val := range values {
fmt.Printf("%s = %v\n", val.Name, val.Value)
}// Subscribe to variable changes
handle, err := client.Subscribe("MAIN.Counter", 100*time.Millisecond, func(value *ads.Value) {
fmt.Printf("Counter changed: %v\n", value.Value)
})
if err != nil {
log.Fatal(err)
}
// Later, unsubscribe
client.Unsubscribe(handle)Package: warlink/omron
Supports CS/CJ/CP series PLCs via FINS/UDP protocol.
package main
import (
"fmt"
"log"
"warlink/omron"
)
func main() {
// Connect to Omron PLC
client, err := omron.Connect("192.168.1.104", 9600, 0, 0, 0)
if err != nil {
log.Fatal(err)
}
defer client.Close()
// Read DM100 as DINT
value, err := client.Read("DM100", "DINT")
if err != nil {
log.Fatal(err)
}
fmt.Printf("DM100 = %v\n", value.Value)
// Write to DM200
err = client.Write("DM200", int32(1234))
if err != nil {
log.Fatal(err)
}
}| Area | Format | Description |
|---|---|---|
| DM | DM<addr> |
Data Memory |
| CIO | CIO<addr> |
Core I/O |
| WR | WR<addr> |
Work Area |
| HR | HR<addr> |
Holding Area |
| AR | AR<addr> |
Auxiliary Area |
Bit access: DM100.5 (bit 5 of DM100)
Arrays: DM100[10] (10 consecutive words starting at DM100)
// Standard connection
client, _ := omron.Connect(
"192.168.1.104", // IP address
9600, // FINS UDP port
0, // Network number (0 = local)
0, // Node number
0, // Unit number
)For Omron NJ/NX series, use EtherNet/IP with symbolic tags:
package main
import (
"fmt"
"log"
"warlink/omron"
)
func main() {
// Connect via EtherNet/IP (port 44818)
client, err := omron.ConnectEIP("192.168.1.105")
if err != nil {
log.Fatal(err)
}
defer client.Close()
// Tag discovery
tags, err := client.AllTags()
for _, tag := range tags {
fmt.Printf("%s: %s\n", tag.Name, tag.TypeName)
}
// Read by symbolic name
value, err := client.Read("ProductCount")
fmt.Printf("ProductCount = %v\n", value.Value)
}For applications that need to manage multiple PLCs, use plcman.Manager:
package main
import (
"fmt"
"log"
"time"
"warlink/config"
"warlink/plcman"
)
func main() {
// Create manager
mgr := plcman.NewManager()
// Add PLCs from configuration
cfg := &config.PLCConfig{
Name: "MainPLC",
Address: "192.168.1.100",
Family: config.FamilyLogix,
Slot: 0,
Enabled: true,
}
mgr.AddPLC(cfg)
// Connect
err := mgr.Connect("MainPLC")
if err != nil {
log.Fatal(err)
}
// Start polling
mgr.StartPolling(500 * time.Millisecond)
defer mgr.StopPolling()
// Register callback for value changes
mgr.OnValueChange(func(plcName, tagName string, value *plcman.TagValue) {
fmt.Printf("%s.%s = %v\n", plcName, tagName, value.GoValue())
})
// Enable specific tags for polling
mgr.EnableTag("MainPLC", "Program:Main.Counter", true)
mgr.EnableTag("MainPLC", "Program:Main.Temperature", true)
// Read a single tag
value, err := mgr.ReadTag("MainPLC", "Program:Main.Counter")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Counter = %v\n", value.GoValue())
// Write a tag
err = mgr.WriteTag("MainPLC", "Program:Main.SetPoint", 100.0)
if err != nil {
log.Fatal(err)
}
// Keep running...
select {}
}package main
import (
"warlink/config"
"warlink/mqtt"
)
func main() {
cfg := &config.MQTTConfig{
Name: "broker1",
Broker: "localhost",
Port: 1883,
Selector: "line1", // Optional sub-namespace (combined with global namespace)
ClientID: "myapp",
}
pub := mqtt.NewPublisher(cfg)
if err := pub.Start(); err != nil {
log.Fatal(err)
}
defer pub.Stop()
// Publish a tag value
msg := &mqtt.TagMessage{
PLC: "MainPLC",
Tag: "Counter",
Value: 42,
Type: "DINT",
Timestamp: time.Now(),
}
pub.Publish(msg)
}package main
import (
"warlink/kafka"
)
func main() {
cfg := &kafka.Config{
Name: "cluster1",
Brokers: []string{"localhost:9092"},
Topic: "plc-tags",
}
mgr := kafka.NewManager()
mgr.AddCluster(cfg)
if err := mgr.Connect("cluster1"); err != nil {
log.Fatal(err)
}
defer mgr.DisconnectAll()
// Publish a message
msg := &kafka.TagMessage{
PLC: "MainPLC",
Tag: "Counter",
Value: 42,
Type: "DINT",
Timestamp: time.Now(),
}
mgr.Publish("cluster1", "plc-tags", msg)
}package main
import (
"warlink/config"
"warlink/valkey"
)
func main() {
cfg := &config.ValkeyConfig{
Name: "redis1",
Address: "localhost:6379",
Selector: "line1", // Optional sub-namespace (combined with global namespace)
PublishChanges: true,
}
mgr := valkey.NewManager()
pub := mgr.Add(cfg)
if err := pub.Start(); err != nil {
log.Fatal(err)
}
defer pub.Stop()
// Store a tag value
msg := &valkey.TagMessage{
PLC: "MainPLC",
Tag: "Counter",
Value: 42,
Type: "DINT",
Timestamp: time.Now(),
}
pub.Publish(msg)
}All drivers return errors that can be inspected for connection issues:
value, err := client.Read("SomeTag")
if err != nil {
// Check if it's a connection error (may need reconnect)
if driver.IsConnectionError(err) {
log.Println("Connection lost, reconnecting...")
client.Close()
client, _ = logix.Connect(address, opts...)
} else {
// Tag-specific error
log.Printf("Read error: %v\n", err)
}
}
// For batch reads, check per-tag errors
values, err := client.ReadMultiple(tags)
if err != nil {
// Overall request failed
log.Fatal(err)
}
for _, val := range values {
if val.Error != nil {
log.Printf("%s: %v\n", val.Name, val.Error)
}
}- Use batch reads - ReadMultiple() is much faster than individual Read() calls
- Maintain connections - Reuse client instances instead of reconnecting
- Use connected messaging - For Logix, Forward Open reduces latency
- Set appropriate poll rates - 250ms minimum, 500ms-1s typical
- Filter changes - Only process values that actually changed
- Use keepalive - Call Keepalive() periodically for idle connections
// Keepalive example
ticker := time.NewTicker(30 * time.Second)
go func() {
for range ticker.C {
if err := client.Keepalive(); err != nil {
log.Printf("Keepalive failed: %v\n", err)
}
}
}()All PLC driver packages are pure Go implementations with no external dependencies:
| Package | External Dependencies |
|---|---|
warlink/logix |
None (native EtherNet/IP + CIP) |
warlink/s7 |
None (native S7comm) |
warlink/ads |
None (native ADS/AMS) |
warlink/omron |
None (native FINS) |
Broker/publisher packages have external dependencies:
| Package | External Dependencies |
|---|---|
warlink/mqtt |
github.com/eclipse/paho.mqtt.golang |
warlink/kafka |
github.com/segmentio/kafka-go |
warlink/valkey |
github.com/redis/go-redis/v9 |