Guide
How to get legacy PLC data into Power BI
A reference architecture for plants running older Allen-Bradley, Omron, Modbus and Siemens PLCs. Read the tags with a C# integration layer, land them in the database you choose, then report in Power BI and share the data through an API.
By Nfinite · Published · 12 minute read
The short answer
Don't connect Power BI to the PLC. Run a C# integration service on the plant network that reads tags with libplctag (or an S7 or OPC UA driver) and writes them, through a database abstraction layer, into PostgreSQL, Snowflake, Databricks or SQL Server. Power BI reports from that database. A read-only API serves everything else.
Architecture
The architecture at a glance
Five layers, each with one job. The PLCs keep running the line. Everything above them can change without anyone touching ladder logic.
1PLCs on the floor
Plant floor2Integration layer · C# / .NET
Edge box on the plant networkReads tags on a schedule, stamps them in UTC, flags bad quality and holds data locally if the database can't be reached.
3Database abstraction layer
Part of the same C# serviceThe plant side writes to a single interface. Changing database is a configuration change, not a rewrite.
4Your database
On site or cloudTag metadata, time-series readings, events and pre-aggregated rollups.
5Power BI
Reporting5REST API
ASP.NET Core · read-onlyStart here
Why Power BI shouldn't talk to the PLC directly
It's the first thing most people try, and it's the wrong place to start. Power BI has no native connector for PLC protocols like EtherNet/IP, Modbus or S7. Even if you bolt one on, you hit four problems quickly.
- The PLC runs the line. Its job is control, not answering report queries. Every dashboard refresh, from every user, becomes another request on the controller's communications.
- PLCs don't keep history. A tag holds its current value. Yesterday's downtime, last week's scrap and this month's OEE all need somewhere to live.
- The plant network should stay closed. Office laptops and cloud services shouldn't have a route to the controllers. One small service that reaches out is far easier to secure than many tools reaching in.
- Raw tags aren't KPIs.
N7:12orLine1_GoodCountmeans nothing to a plant manager. Something has to turn counters and states into agreed numbers like availability, scrap and downtime by reason.
The fix is to separate the jobs. One layer reads the PLCs, one stores the data, and the reporting and integration tools only ever talk to the store.
Layer 1
Reading the PLCs with libplctag
Direct reads over the network, with no OPC server or vendor runtime in between.
In many Australian plants the legacy kit is a mix of Allen-Bradley, Omron, Modbus devices and Siemens. For most of it we use libplctag, an open-source library for reading and writing PLC tags, through its .NET wrapper. It talks to the controllers directly over Ethernet.
What libplctag covers
- Allen-Bradley Logix: ControlLogix, CompactLogix and Micro800, addressed by tag name over EtherNet/IP.
- Older Allen-Bradley: PLC-5, SLC 500 and MicroLogix, addressed by data-table file (for example
N7:0orF8:3) over EtherNet/IP. - Omron: NJ and NX series controllers.
- Modbus TCP: drives, power meters, remote I/O and the many other devices that speak it.
What it doesn't cover
libplctag doesn't speak Siemens S7. For S7-300, S7-400, S7-1200 and S7-1500 PLCs we use a dedicated S7 driver, or the PLC's own OPC UA server where the CPU and firmware support it. Either way it sits behind the same reader interface, so everything downstream looks identical.
Kit that only talks DH+, DH-485 or serial needs a bridge: a ControlLogix chassis with the right communications module, or a protocol gateway that exposes the data over Ethernet.
Read-only, with no program changes
We read the tags the PLC already holds. Nothing in the ladder logic changes, and writes are switched off in configuration. If a project genuinely needs write-back, such as pushing a recipe or a setpoint, that's a separate, agreed change, tested with your controls engineer.
What a read looks like
Here is a simplified sketch of a single read from a ControlLogix PLC in C#. The production service wraps this in batching, retries, timeouts and scheduling, but the core is this small.
// Simplified sketch: one tag, one read. The production service batches
// tags, retries on error and runs each poll group on its own schedule.
using libplctag;
var goodCount = new Tag
{
Name = "Line1_GoodCount", // tag name in the Logix program
Gateway = "10.20.1.15", // PLC address on the plant network
Path = "1,0", // backplane 1, CPU in slot 0
PlcType = PlcType.ControlLogix,
Protocol = Protocol.ab_eip, // EtherNet/IP
Timeout = TimeSpan.FromSeconds(2),
};
await goodCount.InitializeAsync();
await goodCount.ReadAsync();
int value = goodCount.GetInt32(0); // DINT at byte offset 0 Layer 2
Designing the integration layer
The part that decides whether anyone trusts the numbers.
Reading one tag is easy. Reading a few thousand, reliably, for years, on a network you don't want to disturb, is the actual job. These are the rules we build to.
- Poll groups by rate. Machine states and counters might need a read every second. Temperatures every ten seconds. Setpoints and recipe values once a minute. Grouping tags by how fast they change keeps the load on the PLC low and predictable.
- Read arrays and UDTs, not single tags. One request for a 50-element array or a user-defined type is far cheaper than 50 separate requests. Where the PLC program allows it, we read whole structures and unpack them in the service.
- Store changes, not noise. Change detection and deadbands mean a temperature that wobbles by a hundredth of a degree doesn't create a row every second. Counters and states are stored on every change.
- Timestamp in UTC, at the read. The edge box syncs to NTP, every reading is stamped in UTC when it's read, and local time is applied in reporting. That saves real pain twice a year when daylight saving starts and ends in NSW, Victoria, South Australia, Tasmania and the ACT.
- Carry a quality flag. A timeout or comms error is recorded as bad quality, not as zero. A line that looks stopped because a cable was unplugged is the fastest way to lose operations' trust.
- Store and forward. Readings go into a local buffer on disk before they're sent. If the database or the WAN link drops, the service keeps collecting and catches up when it's back, with no gaps.
- Configuration, not code. PLC addresses, tag lists, poll rates and scaling live in a config file. Adding a tag or a line is a config change and a restart, not a software release.
- Watch the watcher. The service publishes its own health: last good read per PLC, read latency, error counts and buffer depth. If a PLC goes quiet, someone hears about it before the shift report is wrong.
The service itself is a .NET worker that runs as a Windows service, a Linux systemd service or a container, on a small industrial PC or virtual machine on the plant network. It needs no inbound connections from outside the plant.
Layer 3
A database abstraction layer
Pick the database that suits your organisation, and change your mind later.
Which database should plant data land in? It depends on what your organisation already runs, and the answer can change. A manufacturer might start on PostgreSQL at the site, then move to Snowflake or Databricks when head office builds a data platform.
So the integration layer never talks to a database directly. It writes to one C# interface, and each database gets its own implementation, chosen in configuration.
public enum Quality { Good, Uncertain, Bad }
public sealed record TagReading(
string TagId,
DateTime TimestampUtc,
double? Value,
Quality Quality);
// The integration layer only knows about this interface.
public interface ITelemetryStore
{
Task WriteBatchAsync(IReadOnlyList<TagReading> readings, CancellationToken ct);
Task WriteEventsAsync(IReadOnlyList<LineEvent> events, CancellationToken ct);
}
// One implementation per database, picked in configuration:
// PostgresTelemetryStore (Npgsql, binary COPY)
// SqlServerTelemetryStore (SqlBulkCopy)
// SnowflakeTelemetryStore (staged batch loads)
// DatabricksTelemetryStore (Delta tables) Each provider uses the fast path for its database: binary COPY for PostgreSQL, bulk copy for SQL Server, and staged batch loads for Snowflake and Databricks rather than row-by-row inserts. Changing the target means enabling a different provider. The PLC side, the tag configuration and the buffer stay as they are.
Choosing a database
| Database | Good fit when | Notes for plant data |
|---|---|---|
| PostgreSQL (optionally TimescaleDB) | You want a capable, low-cost database at the site or in your own cloud. | TimescaleDB adds time-based partitioning and continuous aggregates, which suit high-volume tag data. |
| SQL Server | Your IT team already runs Microsoft infrastructure. | Mature tooling and a first-class Power BI connector. Partition large reading tables by time. |
| Snowflake | Head office already uses Snowflake for business data. | Load in batches rather than single rows, and pre-aggregate for reporting to keep compute costs in check. |
| Databricks | You run a lakehouse, or have data science and machine learning plans for the data. | Land readings as Delta tables. Good for joining plant data with quality, maintenance and ERP data. |
Layer 4
A data model Power BI can use
Raw readings for storage, rollups for reporting. Keep both.
Tag readings are tall and narrow: one row per tag per change. That's the right shape for storage and the wrong shape for reporting, so the database holds a few distinct kinds of table.
- Tag metadata. One row per tag: PLC, address, line, asset, description, engineering units, scaling and poll group. This is what turns
N7:12into "Line 1 good count". - Readings. Tag, UTC timestamp, value and quality. Partitioned by time and kept for as long as you need raw history.
- Events. Machine states, downtime and changeovers as rows with a start, an end, a line and a reason code. Downtime by reason is built from here.
- Rollups. Per-minute, per-hour and per-shift aggregates: good and total counts, run time, planned time and scrap. Power BI reads these, not the raw readings.
- Dimensions. Lines and assets, products, the shift calendar and reason codes, maintained once and shared by every report.
Counters need care. PLC counters reset at shift change, roll over at their maximum and get zeroed during maintenance. The rollup logic works in deltas and handles resets, so totals stay right.
Layer 5a
Power BI on top
With a clean data model, this is the easy part.
Power BI connects to the database with its standard connectors. PostgreSQL, SQL Server, Snowflake and Databricks all have one.
Import or DirectQuery
- Import copies data into the Power BI model on a schedule. It's fast and suits shift, daily and weekly reporting. How often it can refresh depends on your licence.
- DirectQuery queries the database each time a visual loads. Combined with automatic page refresh it gets much closer to live, at the cost of more load on the database. Point it at rollup tables, not raw readings.
If the database sits on your plant or office network, the Power BI service reaches it through the on-premises data gateway. Cloud databases like Snowflake and Databricks are reached directly.
What we usually build first
- OEE, as availability × performance × quality: run time over planned time, actual output over ideal output for that run time, and good count over total count.
- Throughput against target, by hour and by shift.
- Downtime by reason, from the events table.
- Scrap and first-pass yield, by line, product and shift. With machine vision on the line, scrap can be broken down by defect type.
- Energy per unit, where meters are connected.
Agree the definitions with operations before building visuals: what counts as planned downtime, the ideal rate for each product, when each shift starts. If those live in the data model rather than in individual reports, every report agrees. More on our production reporting and Power BI work.
Layer 5b
An API for every other system
Power BI is one consumer. It won't be the last.
Sooner or later the MES wants line states, ERP wants production counts, a customer wants batch data, or another team wants to build an app. Handing each of them database credentials doesn't scale and isn't safe.
So we put an ASP.NET Core API in front of the database for application-to-application (A2A) integration, and for clients and partners who need the data.
- Read-only by default. Current values, history over a time range, rollups by shift, and events such as downtime.
- Authenticated. API keys or OAuth 2.0 client credentials, for example through Microsoft Entra ID, with each consumer given only the lines and data it needs.
- Documented. An OpenAPI description, so an MES vendor or your customer's developers can integrate without a meeting.
- Protected. Rate limits, paging and sensible maximum time ranges, so one heavy query can't slow everyone else down.
- Versioned. Changes ship as a new version, so existing integrations keep working.
The same API can drive a live web dashboard on the floor, which is often a better fit than Power BI for second-by-second line status.
Security
Keeping the plant network safe
OT security isn't an add-on. The architecture is shaped around it.
- Read-only, no program changes. The integration layer reads tags. It doesn't touch the PLC program, and writes stay disabled unless agreed.
- Segmented networks. The edge box sits on the plant network or in an industrial DMZ between the plant and business networks, in line with a Purdue-style model. Controllers are never exposed to the office network or the internet.
- Outbound only. The service starts every connection: down to the PLCs and out to the database. Nothing outside the plant needs a route in.
- Least privilege. The service's database account can insert telemetry and nothing else. The API and Power BI use read-only accounts.
- Secrets out of config files. Credentials live in the operating system's secret store or a vault, not in plain text next to the tag list.
- Patched and logged. The edge box is maintained like any other server, and the service logs what connected and when.
Rollout
Rolling it out
Start with one line. Grow by configuration.
- Survey the plant. List every PLC: make, model, firmware and communications modules. Map the network, any existing SCADA or historian, and who owns each PLC program.
- Agree the tags and the KPIs. Work with operations and the controls engineer on which tags matter, what they mean, and how each KPI is defined. This step does more for data quality than any code.
- Pilot one line. Stand up the integration service, the database and one dashboard. Run it alongside the existing paper or spreadsheet numbers until operations agree they match.
- Harden it. Pull the network cable and check the buffer catches up. Restart the PLC comms. Set up health alerts and write the runbook.
- Scale. Add lines and sites through configuration, then open the API to the systems that need the data.
Pitfalls
Mistakes we see
- Polling every tag as fast as possible, then wondering why the PLC's comms are busy.
- Reading hundreds of tags one request at a time instead of as arrays or structures.
- Stamping data in local time, then losing or doubling an hour at each daylight saving change.
- Recording a comms timeout as zero, so the line looks stopped when it wasn't.
- Storing only the latest value, so there's no history to report on.
- No local buffer, so every database or network hiccup leaves a hole in the data.
- KPI formulas copied into each report, until two reports disagree in the morning meeting.
- Pointing Power BI at raw readings instead of rollups, then blaming Power BI for being slow.
- Writing to the PLC from a reporting project without change control.
How we do it
How Nfinite builds this
This is the pattern we use on real plants, built in C# and .NET from the PLC reader through to the API.
For CSR, we connected directly to legacy PLCs on a production line rather than replacing them. The data feeds a live line dashboard showing sheets per minute, line speed, scrap and delay against target, cumulative volume through the shift against plan, and an outage log with the reason for each stop. An API exposes the same data to other systems.
Every plant is different in its PLCs and its targets, but the layers stay the same. That's what lets us start small on one line and grow without a rewrite. And we stay on the hook after go-live, with remote support and monitoring.
The same pipeline carries results from our machine vision inspection systems, so rejects by defect type sit beside OEE in the same reports.
See our PLC and legacy integration work, or tell us what you're running.
Proof · CSR
Modern reporting on top of legacy PLCs.
We integrated directly with CSR's existing PLCs, then exposed clean data through modern reporting and an API.
Common questions
Questions about PLC data and Power BI
Can Power BI connect directly to a PLC?
Not in any practical way. Power BI has no native connector for PLC protocols such as EtherNet/IP, Modbus or S7, and even with one you'd be querying the controller that runs the line every time someone opens a report. Put an integration layer and a database in between, and point Power BI at the database.
Does reading data affect the PLC?
Reads go through the PLC's communications, not its logic, so nothing about how the line runs changes. A well-built reader adds a small, predictable comms load: sensible poll rates, tags read as arrays or structures rather than one at a time, and testing on the line during the pilot. We don't change the PLC program.
Do we need OPC UA?
Not necessarily. If you already have an OPC UA server, in a newer PLC or a SCADA system, we can use it. For older Allen-Bradley, Omron and Modbus kit, reading directly with libplctag is often simpler and avoids another server to license and maintain. Both sit behind the same interface, so you can mix them.
Which database should we use?
Usually whatever your organisation already runs and supports. PostgreSQL or SQL Server suit a site-level setup. Snowflake or Databricks make sense if head office already has a data platform. Because the integration layer writes through an abstraction layer, you can start on one and move to another later without touching the plant side.
How close to real time can Power BI be?
The integration layer can read every second where it matters, and data lands in the database within seconds. Power BI in Import mode refreshes on a schedule set by your licence. DirectQuery with automatic page refresh gets closer to live. For a second-by-second line screen, a lightweight web dashboard on the API is usually the better tool, with Power BI handling shift, daily and weekly reporting.
Can you do Siemens S7?
Yes. libplctag doesn't speak S7, so S7 PLCs are read with a dedicated S7 driver, or through OPC UA where the CPU supports it (newer S7-1200 and S7-1500 CPUs include an OPC UA server). The readings land in the same database alongside your Allen-Bradley and Omron data. See our PLC and legacy integration page.
Get in touch
Got PLC data stuck on the floor?
Tell us which PLCs you run and where the data needs to go. We'll tell you straight how we'd connect it.