A reactive Allen‑Bradley PLC client built on top of libplctag. It provides a simple, high‑performance, reactive API for reading/writing tags from Rockwell/Allen‑Bradley controllers.
Warning – Disclaimer PLCs control equipment. Mistakes can cause loss of property, production, or life. Use extreme caution. No warranty of suitability is provided.
Supported PLC families (via libplctag)
- ControlLogix/CompactLogix (LGX) over CIP EtherNet/IP
- Micro800 family where supported by libplctag
- PLC‑5, SLC 500, MicroLogix (Ethernet/ENI/DH+ bridging where supported)
- Additional families supported by libplctag may be usable
Core features
- Create tags and group them for bulk operations
- Reactive APIs (IObservable) for on‑change updates
- Async reactive APIs (IObservableAsync) through ReactiveUI.Extensions on .NET 8+
- Read/write primitives: 8/16/32/64‑bit signed/unsigned, 32/64‑bit float
- Bit addressing helpers for coil/word bits
- String and structure support (libplctag style)
- Bulk read/write across groups
- Health monitoring (Ping/ObservePing)
- Source generator attributes for typed PLC stream models
- TUnit tests running on Microsoft Testing Platform
Getting started Installation
- Install the NuGet package:
- Package Manager: Install-Package ABPlcRx
- .NET CLI: dotnet add package ABPlcRx
- The ABPlcRx package includes its source generator analyzer; no separate generator package is required for normal NuGet consumption.
- ABPlcRx depends on libplctag; the NuGet dependency brings required bindings.
Basic concepts
- Variable: your app’s key for a tag (free‑form string)
- TagName: the PLC’s address/name for the tag (e.g., B3:3, N7:0, MyTag)
- TagGroup: logical group to batch operations (e.g., “Default”, “Motion”)
- Types and bits: to read/write a bit, create a tag as short (Int16) and use bit index 0‑15
Quick start
using ABPlcRx;
using System;
using System.Reactive.Disposables;
var disposables = new CompositeDisposable();
// SLC/PLC5/MicroLogix example (500ms scan)
var slc = new ABPlcRx(PlcType.SLC, "192.168.1.50", TimeSpan.FromMilliseconds(500));
disposables.Add(slc);
// Create a word tag and use bit addressing (B3:3/0)
slc.AddUpdateTagItem<short>("LightOn", "B3:3", "Default");
// Observe changes (bool via bit 0)
disposables.Add(
slc.Observe<bool>("LightOn", bit: 0)
.Subscribe(v => Console.WriteLine($"LightOn = {v}"))
);
// Toggle the bit and write
var current = !slc.Value<bool>("LightOn", bit: 0);
slc.Value("LightOn", current, bit: 0); // AutoWriteValue=true writes immediately
Console.WriteLine($"Wrote {current} -> B3:3/0");ControlLogix/CompactLogix (LGX) example
// For LGX you must provide a path (default "1,0" = backplane, slot 0)
var lgx = new ABPlcRx(PlcType.LGX, "192.168.1.60", TimeSpan.FromMilliseconds(200),
timeOut: TimeSpan.FromSeconds(2), path: "1,0");
// Controller tag named MyDINT
lgx.AddUpdateTagItem<int>("Counter", "MyDINT", "Default");
// Observe numeric values
lgx.Observe<int>("Counter").Subscribe(v => Console.WriteLine($"Counter={v}"));
// Increment and write
lgx.Value("Counter", lgx.Value<int>("Counter") + 1);
// Standard Logix BOOL tag named MachineReady
lgx.AddUpdateTagItem<bool>("Ready", "MachineReady", "Default");
lgx.Observe<bool>("Ready").Subscribe(v => Console.WriteLine($"Ready={v}"));
var ready = !lgx.Value<bool>("Ready");
lgx.Value("Ready", ready);Reactive API highlights
- Observe(variable, bit = -1): stream values on change, supports late‑added tags
- ObserveAsync(variable, bit = -1): async-native stream using ReactiveUI.Extensions.Async on .NET 8+
- ObserveMany(params string[] variables): latest values as a dictionary
- ObserveManyAsync(params string[] variables): async-native latest value dictionaries
- ObserveGroup(groupName): emits tag objects in a group when they change
- ObserveGroupAsync(groupName): async-native group stream
- ObserveSampled(variable, sampleInterval, bit, scheduler): sampled stream for rate limiting
- ObserveSampledAsync(variable, sampleInterval, bit, scheduler): async-native sampled stream
- ObserveErrors(): only tag operations that returned an error
- ObserveErrorsAsync(): async-native error stream
- CreateWriter(variable, bit): returns an IObserver that writes on OnNext
Async observables
using ReactiveUI.Extensions.Async;
var counter = lgx.ObserveAsync<int>("Counter");
// IObservableAsync<T> can use ReactiveUI.Extensions.Async operators,
// including Select, Where, Merge, CombineLatest, Retry, Timeout, Publish,
// ReplayLatest, ToObservable, and ToObservableAsync.
var activeCounter =
counter
.Where(value => value > 0)
.Select(value => $"Counter={value}");Source generated stream models
using ABPlcRx.SourceGeneration;
[PlcModel]
[PlcTag(typeof(int), "Counter", "MyDINT")]
[PlcTag(typeof(bool), "LightOn", "B3:3", Bit = 0)]
public partial class MachineTags
{
}
var tags = new MachineTags();
using var binding = tags.AttachPlcStreams(slc);
tags.CounterObservable.Subscribe(value => Console.WriteLine($"Counter={value}"));
tags.LightOnObservable.Subscribe(value => Console.WriteLine($"LightOn={value}"));
// On .NET 8+ the generator also emits IObservableAsync<T> streams.
var asyncLightOn = tags.LightOnObservableAsync;The generator creates a property for each class-level PlcTag attribute, registers tags in AttachPlcStreams, keeps the generated property updated from the observable subscription, and exposes both PropertyNameObservable and PropertyNameObservableAsync on .NET 8+. Boolean tags without a Bit value are registered as native bool tags. Boolean bit tags with Bit set are registered as short tags and exposed as bool values.
Examples Observe multiple variables
// Emits { "LightOn": true, "Counter": 42 }
slc.ObserveMany("LightOn", "Counter")
.Subscribe(dict => Console.WriteLine(string.Join(", ", dict.Select(kv => $"{kv.Key}={kv.Value}"))));Group operations and bulk I/O
// Group creation is implicit via AddUpdateTagItem
slc.AddUpdateTagItem<short>("Alarm", "B3:10", "Safety");
slc.AddUpdateTagItem<short>("Guard", "B3:11", "Safety");
// Bulk read/write across all groups
var results = slc.Read();
var wrote = slc.Write();Health monitoring
// One‑off ping
var ok = slc.Ping();
// Observe ping results every 2 seconds
slc.ObservePing(TimeSpan.FromSeconds(2))
.Subscribe(alive => Console.WriteLine($"PLC reachable: {alive}"));Advanced: writing with an observer
var writer = slc.CreateWriter<bool>("LightOn", bit: 0);
writer.OnNext(true); // writes and commitsConfiguration and options
- ScanEnabled: enable/disable background scanning by group
- AutoWriteValue: when true (default), setting Value(tag) writes immediately
- Timeout: communications timeout (ms) via constructor timeOut
- Groups: use the tagGroup parameter to logically separate tags
API surface (high level)
- ABPlcRx (implements IABPlcRx)
- AddUpdateTagItem(variable, tagName, tagGroup = "Default")
- Observe(variable, bit = -1)
- ObserveAsync(variable, bit = -1) on .NET 8+
- ObserveMany(params string[] variables)
- ObserveManyAsync(params string[] variables) on .NET 8+
- ObserveGroup(groupName)
- ObserveGroupAsync(groupName) on .NET 8+
- ObserveSampled(variable, sampleInterval, bit = -1, scheduler = null)
- ObserveSampledAsync(variable, sampleInterval, bit = -1, scheduler = null) on .NET 8+
- ObserveErrors()
- ObserveErrorsAsync() on .NET 8+
- CreateWriter(variable, bit = -1)
- Value(variable, bit = -1) / Value(variable, value, bit = -1)
- Read()/Read(variable) and Write()/Write(variable)
- Ping(bool echo = false), PingAsync(...), ObservePing(interval,...)
- ObservePingAsync(interval,...) on .NET 8+
Testing
- Tests are in
src/ABPlcRx.Tests. - The suite uses TUnit with Microsoft Testing Platform;
global.jsonsets"test": { "runner": "Microsoft.Testing.Platform" }. - Run all tests with:
dotnet test src/ABPlcRx.slnData types and bit access
- Logix/CompactLogix/Micro800 standard
BOOLtags can be created directly withAddUpdateTagItem<bool>("Ready", "MachineReady")and read or written withValue<bool>("Ready"). - To treat a single bit in an SLC/PLC word as a boolean, create the tag as
shortand use thebitparameter (0‑15). - For numeric tags use C# primitive types: sbyte/byte/short/ushort/int/uint/long/ulong/float/double.
- Strings and structure types are supported where the PLC and libplctag support them.
Error handling
- Each read/write yields a PlcTagResult with StatusCode (see PlcTagStatus).
- If
FailOperationRaiseExceptionis set true on the underlying controller, failed operations will throwPlcTagException.
Performance notes
- Tags are grouped internally; bulk
Read()/Write()iterate groups for fewer round trips. - Tag lookups are cached; prefer consistent
variablekeys. - Use
ObserveSampledto reduce update rates to UI or logs.
Troubleshooting
- LGX controllers require a valid
path(e.g., "1,0" for backplane/slot0). - Ensure your PLC networking, firewall, and CIP routes are reachable from your app host.
- Use
Ping()/ObservePing()to monitor reachability.
License MIT. See LICENSE.
ABPlcRx - Empowering Industrial Automation with Reactive Technology ⚡🏭