<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Marius Gjerd</title>
    <description>The latest articles on DEV Community by Marius Gjerd (@mariusgjerd).</description>
    <link>https://dev.to/mariusgjerd</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4024546%2F5b0a0212-cd82-4f0e-be31-63ae2bf42ea4.jpeg</url>
      <title>DEV Community: Marius Gjerd</title>
      <link>https://dev.to/mariusgjerd</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mariusgjerd"/>
    <language>en</language>
    <item>
      <title>What 4-20mA actually means (and why your zero is at 4)</title>
      <dc:creator>Marius Gjerd</dc:creator>
      <pubDate>Sun, 26 Jul 2026 10:00:18 +0000</pubDate>
      <link>https://dev.to/mariusgjerd/what-4-20ma-actually-means-and-why-your-zero-is-at-4-7fh</link>
      <guid>https://dev.to/mariusgjerd/what-4-20ma-actually-means-and-why-your-zero-is-at-4-7fh</guid>
      <description>&lt;p&gt;The temperature in your database was, at some point, a current in a wire. Not as a metaphor. Actual milliamps through copper. Almost everything your code knows about a physical process traveled as an electrical current through a cable before it became a number, and the rules for that journey were written in the 1950s.&lt;/p&gt;

&lt;p&gt;The standard is called 4-20mA. It is old, it is analog, and it still carries most of the world’s industrial sensor data. I have pulled the cables for it as an electrician and consumed the numbers from it as a developer, and the two jobs taught me very different things about the same signal. This post is what I wish the developer side knew.&lt;/p&gt;


&lt;p&gt;Words in this post, in plain language&lt;/p&gt;
&lt;br&gt;
  &lt;ul&gt;

&lt;li&gt;

&lt;strong&gt;Transmitter:&lt;/strong&gt; the measurement converter mounted on or near the sensor. It turns the sensor’s raw signal (the resistance of a PT100, for example) into a standardized 4-20 mA current.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;PLC (programmable logic controller):&lt;/strong&gt; the industrial computer that runs the control logic of a plant. It reads inputs, executes its program and sets outputs, over and over, in real time. In this post we care about its analog inputs.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Span:&lt;/strong&gt; the measuring range the transmitter is configured with, meaning which physical values 4 and 20 mA correspond to. For example 0-200°C.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Frequency drive:&lt;/strong&gt; power electronics that control the speed of a motor by varying the frequency and voltage it is fed. Found on everything from fans to pumps, and it returns in the noise section below.&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why your zero is at 4
&lt;/h2&gt;

&lt;p&gt;A 4-20mA sensor maps its measuring range onto a current. 4 mA means the bottom of the range, 20 mA means the top. If the sensor measures 0-200°C, then 4 mA is 0 degrees and 20 mA is 200 degrees. Whatever sits between is your value, scaled linearly.&lt;/p&gt;

&lt;p&gt;The first question everyone asks: why does zero sit at 4 and not at 0? The answer is the smartest part of the whole standard. 0 mA is reserved for failure. A healthy loop always carries at least 4 mA, so if the current drops to zero, something is wrong. Broken wire, dead transmitter, loop lost its power. The fault detection is built into the physics itself, which means a dead sensor cannot pretend to be a cold pipe.&lt;/p&gt;

&lt;p&gt;There is a convention that makes this precise, NAMUR NE43. Normal measurement information lives between 3.8 and 20.5 mA. Anything below 3.6 mA or above 21.0 mA is a failure signal. Transmitters are configured to drive the current high or low when the sensor itself fails (this is called burnout direction), and your code needs to know which way your transmitters are set. A sensor failing upwards looks exactly like a process that is overheating, if you insist on treating it as a measurement.&lt;/p&gt;

&lt;p&gt;So read the sensor documentation and know what to expect. A value below 3.6 mA is not a very cold pipe. It is a broken wire telling you about itself, in the only language it has.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  &amp;lt;br&amp;gt;
    .lbl { font-family: ui-sans-serif, system-ui, sans-serif; font-size: 13px; fill: var(--content, #333); }&amp;lt;br&amp;gt;
    .sub { font-family: ui-sans-serif, system-ui, sans-serif; font-size: 12px; fill: var(--secondary, #666); }&amp;lt;br&amp;gt;
    .leg { font-family: ui-sans-serif, system-ui, sans-serif; font-size: 11px; fill: var(--content, #333); }&amp;lt;br&amp;gt;
    .fault-lbl { font-family: ui-sans-serif, system-ui, sans-serif; font-size: 12px; font-weight: 600; fill: #d64545; }&amp;lt;br&amp;gt;
    .sig-lbl { font-family: ui-sans-serif, system-ui, sans-serif; font-size: 13px; font-weight: 600; fill: #ffffff; }&amp;lt;br&amp;gt;
    .tick { stroke: var(--secondary, #666); stroke-width: 1; }&amp;lt;br&amp;gt;
    .map { stroke: var(--border, #bbb); stroke-width: 1; stroke-dasharray: 4 4; }&amp;lt;br&amp;gt;
  &lt;/p&gt;

&lt;p&gt;&amp;lt;!-- mA bar: x = 40 + mA * (640/22) --&amp;gt;&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  &lt;/p&gt;

&lt;p&gt;measurement range&lt;br&gt;
  fault: fail low&lt;br&gt;
  fault: fail high&lt;/p&gt;

&lt;p&gt;&amp;lt;!-- ticks and numbers --&amp;gt;&lt;br&gt;
  &lt;br&gt;
  0 mA&lt;br&gt;
  &lt;br&gt;
  3.6&lt;br&gt;
  &lt;br&gt;
  4&lt;br&gt;
  &lt;br&gt;
  12&lt;br&gt;
  &lt;br&gt;
  20&lt;br&gt;
  &lt;br&gt;
  21&lt;/p&gt;

&lt;p&gt;&amp;lt;!-- mapping to engineering units --&amp;gt;&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  0 °C&lt;br&gt;
  100 °C&lt;br&gt;
  200 °C&lt;br&gt;
  what your code sees, after the span (here 0-200 °C) is applied&lt;/p&gt;

&lt;p&gt;&amp;lt;!-- legend: every zone named with its range --&amp;gt;&lt;br&gt;
  &lt;br&gt;
  measurement range (4-20 mA)&lt;br&gt;
  &lt;br&gt;
  NE43 margin (3.8-4/20-20.5)&lt;br&gt;
  &lt;br&gt;
  dead band (3.6-3.8/20.5-21)&lt;br&gt;
  &lt;br&gt;
  fault (&amp;lt;3.6/&amp;gt;21)&lt;br&gt;
&lt;br&gt;
&lt;em&gt;The whole scale in one picture: fault zones outside 3.6 and 21 mA, the NE43 margins in faint blue, dead bands in gray between them, and the linear mapping to engineering units underneath.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why current and not voltage
&lt;/h2&gt;

&lt;p&gt;Two reasons. First, current is the same everywhere in the loop. A voltage signal drops along the cable, and the longer the cable, the more you lose. Current does not have that problem. The loop can run through half a building and the milliamps arriving at the panel are the milliamps the transmitter set.&lt;/p&gt;

&lt;p&gt;Second, a current loop shrugs off electrical noise far better than a voltage signal does. Industrial buildings are electrically loud places. More on that below, because I have a story.&lt;/p&gt;

&lt;p&gt;The wiring itself is almost elegant. A typical loop is just two wires, and those same two wires carry both the power supply (usually 24 VDC) and the signal. The transmitter sits in the loop and regulates how much current it lets through. On the receiving end, the analog input card often just measures the current across a 250 ohm resistor, which turns 4-20 mA into 1-5 V that the card can read. Two wires, hundreds of meters, powered and talking at the same time.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  &amp;lt;br&amp;gt;
    .wire { stroke: var(--content, #333); stroke-width: 2; fill: none; }&amp;lt;br&amp;gt;
    .box { fill: var(--entry, #f6f6f6); stroke: var(--content, #333); stroke-width: 1.5; }&amp;lt;br&amp;gt;
    .lbl { font-family: ui-sans-serif, system-ui, sans-serif; font-size: 14px; fill: var(--content, #333); }&amp;lt;br&amp;gt;
    .sub { font-family: ui-sans-serif, system-ui, sans-serif; font-size: 12px; fill: var(--secondary, #666); }&amp;lt;br&amp;gt;
    .pol { font-family: ui-sans-serif, system-ui, sans-serif; font-size: 13px; font-weight: 700; fill: var(--content, #333); }&amp;lt;br&amp;gt;
    .sig { font-family: ui-sans-serif, system-ui, sans-serif; font-size: 13px; font-weight: 600; fill: #2f6fdd; }&amp;lt;br&amp;gt;
    .term { fill: var(--content, #333); }&amp;lt;br&amp;gt;
    .arrow { fill: #2f6fdd; }&amp;lt;br&amp;gt;
  &lt;/p&gt;

&lt;p&gt;&amp;lt;!-- transmitter, top center, separated from both --&amp;gt;&lt;br&gt;
  &lt;br&gt;
  transmitter&lt;br&gt;
  + sensor (2-wire)&lt;br&gt;
  &lt;br&gt;
  +&lt;br&gt;
  &lt;br&gt;
  −&lt;/p&gt;

&lt;p&gt;&amp;lt;!-- power supply, left --&amp;gt;&lt;br&gt;
  &lt;br&gt;
  24 VDC&lt;br&gt;
  power supply&lt;br&gt;
  &lt;br&gt;
  +&lt;br&gt;
  &lt;br&gt;
  −&lt;/p&gt;

&lt;p&gt;&amp;lt;!-- PLC analog input, right --&amp;gt;&lt;br&gt;
  &lt;br&gt;
  PLC analog input&lt;br&gt;
  250 Ω → 1-5 V&lt;br&gt;
  &lt;br&gt;
  AI+&lt;br&gt;
  &lt;br&gt;
  AI−&lt;/p&gt;

&lt;p&gt;&amp;lt;!-- wire: supply + to transmitter +, right-angle routing --&amp;gt;&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  4-20 mA&lt;/p&gt;

&lt;p&gt;&amp;lt;!-- wire: transmitter - to PLC AI+ --&amp;gt;&lt;br&gt;
  &lt;br&gt;
  &lt;/p&gt;

&lt;p&gt;&amp;lt;!-- return wire: PLC AI- to supply - --&amp;gt;&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  return&lt;/p&gt;

&lt;p&gt;&amp;lt;!-- documentation --&amp;gt;&lt;br&gt;
  cable: 2 x 0.75 mm² shielded twisted pair, shield grounded at one end&lt;br&gt;
  series loop: the same 4-20 mA flows through every component&lt;br&gt;
&lt;br&gt;
&lt;em&gt;One loop, three components in series. The transmitter regulates the current, and the same milliamps flow through every point in the loop.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  From current to a number
&lt;/h2&gt;

&lt;p&gt;The current becomes a number through the transmitter’s span. The span is the configured measuring range, and the transmitter maps it onto 4-20 mA linearly. With a span of 0-200°C, 12 mA means 100 degrees. Simple.&lt;/p&gt;

&lt;p&gt;Here is the danger: the span lives in the transmitter, not in your code. If someone reconfigures the range to 0-150°C and does not tell anyone downstream, every value in your pipeline is scaled wrong from that moment on. Silently. The numbers still look plausible, no error, no gap, just a temperature that is no longer the temperature. If a measurement setup changes, the configuration and the documentation have to move together, because the person consuming the data three systems away has no other way to know.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  &amp;lt;br&amp;gt;
    .lbl { font-family: ui-sans-serif, system-ui, sans-serif; font-size: 13px; fill: var(--content, #333); }&amp;lt;br&amp;gt;
    .sub { font-family: ui-sans-serif, system-ui, sans-serif; font-size: 12px; fill: var(--secondary, #666); }&amp;lt;br&amp;gt;
    .ok { font-family: ui-sans-serif, system-ui, sans-serif; font-size: 14px; font-weight: 700; fill: #2f6fdd; }&amp;lt;br&amp;gt;
    .bad { font-family: ui-sans-serif, system-ui, sans-serif; font-size: 14px; font-weight: 700; fill: #d64545; }&amp;lt;br&amp;gt;
    .track { fill: var(--border, #d8d8d8); opacity: 0.55; }&amp;lt;br&amp;gt;
    .dash { stroke: var(--content, #333); stroke-width: 1.5; stroke-dasharray: 5 4; }&amp;lt;br&amp;gt;
  &lt;/p&gt;

&lt;p&gt;&amp;lt;!-- 12 mA line: x = 120 + (12-4)/16 * 500 = 370 --&amp;gt;&lt;br&gt;
  the same 12 mA&lt;br&gt;
  &lt;/p&gt;

&lt;p&gt;&amp;lt;!-- row 1: the transmitter's actual span --&amp;gt;&lt;br&gt;
  span in the transmitter: 0-150 °C&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  = 75 °C, what it sends&lt;br&gt;
  4 mA = 0 °C&lt;br&gt;
  20 mA = 150 °C&lt;/p&gt;

&lt;p&gt;&amp;lt;!-- row 2: the span your code assumes --&amp;gt;&lt;br&gt;
  span your code assumes: 0-200 °C&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  = 100 °C, what you compute&lt;br&gt;
  4 mA = 0 °C&lt;br&gt;
  20 mA = 200 °C&lt;/p&gt;

&lt;p&gt;same current, two different temperatures. The transmitter always wins, silently.&lt;br&gt;
&lt;br&gt;
&lt;em&gt;The span problem in one picture. The current is correct, the assumption is not, and nothing in the data tells you.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;(Some loops also run HART, a digital signal on top of the same 4-20 mA wires, which can carry diagnostics and configuration. Worth knowing it exists.)&lt;/p&gt;

&lt;p&gt;One honest note about the last step. Inside the PLC, the analog card converts the current into raw integer counts, and software scales those counts into engineering units. I have seen those raw counts in systems I integrate against, and I have learned not to assume I know which scaling sits behind a value. The converter layer itself is a part of the chain I have not worked hands-on with yet. I start my industrial automation degree this fall and that layer is high on my list. When I have measured it myself, it gets its own post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Noise, or the days we spent chasing ghost data
&lt;/h2&gt;

&lt;p&gt;From my electrician years. We were on a project in a large building, pulling cables from a huge number of sensors to local nodes, and from the nodes into a main panel where all the building’s sensor data came together. Lots of cable, lots of runs, everything landed and terminated. Our part was done.&lt;/p&gt;

&lt;p&gt;Then the automation guys started commissioning, and the data was garbage. Not dead, which would have been easier, but wrong and weird and jumpy. They spent days troubleshooting before someone found it: the sensor cables were unshielded, and long stretches of them were lying on the same cable ladder as high voltage power cables. The power lines were inducing noise straight into the signal wires, and every reading arriving in that panel had a little bit of the building’s electrical activity mixed into it.&lt;/p&gt;

&lt;p&gt;This is what electrical noise looks like from the data side: values that vibrate a few counts around the real reading, and sudden jumps that look like process changes but are actually a motor starting somewhere. Frequency drives are notorious for this. Shielding helps, grounding the shield correctly matters (grounding it at both ends can create its own problem, a ground loop), and physical distance from power cables is the cheapest fix of all. None of which your code can see. It just sees a temperature that trembles.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  &amp;lt;br&amp;gt;
    .lbl { font-family: ui-sans-serif, system-ui, sans-serif; font-size: 13px; fill: var(--content, #333); }&amp;lt;br&amp;gt;
    .sub { font-family: ui-sans-serif, system-ui, sans-serif; font-size: 12px; fill: var(--secondary, #666); }&amp;lt;br&amp;gt;
    .noise-lbl { font-family: ui-sans-serif, system-ui, sans-serif; font-size: 12px; font-weight: 600; fill: #d64545; }&amp;lt;br&amp;gt;
    .ladder { stroke: var(--border, #bbb); stroke-width: 2; }&amp;lt;br&amp;gt;
    .power { stroke: var(--content, #333); stroke-width: 9; stroke-linecap: round; }&amp;lt;br&amp;gt;
    .signal { stroke: #2f6fdd; stroke-width: 2.5; fill: none; }&amp;lt;br&amp;gt;
    .noise { stroke: #d64545; stroke-width: 1.8; fill: none; }&amp;lt;br&amp;gt;
  &lt;/p&gt;

&lt;p&gt;&amp;lt;!-- cable ladder --&amp;gt;&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  &lt;/p&gt;

&lt;p&gt;&amp;lt;!-- power cable --&amp;gt;&lt;br&gt;
  &lt;br&gt;
  power cable (motors, frequency drives)&lt;/p&gt;

&lt;p&gt;&amp;lt;!-- unshielded signal pair --&amp;gt;&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  unshielded 4-20 mA signal pair, same ladder&lt;/p&gt;

&lt;p&gt;&amp;lt;!-- induced noise arrows --&amp;gt;&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  induced noise&lt;/p&gt;

&lt;p&gt;&amp;lt;!-- sent vs read --&amp;gt;&lt;br&gt;
  &lt;br&gt;
  what the transmitter sent&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  what the PLC read&lt;br&gt;
&lt;br&gt;
&lt;em&gt;The project in one drawing: unshielded signal cable on the same ladder as power cables. The transmitter sends a clean signal, the PLC reads a trembling one.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What your code should assume
&lt;/h2&gt;

&lt;p&gt;The practical takeaways, same spirit as &lt;a href="https://dev.to/posts/trust-your-sensor-data/"&gt;the sensor data post&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Treat anything below 3.6 mA or above 21 mA as a fault, never as a measurement. Do not let fault currents sneak into averages and aggregates.&lt;/li&gt;
&lt;li&gt;Know the burnout direction of your transmitters. A sensor that fails high should not page anyone about an overheating process.&lt;/li&gt;
&lt;li&gt;Do not trust that the span in the documentation is the span in the transmitter. Verify when you can, and log loudly when values stop making physical sense.&lt;/li&gt;
&lt;li&gt;Expect a few counts of noise. Alert thresholds tighter than the noise floor produce alerts about electricity, not about the process.&lt;/li&gt;
&lt;li&gt;If you can, log the raw mA or counts alongside the scaled value. It is the only way to debug a scaling problem after the fact.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;br&gt;
  &amp;lt;br&amp;gt;
    .box { fill: var(--entry, #f6f6f6); stroke: var(--content, #333); stroke-width: 1.5; }&amp;lt;br&amp;gt;
    .fbox { fill: none; stroke: #d64545; stroke-width: 1.8; }&amp;lt;br&amp;gt;
    .lbl { font-family: ui-sans-serif, system-ui, sans-serif; font-size: 13px; fill: var(--content, #333); }&amp;lt;br&amp;gt;
    .sub { font-family: ui-sans-serif, system-ui, sans-serif; font-size: 11.5px; fill: var(--secondary, #666); }&amp;lt;br&amp;gt;
    .yes { font-family: ui-sans-serif, system-ui, sans-serif; font-size: 12px; font-weight: 700; fill: #2f6fdd; }&amp;lt;br&amp;gt;
    .no { font-family: ui-sans-serif, system-ui, sans-serif; font-size: 12px; font-weight: 700; fill: #d64545; }&amp;lt;br&amp;gt;
    .fault { font-family: ui-sans-serif, system-ui, sans-serif; font-size: 13px; font-weight: 600; fill: #d64545; }&amp;lt;br&amp;gt;
    .wire { stroke: var(--content, #333); stroke-width: 1.8; fill: none; }&amp;lt;br&amp;gt;
    .head { fill: var(--content, #333); }&amp;lt;br&amp;gt;
  &lt;/p&gt;

&lt;p&gt;&amp;lt;!-- value arrives --&amp;gt;&lt;br&gt;
  &lt;br&gt;
  value arrives&lt;br&gt;
  mA or raw counts&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  &lt;/p&gt;

&lt;p&gt;&amp;lt;!-- range check --&amp;gt;&lt;br&gt;
  &lt;br&gt;
  inside 3.8-20.5 mA?&lt;br&gt;
  the zones from above&lt;/p&gt;

&lt;p&gt;&amp;lt;!-- no: fault path --&amp;gt;&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  no&lt;br&gt;
  &lt;br&gt;
  fault path&lt;br&gt;
  alarm it, keep it out of stats&lt;/p&gt;

&lt;p&gt;&amp;lt;!-- yes: continue --&amp;gt;&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  yes&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  apply the span&lt;br&gt;
  validate range + rate&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  &lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  store scaled&lt;br&gt;
  + raw value&lt;br&gt;
&lt;br&gt;
&lt;em&gt;The whole list as an ingestion flow: fault currents take the alarm path, everything else gets scaled, validated and stored with the raw value.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And the biggest one, the assumption behind all the others: assume that nobody has the full picture. The electrician who pulled the cable, the automation tech who commissioned the system, the project engineer sitting on the datasheets, and you, the developer consuming the number at the end. Every one of them knows their own layer. Nobody is checking yours against theirs unless you ask.&lt;/p&gt;

&lt;p&gt;The current loop is honest. It is your assumptions about it that lie.&lt;/p&gt;

&lt;p&gt;This post is step two in a chain. Step one, what happens to the value after it becomes a number, is here: &lt;a href="https://dev.to/posts/trust-your-sensor-data/"&gt;Can you trust your sensor data?&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://mariusgjerd.github.io/posts/what-4-20ma-actually-means/" rel="noopener noreferrer"&gt;mariusgjerd.github.io&lt;/a&gt;, where I write about what happens when code touches the real world. New posts also go out by email: &lt;a href="https://marius-newsletter-d94bcd.beehiiv.com/" rel="noopener noreferrer"&gt;subscribe here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>420ma</category>
      <category>sensors</category>
      <category>plc</category>
      <category>scada</category>
    </item>
    <item>
      <title>Can you trust your sensor data? A developer's guide to the full stack</title>
      <dc:creator>Marius Gjerd</dc:creator>
      <pubDate>Mon, 13 Jul 2026 20:30:19 +0000</pubDate>
      <link>https://dev.to/mariusgjerd/can-you-trust-your-sensor-data-a-developers-guide-to-the-full-stack-3376</link>
      <guid>https://dev.to/mariusgjerd/can-you-trust-your-sensor-data-a-developers-guide-to-the-full-stack-3376</guid>
      <description>&lt;p&gt;Every data pipeline in industrial IoT starts the same way: something physical happens, a sensor detects it, and a number appears in your database.&lt;/p&gt;

&lt;p&gt;The gap between “something physical happens” and “a number appears in your database” is where most data quality problems live. If you’ve only ever worked on the database side, that gap is invisible. This post is a map of it.&lt;/p&gt;

&lt;p&gt;We’ll trace a single temperature reading from a heat exchanger through to a cloud application, step by step. At each stage I’ll show what the data looks like, what can go wrong, and what your code should account for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 1: The physical measurement
&lt;/h2&gt;

&lt;p&gt;A PT100 resistance temperature detector is embedded in a pipe. Its resistance changes with temperature, predictably but not perfectly. Every PT100 has a tolerance class. Class B (the cheaper option) has an accuracy of ±0.3°C at 0°C, widening to ±1.3°C at 300°C.&lt;/p&gt;

&lt;p&gt;Your sensor is already an approximation.&lt;/p&gt;

&lt;p&gt;There’s also the installation to consider. Is the sensor mounted correctly in the flow? Is there air around it that might affect the reading? Has it been in service for 10 years without recalibration? Sensor drift, where a sensor’s output gradually deviates from the true value over time, is common and often undetected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to remember:&lt;/strong&gt; The physical measurement has inherent uncertainty. Your data has error bars even before it leaves the sensor. ±1°C is normal. On an alert threshold of ±0.5°C, that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 2: Signal transmission (4-20mA)
&lt;/h2&gt;

&lt;p&gt;The PT100’s resistance is converted to a 4-20mA current signal by a transmitter. This current travels over a cable to an input card in a PLC or remote I/O.&lt;/p&gt;

&lt;p&gt;4-20mA is a very old standard and deliberately hard to break: it’s a current loop, so resistance in the cable doesn’t affect the reading, and a broken wire is detectable (0mA means fault, not zero). But it has limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The 16mA range (4 to 20) maps to your full engineering range (say, 0-200°C). Your resolution is limited by how finely the analog-to-digital converter on the input card can divide that range.&lt;/li&gt;
&lt;li&gt;Cable length and electromagnetic interference can introduce noise. A variable-frequency motor drive nearby can inject enough noise to make a reading jump by several counts.&lt;/li&gt;
&lt;li&gt;The current-to-value conversion uses a calibrated span. If the transmitter is configured for 0-200°C but the range has changed to 0-150°C and nobody reconfigured it, every reading is scaled wrong.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What to remember:&lt;/strong&gt; By the time the physical measurement has become a digital number in the PLC, it has already been scaled, quantised, and potentially affected by electrical noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 3: The PLC scan cycle
&lt;/h2&gt;

&lt;p&gt;The PLC reads its inputs, executes its logic, and updates its outputs, in a cycle that repeats every 10-100ms. When you request a value from the PLC, you get whatever was in memory from the last scan.&lt;/p&gt;

&lt;p&gt;This introduces a latency of up to one scan cycle between the physical world and the PLC’s view of it. For slow processes (temperatures, pressures, levels) this is irrelevant. For fast events (a valve that opens for 20ms), you might miss it entirely.&lt;/p&gt;

&lt;p&gt;The timestamp on a PLC value is usually the time it was read by the SCADA system, not the time the physical event occurred. These can differ by seconds depending on polling frequency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to remember:&lt;/strong&gt; PLC values are samples, not a continuous stream. Events faster than the scan cycle are invisible. The timestamp is often “when we asked”, not “when it happened.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 4: SCADA / historian
&lt;/h2&gt;

&lt;p&gt;The SCADA system polls the PLC and stores values in a historian, a time-series database built for industrial data. Historians use a technique called &lt;em&gt;exception reporting&lt;/em&gt; or &lt;em&gt;deadbanding&lt;/em&gt;: they only store a new value if the reading has changed by more than a configured threshold.&lt;/p&gt;

&lt;p&gt;If your temperature is stable at 73.2°C, the historian might store that value once and then not again for an hour. This is efficient for storage, but it means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Querying the historian for “the value at 14:32:17” returns the last recorded value before that time, which might be from 14:00. If you’re not aware of this, your trend looks flat because it is flat in the database, not necessarily flat in reality.&lt;/li&gt;
&lt;li&gt;The threshold for “changed enough to store” is a configuration parameter. If someone set it to 5°C to save storage, you’ll never see small fluctuations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What to remember:&lt;/strong&gt; Historian data is not a complete record of what happened. It’s a compressed approximation. Flat lines in your data might mean “nothing changed” or “we stopped recording small changes.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 5: The cloud pipeline
&lt;/h2&gt;

&lt;p&gt;The historian exports data to your cloud pipeline via an MQTT broker, OPC-UA server, or a bespoke API. Each of these adds its own considerations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Buffering:&lt;/strong&gt; If the connection drops, does the local system buffer unsent data? For how long? What happens at reconnect? Does it send a burst, or drop the gap?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resampling:&lt;/strong&gt; If your cloud pipeline expects data at 1-minute intervals but the historian sends on-change, you’ll need to resample. Forward-fill? Interpolate? The choice changes the data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time zones:&lt;/strong&gt; Industrial systems often use local time. Your cloud pipeline probably expects UTC. If the historian is in Norway (CET/CEST), you have a seasonal 1-hour offset to handle.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What to remember:&lt;/strong&gt; The cloud pipeline introduces its own latency, buffering behaviour, and resampling decisions. Each one is a transformation that should be explicit in your schema and documented.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 6: Your application
&lt;/h2&gt;

&lt;p&gt;By the time a temperature reading reaches your application, it has been:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Measured with inherent uncertainty (±1°C or more)&lt;/li&gt;
&lt;li&gt;Transmitted as an analog signal and converted to digital&lt;/li&gt;
&lt;li&gt;Sampled at the PLC scan cycle&lt;/li&gt;
&lt;li&gt;Compressed by deadbanding in the historian&lt;/li&gt;
&lt;li&gt;Transmitted over a network that may buffer or drop values&lt;/li&gt;
&lt;li&gt;Resampled to fit your pipeline’s cadence&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The number in your database is not “the temperature at that time.” It’s the best available approximation, given all of the above.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do about it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Document the uncertainty.&lt;/strong&gt; Add metadata to your schema: sensor type, accuracy class, historian deadband, poll interval. Future you, or the developer debugging an anomaly at 2am, needs this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Be conservative with alert thresholds.&lt;/strong&gt; If your alert fires when temperature exceeds 95°C and your sensor accuracy is ±1.5°C at that range, your alert might be lying.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don’t interpolate silently.&lt;/strong&gt; If you resample sparse historian data, make it visible. A value filled from a reading that’s 30 minutes old is different from a value sampled 10 seconds ago.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Validate range and rate-of-change.&lt;/strong&gt; A PT100 measuring a pipe temperature that jumps from 70°C to 500°C in one second is probably a sensor fault, not reality. Industrial processes have physical limits. Enforce them at ingestion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ask the operators.&lt;/strong&gt; The people who run the plant know which sensors are reliable and which “always read a bit high.” That knowledge is rarely in the documentation.&lt;/p&gt;




&lt;p&gt;The sensor data you’re working with is honest about what it is, if you know how to read it. The full stack is the context. Without it, you’re optimising code on top of uncertainty you haven’t accounted for.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://mariusgjerd.github.io/posts/trust-your-sensor-data/" rel="noopener noreferrer"&gt;mariusgjerd.github.io&lt;/a&gt;, where I write about what happens when code touches the real world. New posts also go out by email: &lt;a href="https://marius-newsletter-d94bcd.beehiiv.com/" rel="noopener noreferrer"&gt;subscribe here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>iot</category>
      <category>data</category>
      <category>scada</category>
      <category>programming</category>
    </item>
    <item>
      <title>Your uptime SLA means nothing when the physical process can't wait for your rollback</title>
      <dc:creator>Marius Gjerd</dc:creator>
      <pubDate>Sun, 12 Jul 2026 20:52:59 +0000</pubDate>
      <link>https://dev.to/mariusgjerd/your-uptime-sla-means-nothing-when-the-physical-process-cant-wait-for-your-rollback-12i2</link>
      <guid>https://dev.to/mariusgjerd/your-uptime-sla-means-nothing-when-the-physical-process-cant-wait-for-your-rollback-12i2</guid>
      <description>&lt;p&gt;There’s a conversation that happens when IT developers first encounter operational technology. It usually goes something like this:&lt;/p&gt;

&lt;p&gt;“What’s your uptime requirement?”&lt;/p&gt;

&lt;p&gt;“99.9%.”&lt;/p&gt;

&lt;p&gt;“That’s about 8 hours of downtime per year. We can work with that.”&lt;/p&gt;

&lt;p&gt;“No, 99.9% &lt;em&gt;per shift&lt;/em&gt;. We run 24/7. And if the system goes down mid-batch, we lose the entire batch.”&lt;/p&gt;

&lt;p&gt;The IT developer nods, makes a note, and quietly recalculates.&lt;/p&gt;

&lt;h2&gt;
  
  
  What uptime means in IT
&lt;/h2&gt;

&lt;p&gt;In web services, uptime is a statistical measure. Your load balancer distributes traffic. A rolling deployment takes down one instance at a time. If a deploy goes wrong, you roll back. The user retries their request. Maybe they see an error page for a few seconds.&lt;/p&gt;

&lt;p&gt;99.9% uptime means roughly 8.7 hours of downtime per year. For most web applications, that’s acceptable. For the unlucky users who hit those 8.7 hours, it’s annoying but not catastrophic.&lt;/p&gt;

&lt;p&gt;The whole model assumes the system’s state is recoverable. A failed transaction gets retried. A dropped connection reconnects. Deployments are reversible.&lt;/p&gt;

&lt;h2&gt;
  
  
  What uptime means in OT
&lt;/h2&gt;

&lt;p&gt;In industrial control systems, “the system” is not the software. It’s the physical process the software controls.&lt;/p&gt;

&lt;p&gt;A water treatment plant doesn’t pause while you roll back a SCADA update. A cement kiln running at 1400°C doesn’t wait for your deployment pipeline. A paper machine running at 1000 meters per minute doesn’t retry when your historian goes offline.&lt;/p&gt;

&lt;p&gt;The physical process continues whether the software is healthy or not. And if the software loses control of the process, even briefly, the consequences are measured in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Batches scrapped&lt;/li&gt;
&lt;li&gt;Equipment damaged by running outside safe parameters&lt;/li&gt;
&lt;li&gt;Product out of spec that must be discarded&lt;/li&gt;
&lt;li&gt;In the worst cases: fires, injuries, environmental incidents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your 8.7 hours of acceptable downtime might span three production batches. That’s not an SLA problem. That’s a business continuity problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rollback problem
&lt;/h2&gt;

&lt;p&gt;Web developers treat rollbacks as a safety net. Something went wrong in production? Roll back to the last good version. Worst case, you lose a few minutes of data.&lt;/p&gt;

&lt;p&gt;In industrial systems, rollback is often not an option mid-process.&lt;/p&gt;

&lt;p&gt;If a PLC program update goes wrong mid-batch, you can’t simply restore the previous version and continue. The physical state of the process has changed. Temperatures, pressures, chemical compositions are now different from what the previous version expected. Restoring old software to a new physical state can be more dangerous than finishing the bad deploy.&lt;/p&gt;

&lt;p&gt;This is why industrial systems have change management processes that look bureaucratic and slow to IT eyes: planned maintenance windows, tested rollback procedures, operator sign-offs. They’re not bureaucracy for its own sake. They’re built around the reality that the software controls something that doesn’t stop.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do about it
&lt;/h2&gt;

&lt;p&gt;If you’re writing software that talks to industrial systems, a few things are worth internalising:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Graceful degradation looks different here.&lt;/strong&gt; In web services, graceful degradation means showing a cached page or a friendly error. In OT, it means the control system continuing to operate safely in manual mode while your software is down. Design for that hand-off explicitly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test against the physical constraints, not just the software ones.&lt;/strong&gt; What happens to your system when the network drops for 30 seconds? What does the PLC do? What does the operator see? What alarms trigger?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Understand the process before you touch it.&lt;/strong&gt; The operators who run the plant know things about failure modes that aren’t in any documentation. Talk to them before you write a line of code.&lt;/p&gt;

&lt;p&gt;The SLA number on the contract is the easy part. Understanding what it actually means for the thing being controlled is the hard part.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://mariusgjerd.github.io/posts/bridge-builder-uptime/" rel="noopener noreferrer"&gt;mariusgjerd.github.io&lt;/a&gt;, where I write about what happens when code touches the real world. New posts also go out by email: &lt;a href="https://marius-newsletter-d94bcd.beehiiv.com/" rel="noopener noreferrer"&gt;subscribe here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>sre</category>
      <category>programming</category>
      <category>iot</category>
    </item>
    <item>
      <title>Why this blog exists</title>
      <dc:creator>Marius Gjerd</dc:creator>
      <pubDate>Sun, 12 Jul 2026 20:49:06 +0000</pubDate>
      <link>https://dev.to/mariusgjerd/why-this-blog-exists-47dc</link>
      <guid>https://dev.to/mariusgjerd/why-this-blog-exists-47dc</guid>
      <description>&lt;p&gt;Hi, I’m Marius Gjerd, a developer based in Bergen, Norway, working at the crossroads of code and physical infrastructure.&lt;/p&gt;

&lt;p&gt;I started out as an electrician. Then I learned to write software, and for the past few years I’ve been building IoT solutions in the energy sector. This autumn I’m starting a three-year degree in Industrial Automation alongside my day job. It covers PLCs, instrumentation, industrial networks, and OT security.&lt;/p&gt;

&lt;p&gt;That’s a deliberate move. The bridge between IT and OT, between the cloud-connected world of developers and the physical world of industrial control systems, is somewhere I find genuinely interesting, and a place where very few people are comfortable on both sides.&lt;/p&gt;

&lt;p&gt;This blog is where I’ll write about what I learn along the way. Expect posts on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The strange and useful corners where IT and OT meet&lt;/li&gt;
&lt;li&gt;Industrial cybersecurity from a developer’s perspective&lt;/li&gt;
&lt;li&gt;Hands-on experiments with PLCs, sensors, and home labs&lt;/li&gt;
&lt;li&gt;Lessons from building IoT at scale in energy&lt;/li&gt;
&lt;li&gt;Whatever else I find worth sharing as I work my way through the degree&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’ll write when I have something to say, not on a schedule. Some posts will be technical deep-dives; others will be shorter reflections. All of it will be in my own words, and the code behind the projects will be open source on my GitHub.&lt;/p&gt;

&lt;p&gt;If any of this is interesting to you, stick around.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://mariusgjerd.github.io/posts/welcome/" rel="noopener noreferrer"&gt;mariusgjerd.github.io&lt;/a&gt;, where I write about what happens when code touches the real world. New posts also go out by email: &lt;a href="https://marius-newsletter-d94bcd.beehiiv.com/" rel="noopener noreferrer"&gt;subscribe here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>meta</category>
      <category>career</category>
      <category>ot</category>
      <category>it</category>
    </item>
  </channel>
</rss>
