<?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: Smrati</title>
    <description>The latest articles on DEV Community by Smrati (@smrati_verma).</description>
    <link>https://dev.to/smrati_verma</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3896217%2F617a46f9-c536-46ee-a5ec-a77babd4ef8d.png</url>
      <title>DEV Community: Smrati</title>
      <link>https://dev.to/smrati_verma</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/smrati_verma"/>
    <language>en</language>
    <item>
      <title>10 IoT terms every developer encounters in asset tracking — explained simply</title>
      <dc:creator>Smrati</dc:creator>
      <pubDate>Wed, 27 May 2026 09:07:26 +0000</pubDate>
      <link>https://dev.to/smrati_verma/10-iot-terms-every-developer-encounters-in-asset-tracking-explained-simply-5d2g</link>
      <guid>https://dev.to/smrati_verma/10-iot-terms-every-developer-encounters-in-asset-tracking-explained-simply-5d2g</guid>
      <description>&lt;p&gt;From the first exposure to a piece of IoT asset tracking code, you are confronted by the barrage of terms, including MQTT, RTLS, RSSI, geofencing, and digital twin. This piece will provide clear explanations without any buzzwords or technical assumption. It will give practical examples to illustrate your understanding of each term.&lt;/p&gt;

&lt;h2&gt;
  
  
  01
&lt;/h2&gt;

&lt;p&gt;MQTT&lt;br&gt;
&lt;strong&gt;Protocol&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An open communication protocol used for devices that have little bandwidth and computing power. All devices in the system broadcast messages on certain topics. The message is received by other connected systems interested in these topics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analogy&lt;/strong&gt;&lt;br&gt;
Imagine a radio station. The GPS tracker acts as the broadcaster. The broker functions as the radio tower, while your back end becomes the listening device on that frequency. The tracker doesn't matter who receives its transmissions as long as it is transmitting.&lt;/p&gt;

&lt;h2&gt;
  
  
  02
&lt;/h2&gt;

&lt;p&gt;RSSI&lt;br&gt;
&lt;strong&gt;Signal&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The abbreviation for "Received Signal Strength Indicator". Indicates the strength of wireless signal received at a particular place, measured in dBm (always negative — more towards zero value indicates greater strength). Utilized in BLE indoor positioning technology to gauge the distance between the device and an antenna in a fixed location.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analogy&lt;/strong&gt;: Think of someone speaking in the same room. If their voice sounds more prominent, the chances are that they are standing closer to you. RSSI measures the same thing with radio waves.&lt;/p&gt;

&lt;h2&gt;
  
  
  03
&lt;/h2&gt;

&lt;p&gt;Geofencing&lt;br&gt;
&lt;strong&gt;Location&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A zone established virtually based on a specific physical location that triggers an action once crossed. An asset that crosses this boundary either while leaving or entering this geofenced location will trigger an action event. There can be circular (based on center point and radius) and polygon geofences.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analogy&lt;/strong&gt;: You may draw a line on a map with a marker to represent your warehouse boundaries. Geofencing represents this line digitally — but invisibly and it notifies you instantly once anything enters this invisible zone.&lt;/p&gt;

&lt;h2&gt;
  
  
  04
&lt;/h2&gt;

&lt;p&gt;Digital Twin&lt;br&gt;
&lt;strong&gt;Architecture&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is an instance of a physical asset in a virtual world — that is, a digital entity which represents the exact current state of your physical asset through your backend system. Location, temperature, battery life, and status — everything gets updated as soon as the sensors update data. Your digital twin will always be in sync with your physical asset.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analogy&lt;/strong&gt;: The digital twin of your mobile device on the "Find my" app of your smartphone works in the same way, except that instead of a phone, your asset can be something like a fork lift or some medical equipment.&lt;/p&gt;

&lt;h2&gt;
  
  
  05
&lt;/h2&gt;

&lt;p&gt;RTLS&lt;br&gt;
&lt;strong&gt;System&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Real-Time Locating System. An infrastructural technology used to track the location of the asset inside a building where global positioning systems cannot operate. Gateways or readers pick up signals from tags fitted on your assets and determine their position using RSSI, Ultra Wideband (UWB), and RFID.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analogy&lt;/strong&gt;: While GPS relies on satellites which are outside of the buildings, RTLS replaces the satellites with fixed anchors which can be inside of the building.&lt;/p&gt;

&lt;h2&gt;
  
  
  06
&lt;/h2&gt;

&lt;p&gt;QoS (Quality of Service)&lt;br&gt;
&lt;strong&gt;Protocol&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the MQTT protocol, QoS refers to the efforts that are put in by the broker in delivering the message. QoS 0 = fire and forget (message not guaranteed). QoS 1 = message delivery at least once (possible duplication). QoS 2 = exactly one-time message delivery (slowest but safest). Most asset management systems rely on QoS 1 for location pings and QoS 2 for alerts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analogy&lt;/strong&gt;: QoS 0 – Texting without expecting to hear back (no confirmation of receipt). QoS 1 – Sending a message repeatedly until you are sure the receiver got it. QoS 2 – A registered letter; signed, sealed, and delivered precisely once.&lt;/p&gt;

&lt;h2&gt;
  
  
  07
&lt;/h2&gt;

&lt;p&gt;Dead Letter Queue (DLQ)&lt;br&gt;
&lt;strong&gt;Architecture&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is where the failed attempts at processing the messages end up when no more retries are possible. Rather than deleting the failed IoT messages (and losing the data silently), they are routed to a special queue for debugging purposes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analogy&lt;/strong&gt;: It works just like the dead letter office in a post office; instead of deleting the mail that didn’t reach the receiver, it ends up here where the user can collect it and retry.&lt;/p&gt;

&lt;h2&gt;
  
  
  08
&lt;/h2&gt;

&lt;p&gt;Edge Computing&lt;br&gt;
&lt;strong&gt;Architecture&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Processing of information locally in the device itself or at a gateway nearby without routing it to a distant cloud first. When talking about asset tracking, it allows a smart tag to check whether a temperature threshold is violated and to raise an alarm instantly within milliseconds, instead of waiting for the cloud round trip which would take several seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analogy&lt;/strong&gt;: As opposed to contacting headquarters each time in order to get an answer to something trivial, edge computing is similar to empowering the people who are on site to be able to make decisions by themselves, leaving only critical cases for the higher-ups.&lt;/p&gt;

&lt;h2&gt;
  
  
  09
&lt;/h2&gt;

&lt;p&gt;Telemetry&lt;br&gt;
&lt;strong&gt;Data&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The constant stream of data sent by a particular device to its backend infrastructure. Within the asset tracking domain, the information transmitted consists of location coordinates, sensor readings (temperature, humidity, shock), battery life, and overall health of the device – depending on the frequency of transmission.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analogy&lt;/strong&gt;: Telemetry feed of a race car consisting of speed, tyre pressure, fuel level, and engine temperature. Exactly the way your IoT device works, just applied to your assets.&lt;/p&gt;

&lt;h2&gt;
  
  
  10
&lt;/h2&gt;

&lt;p&gt;Cold Start&lt;br&gt;
&lt;strong&gt;Hardware&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Time taken by the GPS equipment to initialize and receive a satellite fix from scratch. A cold start may take 30-90 seconds since there are no previous satellites in the device memory. This process is much quicker during a warm start, which takes 5-15 seconds. A hot start will only require a fraction of this time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analogy&lt;/strong&gt;: Think of turning on your map application after being off for one week – it may take some time to determine your location. However, if you simply lock and unlock your device, it would show your location immediately.&lt;/p&gt;

&lt;p&gt;Each of these 10 key concepts will pop up all the time once you get started building your asset tracking solution. The good news is that none of them will seem quite as scary at first glance as they do now – each is simply an analogy to something you probably already know about from web application development.&lt;/p&gt;

&lt;p&gt;The best way to understand any new set of technology concepts is to see them put into practice – and that means checking out a real asset tracking platform.&lt;/p&gt;

&lt;p&gt;AssetTrackPro uses MQTT for its telemetry ingest; RTLS and RSSI for locating assets; geofences for triggering notifications on the digital twin; and even has edge computing capabilities, in addition to everything else covered here. You can find all of that and more on assettrackpro.com.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;See all of these concepts in action with AssetTrackPro's production IoT platform.&lt;br&gt;
&lt;a href="https://assettrackpro.com" rel="noopener noreferrer"&gt;Learn More About AssetTrackPro ↗&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>webdev</category>
      <category>beginners</category>
      <category>explainlikeimfive</category>
      <category>iot</category>
    </item>
    <item>
      <title>The difference between polling, webhooks, and WebSockets — and when asset tracking uses each</title>
      <dc:creator>Smrati</dc:creator>
      <pubDate>Wed, 27 May 2026 08:48:45 +0000</pubDate>
      <link>https://dev.to/smrati_verma/the-difference-between-polling-webhooks-and-websockets-and-when-asset-tracking-uses-each-2mdc</link>
      <guid>https://dev.to/smrati_verma/the-difference-between-polling-webhooks-and-websockets-and-when-asset-tracking-uses-each-2mdc</guid>
      <description>&lt;p&gt;Three methods of data transfer from the server to the client. They're all correct. They're all used in asset tracking systems, though for totally different purposes. Ever wonder which one to choose and why? Here's that article.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The common problem that all of them solve&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;All patterns of data transmission try to solve one single problem: how does the client know when there's been an update? They're just solving the same problem using different methodologies depending on how frequently updates occur and how quickly they need to be received.&lt;/p&gt;

&lt;h2&gt;
  
  
  Polling
&lt;/h2&gt;

&lt;p&gt;Client asks server continuously: "anything new?"&lt;br&gt;
&lt;strong&gt;Pull Model&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Client polls server at a set interval of every 5 seconds, every 30 seconds, or every minute. Server responds with latest information regardless of any change. Easy, dependable, and works anywhere. Inefficient – server wastes processing power returning empty replies because you paid for the call no matter what.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Comparison&lt;/strong&gt;: Checking your email for new messages by refreshing your inbox every 30 seconds. Effective but inefficient as you waste effort refreshing even when there are no emails to check.&lt;/p&gt;

&lt;p&gt;✓ Ideal for situations where&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Updates rarely occur (a few times a minute)&lt;/li&gt;
&lt;li&gt;Minimal network requirements&lt;/li&gt;
&lt;li&gt;Operating behind restrictive firewalls&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;✗ Difficulties arise when&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Frequent data updates occur&lt;/li&gt;
&lt;li&gt;Latency is critical&lt;/li&gt;
&lt;li&gt;Large scale (10,000 clients polling creates server headaches)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;📍 &lt;strong&gt;For asset tracking&lt;/strong&gt;&lt;br&gt;
Not appropriate for live updates — retrieving daily usage reports, pulling maintenance schedules, verifying fleet status from a management dashboard that updates every couple of minutes. Not good for anything that needs to be urgent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Webhooks
&lt;/h2&gt;

&lt;p&gt;Your server gets called when an event happens&lt;br&gt;
&lt;strong&gt;Push-based&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You give the server an endpoint URL. The server will send an HTTP POST to your URL whenever the relevant event happens. You don't poll — you just wait for your server to get the event message. Efficient, since it only sends HTTP messages when something interesting has occurred. Requires that your receiving end is publicly reachable and capable of handling HTTP messages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Comparison&lt;/strong&gt;: setting up email notifications rather than constantly refreshing your email inbox. No action on your part until you have something to act upon.&lt;/p&gt;

&lt;p&gt;✓ Ideal for situations where&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Event happens once in a while&lt;/li&gt;
&lt;li&gt;When communicating with other services&lt;/li&gt;
&lt;li&gt;Event triggers some workflow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;✗ Difficulties arise when&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lots of events firing per second&lt;/li&gt;
&lt;li&gt;Receiving end does not accept HTTP messages&lt;/li&gt;
&lt;li&gt;Two-way communication is required&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;📍 &lt;strong&gt;For asset tracking&lt;/strong&gt;&lt;br&gt;
Ideal for zone exit alerts, maintenance reminders, threshold violations, and ERP integrations. If a truck leaves a defined area or if a sensor crosses a critical threshold — trigger a webhook to Slack, an ERP, or a paging service. Once-off event, once-off POST action, problem solved.&lt;/p&gt;

&lt;h2&gt;
  
  
  WebSockets
&lt;/h2&gt;

&lt;p&gt;Continuous two-way communication through an open connection&lt;br&gt;
&lt;strong&gt;Two-Way&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One persistent TCP connection established between client and server which allows both ends to communicate without requesting another connection. The fastest and most efficient way of communicating — ideal for streaming data. Requires more coding than polling or webhooks but is unparalleled for dashboard purposes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Comparison&lt;/strong&gt;: Imagine a phone conversation rather than text messaging where the connection remains open allowing both participants to communicate anytime with no need to establish a new connection each time.&lt;/p&gt;

&lt;p&gt;✓ Ideal for situations where&lt;/p&gt;

&lt;p&gt;When data updates constantly&lt;br&gt;
Need for less than second latency&lt;br&gt;
Client-side sends data as well&lt;/p&gt;

&lt;p&gt;✗ Difficulties arise when&lt;/p&gt;

&lt;p&gt;When corporate firewalls block connections that aren’t HTTP&lt;br&gt;
When stateful load balancers aren't available&lt;br&gt;
When updates are rare and connection overhead becomes pointless&lt;/p&gt;

&lt;p&gt;📍 &lt;strong&gt;For asset tracking&lt;/strong&gt;&lt;br&gt;
A crucial component of live dashboards – live map with dynamic vehicle pin markers, live sensor data, and live alert feeds. All updates from the IoT devices get passed through WebSockets to live dashboards in just milliseconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;Polling&lt;/th&gt;
&lt;th&gt;Webhooks&lt;/th&gt;
&lt;th&gt;WebSockets&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Direction&lt;/td&gt;
&lt;td&gt;Client → Server&lt;/td&gt;
&lt;td&gt;Server → Client&lt;/td&gt;
&lt;td&gt;Both-way communication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speed (latency)&lt;/td&gt;
&lt;td&gt;Poll interval in seconds&lt;/td&gt;
&lt;td&gt;Real-time (event-triggered)&lt;/td&gt;
&lt;td&gt;response time sub-seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effectiveness&lt;/td&gt;
&lt;td&gt;Low (empty results)&lt;/td&gt;
&lt;td&gt;win – only triggered by events&lt;/td&gt;
&lt;td&gt;High (always connected)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ease of use&lt;/td&gt;
&lt;td&gt;simple&lt;/td&gt;
&lt;td&gt;somewhat complex&lt;/td&gt;
&lt;td&gt;balanced (more complex)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firewall friendliness&lt;/td&gt;
&lt;td&gt;passes firewall&lt;/td&gt;
&lt;td&gt;passes firewall&lt;/td&gt;
&lt;td&gt;sometimes firewalled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ideal for asset tracking&lt;/td&gt;
&lt;td&gt;reports&lt;/td&gt;
&lt;td&gt;notifications&lt;/td&gt;
&lt;td&gt;live dashboards&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The interplay of the three together in one system&lt;/strong&gt;&lt;br&gt;
The best asset tracking solutions don’t choose – they implement all three for distinct purposes:&lt;/p&gt;

&lt;h2&gt;
  
  
  A full asset tracking communication pipeline
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Polling&lt;/strong&gt; – dashboard polls for summary information about fleet every 5 minutes. Live updates are not required, just a snapshot.&lt;br&gt;
&lt;strong&gt;Webhooks&lt;/strong&gt; – violation of geofence triggers a POST to Slack, ERP, and another custom URL simultaneously. Event-driven, loosely coupled, instant.&lt;br&gt;
&lt;strong&gt;Websockets&lt;/strong&gt; – live ops dashboard displays continuous real-time asset locations, sensor data, and alerts to authenticated users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hybrid tip&lt;/strong&gt; – if you can't use Websockets because of corporate proxy, Server-Sent Events (SSE) is a great alternative to Websockets when it comes to the live dashboard – HTTP, built-in reconnection logic, uni-directional server -&amp;gt; client message protocol, covers almost all use cases of dashboards.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;AssetTrackPro utilizes all three approaches in production – Websockets are used for live dashboard functionality, webhooks for integrating alerts into ERP and Slack, and REST APIs for reporting/historical data access. &lt;a href="https://assettrackpro.com/" rel="noopener noreferrer"&gt;Learn more →&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Planning to develop a real-time asset tracking solution? AssetTrackPro’s platform provides WebSocket streaming capabilities, webhook delivery, and APIs for your reports – everything is ready to use out-of-the-box.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;&lt;a href="https://assettrackpro.com/" rel="noopener noreferrer"&gt;Discover AssetTrackPro ↗&lt;/a&gt;&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>backend</category>
      <category>iot</category>
    </item>
    <item>
      <title>How Geofencing Actually Works in Enterprise Asset Tracking</title>
      <dc:creator>Smrati</dc:creator>
      <pubDate>Tue, 26 May 2026 17:38:43 +0000</pubDate>
      <link>https://dev.to/smrati_verma/how-geofencing-actually-works-in-enterprise-asset-tracking-158o</link>
      <guid>https://dev.to/smrati_verma/how-geofencing-actually-works-in-enterprise-asset-tracking-158o</guid>
      <description>&lt;p&gt;Often geofencing is presented quite simply like this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Receive alerts when an asset leaves an area."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This definition isn't incorrect, but it doesn't convey all that much about the complexity of the technology itself.&lt;/p&gt;

&lt;p&gt;When using geofencing as part of enterprise asset tracking systems, the process involves not only defining boundaries on the map but also managing movement events, taking automated actions, boosting security, and minimizing any possible blind spots in the process.&lt;/p&gt;

&lt;p&gt;Then, how does geofencing really work?&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Geofencing?
&lt;/h2&gt;

&lt;p&gt;A geofence represents a virtual boundary set up for some location in reality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Depending on your use case, this area can be:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Warehouse&lt;/li&gt;
&lt;li&gt;Construction site&lt;/li&gt;
&lt;li&gt;Distribution center&lt;/li&gt;
&lt;li&gt;Office park&lt;/li&gt;
&lt;li&gt;Delivery territory&lt;/li&gt;
&lt;li&gt;Prohibited area&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each time the asset either enters or leaves this predefined area, an event can take place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Alert: Equipment was taken out of the construction site after work time&lt;br&gt;
Notification: Package was delivered to the right destination&lt;br&gt;
Automate: Asset status update&lt;br&gt;
Security: Report about unauthorized movement&lt;/p&gt;

&lt;p&gt;However, the virtual boundary needs to be identified first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Collecting Location Data
&lt;/h2&gt;

&lt;p&gt;Geofencing requires collecting data regarding asset location.&lt;/p&gt;

&lt;p&gt;Sources may vary depending on your particular use case.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GPS&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Popular outdoors.&lt;/p&gt;

&lt;p&gt;The devices with GPS technology provide latitude and longitude data, usually at periodic intervals.&lt;/p&gt;

&lt;p&gt;Appropriate for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vehicle tracking&lt;/li&gt;
&lt;li&gt;Construction machines&lt;/li&gt;
&lt;li&gt;Cargo containers&lt;/li&gt;
&lt;li&gt;Outdoor property&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Constraints:&lt;/p&gt;

&lt;p&gt;Lower accuracy indoors&lt;br&gt;
Battery usage&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BLE (Bluetooth Low Energy)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Utilized indoors.&lt;/p&gt;

&lt;p&gt;BLE beacons determine relative distance instead of actual GPS locations.&lt;/p&gt;

&lt;p&gt;Applicable to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Warehouses&lt;/li&gt;
&lt;li&gt;Hospitals&lt;/li&gt;
&lt;li&gt;Manufacturing plants&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;RFID&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Unlike GPS, RFID does not track locations constantly but identifies when items cross certain checkpoints.&lt;/p&gt;

&lt;p&gt;Applicable to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Inventory movement&lt;/li&gt;
&lt;li&gt;Entrance/exit tracking&lt;/li&gt;
&lt;li&gt;Wi-Fi or RTLS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most companies employ RTLS for indoor positioning purposes.&lt;/p&gt;

&lt;p&gt;These devices use several signals to calculate an estimated location.&lt;/p&gt;

&lt;p&gt;Each environment has unique needs in terms of tracking technology.&lt;/p&gt;

&lt;p&gt;Which is the reason why GPS is seldom utilized as a sole tracker for enterprise assets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Comparing the Position to Geofences
&lt;/h2&gt;

&lt;p&gt;After receiving location data, the software will check if:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Is that asset inside the pre-set zone?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Essentially, the process entails:&lt;/p&gt;

&lt;p&gt;Comparing the coordinates of the asset in question&lt;br&gt;
To stored geofence coordinates&lt;/p&gt;

&lt;p&gt;If the position moves across the border:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Event triggered&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Such events will log:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Date/time&lt;/li&gt;
&lt;li&gt;Location&lt;/li&gt;
&lt;li&gt;Identification of the item&lt;/li&gt;
&lt;li&gt;Moving direction (entrance/exit)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result will become part of the asset's motion history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Automated Actions Initiated by Geofences
&lt;/h2&gt;

&lt;p&gt;The main advantage of geofencing lies in automation, not detection.&lt;/p&gt;

&lt;p&gt;Examples of actions performed:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security Monitoring&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In case the expensive equipment moves out of the building:&lt;/p&gt;

&lt;p&gt;→ Alerts sent instantly&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inventory Management&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Upon the arrival of shipments into warehouses:&lt;/p&gt;

&lt;p&gt;→ Update status automatically&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workflows for Maintenance&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In case assets enter maintenance areas:&lt;/p&gt;

&lt;p&gt;→ Activate inspection tasks&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compliance&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Some business sectors need proof of asset location and timing information. Geofences allow to generate audit logs. Without automation, geofencing will remain just another notifications solution. Automation transforms it into a workflow generator.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Geofencing Is Important Besides Anti-theft Purposes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;People tend to associate geofencing with theft prevention alone.&lt;/p&gt;

&lt;p&gt;However, a large number of companies employ it to find answers to operational questions:&lt;/p&gt;

&lt;p&gt;Why do our assets spend much time at some locations?&lt;br&gt;
Which routes are responsible for delays?&lt;br&gt;
How often are our assets moving between different locations?&lt;br&gt;
Where is our inefficiency?&lt;/p&gt;

&lt;p&gt;Motion information becomes operational insights.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Future: Geofencing + Predictive Analytics
&lt;/h2&gt;

&lt;p&gt;The combination of geofencing with lifecycle intelligence is becoming commonplace in modern asset tracking solutions.&lt;/p&gt;

&lt;p&gt;Whereas traditional systems focused on answering such question as:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“Where is my asset right now?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;New platforms enable you to analyze your motion data:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"What is the meaning of this particular pattern of movement?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This turns geofencing into a business analysis solution.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Solutions like &lt;a href="https://assettrackpro.com/" rel="noopener noreferrer"&gt;AssetTrackPro &lt;/a&gt;offer you the possibility to integrate geofencing and overall asset visualization capabilities.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Because, within corporate environments, it is important not only to know where an asset is but also to understand why.&lt;/p&gt;

</description>
      <category>geofencing</category>
      <category>rfid</category>
      <category>assettracking</category>
      <category>iot</category>
    </item>
    <item>
      <title>What Happens to Asset Data After Collection? A Look at Lifecycle Intelligence</title>
      <dc:creator>Smrati</dc:creator>
      <pubDate>Tue, 26 May 2026 17:17:58 +0000</pubDate>
      <link>https://dev.to/smrati_verma/what-happens-to-asset-data-after-collection-a-look-at-lifecycle-intelligence-1a6m</link>
      <guid>https://dev.to/smrati_verma/what-happens-to-asset-data-after-collection-a-look-at-lifecycle-intelligence-1a6m</guid>
      <description>&lt;p&gt;&lt;strong&gt;Asset tracking tends to be condensed down to one single question:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Where is my asset?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;But mere location is just a first step.&lt;/p&gt;

&lt;p&gt;True value arises from the collection of data and then its transformation by organizations into insights around maintenance, utilization, lifecycle management, and potential risks. It is a stage which modern businesses refer to as lifecycle intelligence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data Collection vs Insight Generation
&lt;/h2&gt;

&lt;p&gt;Modern tracking devices gather asset information non-stop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Location&lt;/li&gt;
&lt;li&gt;Number of uses&lt;/li&gt;
&lt;li&gt;Environmental factors&lt;/li&gt;
&lt;li&gt;History of maintenance procedures&lt;/li&gt;
&lt;li&gt;Movement patterns&lt;/li&gt;
&lt;li&gt;Records of ownership&lt;/li&gt;
&lt;li&gt;Performance metrics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Alone, these figures are not very informative.&lt;/p&gt;

&lt;p&gt;But combined, they form certain patterns.&lt;/p&gt;

&lt;p&gt;For instance, assets that have recently been used more often than usual while their maintenance history has been deferred may signal increased risk of malfunctioning. Similarly, equipment moving across several locations too frequently might point at misuse.&lt;/p&gt;

&lt;p&gt;The objective here is not merely collecting data.&lt;/p&gt;

&lt;p&gt;The objective is predicting future events.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Predictive Maintenance Makes a Difference
&lt;/h2&gt;

&lt;p&gt;Classical maintenance is done based on set time periods:&lt;/p&gt;

&lt;p&gt;Check equipment each six months. Replace equipment after X number of years.&lt;/p&gt;

&lt;p&gt;In contrast, predictive maintenance utilizes the asset telemetry as well as the history of the maintenance process to determine when the procedure is really necessary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This results in:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Downtime avoidance&lt;/li&gt;
&lt;li&gt;Over-maintenance savings&lt;/li&gt;
&lt;li&gt;Early replacement prevention&lt;/li&gt;
&lt;li&gt;Efficiency improvements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Servicing too soon is inefficient while servicing too late increases risk of malfunction.&lt;/p&gt;

&lt;p&gt;The key here is striking a balance between servicing and downtime through asset lifecycle intelligence.&lt;/p&gt;

&lt;p&gt;Asset Data Can Identify Underutilization Too&lt;/p&gt;

&lt;p&gt;Sometimes, one of the biggest issues is not utilization at all – but underutilization.&lt;/p&gt;

&lt;p&gt;Many companies acquire new equipment due to perceptions of shortages, even if current assets are sitting idly on shelves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lifecycle analytics can pinpoint:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Underutilized assets&lt;/li&gt;
&lt;li&gt;Overworked equipment&lt;/li&gt;
&lt;li&gt;Redundant purchases&lt;/li&gt;
&lt;li&gt;Trends towards underutilization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Often, just being aware can result in cost savings before any unnecessary acquisitions take place.&lt;/p&gt;

&lt;p&gt;Beyond Asset Management: Building a More Intelligent Operation&lt;/p&gt;

&lt;p&gt;The development of asset management moves beyond:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tracking → Monitoring → Predicting → Optimization&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;More organizations want their asset management systems to be able to:&lt;/p&gt;

&lt;p&gt;Which equipment will most likely fail in the next quarter?&lt;br&gt;
Which pieces of equipment are overcosting our organization?&lt;br&gt;
Which maintenance schedule needs to be adjusted?&lt;br&gt;
Are there new efficiencies we can optimize for?&lt;/p&gt;

&lt;p&gt;These answers aren’t based on mere data – they're based on intelligence.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Tools like &lt;a href="https://assettrackpro.com/" rel="noopener noreferrer"&gt;AssetTrackPro&lt;/a&gt; have been built with this holistic approach in mind – bringing together tracking, analytics, and lifecycle intelligence within one platform.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Since data collection is no longer the hard part.&lt;/strong&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to Implement OTA Firmware Updates for IoT Asset Tracking Devices</title>
      <dc:creator>Smrati</dc:creator>
      <pubDate>Mon, 25 May 2026 15:24:40 +0000</pubDate>
      <link>https://dev.to/smrati_verma/how-to-implement-ota-firmware-updates-for-iot-asset-tracking-devices-24lg</link>
      <guid>https://dev.to/smrati_verma/how-to-implement-ota-firmware-updates-for-iot-asset-tracking-devices-24lg</guid>
      <description>&lt;p&gt;Distribution of firmware updates to thousands of GPS trackers and BLE beacons installed in warehouses, vehicles, and installations is a very difficult aspect of operations in IoT solutions. Do it wrong, and you brick the devices in the field. Do it right, and you deliver new features, security patches, and other improvements to all your fleet while they continue working normally. This is how it works in detail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why OTA updates are mandatory once you have a fleet&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once you deploy several dozen of IoT devices in the field, manual updates become impossible from an operational perspective. Imagine manually updating a fleet of 500 GPS trackers located in 10 different sites in the field – it will take you weeks. During those weeks your fleet will be running with different versions of firmware, exhibiting inconsistent behavior and being vulnerable to security threats.&lt;/p&gt;

&lt;p&gt;OTA updates solve this problem by sending a firmware update to the device over the air through its connection. This could be either MQTT, cellular connection, WiFi or even LoRaWAN connections.&lt;/p&gt;

&lt;h2&gt;
  
  
  OTA update process – Four steps for safety
&lt;/h2&gt;

&lt;p&gt;📦&lt;strong&gt;Package&lt;/strong&gt;&lt;br&gt;
Create, sign, and version the firmware binary. Distribute over CDN with a checksum.&lt;/p&gt;

&lt;p&gt;📡&lt;strong&gt;Notify&lt;/strong&gt;&lt;br&gt;
Inform the device about availability of a newer version using MQTT protocol. The device verifies its eligibility to receive an update.&lt;/p&gt;

&lt;p&gt;⬇️&lt;strong&gt;Download and verify&lt;/strong&gt;&lt;br&gt;
Downloads the binary in chunks and verifies the SHA-256 hash and digital signature.&lt;/p&gt;

&lt;p&gt;🔄&lt;strong&gt;Apply and rollback&lt;/strong&gt;&lt;br&gt;
Applies the update by flashing the firmware binary to the inactive partition. Boots to the new firmware version. Performs a rollback if health-check fails.&lt;/p&gt;
&lt;h2&gt;
  
  
  Dual-partition design (A/B)
&lt;/h2&gt;

&lt;p&gt;The safest OTA update pattern includes two partitions, one which contains the active version of firmware while the other is used to store the new firmware version. This is referred to as the inactive partition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Device runs from Partition A&lt;/strong&gt;&lt;br&gt;
Normal operation. Partition B is empty or holds the previous version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2&lt;br&gt;
OTA notification received&lt;/strong&gt;&lt;br&gt;
Device downloads new firmware into Partition B in chunks, verifies hash on completion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3&lt;br&gt;
Reboot into Partition B&lt;/strong&gt;&lt;br&gt;
Bootloader switches boot target to Partition B. Device boots new firmware.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4a&lt;br&gt;
Health check passes → commit&lt;/strong&gt;&lt;br&gt;
Device reports successful boot. Partition B becomes the new active partition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4b&lt;br&gt;
Health check fails → rollback&lt;/strong&gt;&lt;br&gt;
Bootloader detects failed boot attempts. Reverts to Partition A automatically. Device stays online.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1 - Back End: Server and notify
&lt;/h2&gt;

&lt;p&gt;OTA back end will do two things: server and notify the eligible devices. Notify using MQTT: devices have a persistent connection anyways:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Node.js OTA notification — push to eligible devices via MQTT&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;pushOTANotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;firmwareVersion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targetFleet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;firmwareVersion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`https://cdn.yourdomain.com/firmware/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;firmwareVersion&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.bin`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getFileHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;firmwareVersion&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getFileSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;firmwareVersion&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;releaseNotes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Fix GPS drift bug + improved cold start&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Publish to fleet topic — only devices in targetFleet receive it&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;mqttClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`fleet/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;targetFleet&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/ota/available`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;qos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;retain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  &lt;span class="c1"&gt;// retain so offline devices get it on reconnect&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2 - Device: Receive &amp;amp; Download
&lt;/h2&gt;

&lt;p&gt;At the device level, subscribing to OTA topic and performing updates on Python (Raspberry Pi tracker):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;paho.mqtt.client&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;mqtt&lt;/span&gt;

&lt;span class="n"&gt;CURRENT_VERSION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.4.2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;FIRMWARE_PATH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/firmware/update.bin&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_ota_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userdata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
  &lt;span class="n"&gt;update&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;# Skip if already on this version
&lt;/span&gt;  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;CURRENT_VERSION&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;

  &lt;span class="c1"&gt;# Download firmware in chunks
&lt;/span&gt;  &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Downloading firmware &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FIRMWARE_PATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iter_content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
      &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Verify SHA-256 hash before applying
&lt;/span&gt;  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;verify_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FIRMWARE_PATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sha256&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="nf"&gt;report_failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;HASH_MISMATCH&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;

  &lt;span class="c1"&gt;# Hash verified — apply and reboot
&lt;/span&gt;  &lt;span class="nf"&gt;apply_firmware&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
  &lt;span class="n"&gt;sha256&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
      &lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3 - Sign firmware for security purposes
&lt;/h2&gt;

&lt;p&gt;Hash verification for the purpose of corruption detection and signature verification for preventing malicious updates. Only your signed firmware can be updated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Build pipeline — sign firmware binary before uploading&lt;/span&gt;
&lt;span class="c"&gt;# Generate signing key (once, store private key securely in CI secrets)&lt;/span&gt;
openssl genrsa &lt;span class="nt"&gt;-out&lt;/span&gt; firmware_signing.key 4096
openssl rsa &lt;span class="nt"&gt;-in&lt;/span&gt; firmware_signing.key &lt;span class="nt"&gt;-pubout&lt;/span&gt; &lt;span class="nt"&gt;-out&lt;/span&gt; firmware_signing.pub

&lt;span class="c"&gt;# Sign the firmware binary&lt;/span&gt;
openssl dgst &lt;span class="nt"&gt;-sha256&lt;/span&gt; &lt;span class="nt"&gt;-sign&lt;/span&gt; firmware_signing.key &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-out&lt;/span&gt; firmware_v1.5.0.bin.sig firmware_v1.5.0.bin

&lt;span class="c"&gt;# On device — verify signature before applying&lt;/span&gt;
openssl dgst &lt;span class="nt"&gt;-sha256&lt;/span&gt; &lt;span class="nt"&gt;-verify&lt;/span&gt; firmware_signing.pub &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-signature&lt;/span&gt; firmware_v1.5.0.bin.sig firmware_v1.5.0.bin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4 - Health checks and Rollback mechanism
&lt;/h2&gt;

&lt;p&gt;Once the device reboots into the new firmware, it needs to send out a successful health check message within the time limit. Otherwise, the bootloader will roll back to its old state, in case the new firmware doesn't work (causes crashes, can't connect):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Post-boot health check — report to backend within 60 seconds&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;postBootHealthCheck&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;checks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;mqttConnected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;checkMQTTConnection&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;gpsLock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;checkGPSLock&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;sensorReadings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;checkSensors&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;memoryOk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;memoryUsage&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;heapUsed&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;passed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;checks&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// Report result to backend&lt;/span&gt;
  &lt;span class="nx"&gt;mqttClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`devices/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;DEVICE_ID&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/ota/status`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CURRENT_VERSION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;passed&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SUCCESS&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;FAILED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;checks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}))&lt;/span&gt;

  &lt;span class="c1"&gt;// If failed — trigger rollback to previous partition&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;passed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;triggerRollback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Never do OTAs across all your fleet simultaneously; otherwise, if something goes wrong and a whole 10,000 devices get bricked, it's an unrecoverable scenario. Use staged rollouts from 1% → 10% → 50% → 100%, confirming health check after each step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Staged rollout strategy
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Staged rollout controller&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ROLLOUT_STAGES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;percent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;waitHours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;canary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;percent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;waitHours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;early&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;percent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;waitHours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;majority&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;percent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="na"&gt;waitHours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;full&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;runStagedRollout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fleet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stage&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;ROLLOUT_STAGES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;devices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sampleFleet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fleet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;percent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;pushOTAToDevices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// Wait and check success rate before next stage&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;waitHours&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;successRate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getSuccessRate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;successRate&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Less than 95% success — halt and investigate&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;haltRollout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Rollout halted at &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; — success rate: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;successRate&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scheduling Tip: Schedule OTA pushes in a known period of low activity for your fleet - nighttime for warehouses devices, off shift time for manufacturing. Do not schedule pushes at peak operational time since rebooting cycles costs are much higher then.&lt;/p&gt;

&lt;h2&gt;
  
  
  OTA Safety Checklist
&lt;/h2&gt;

&lt;p&gt;✓ &lt;strong&gt;Dual partition (A/B)&lt;/strong&gt; - update image flashed to inactive partition, rollback is possible at any moment&lt;br&gt;
✓ &lt;strong&gt;SHA-256 hash check&lt;/strong&gt; - do verification before flashing, reject corrupt images&lt;br&gt;
✓ &lt;strong&gt;Cryptographic signature&lt;/strong&gt; - use RSA/ECDSA and keep secret keys in CI system secrets only&lt;br&gt;
✓ &lt;strong&gt;Auto-rollback&lt;/strong&gt; on health check failed in timeout&lt;br&gt;
✓ &lt;strong&gt;Staged deployment&lt;/strong&gt;: canary -&amp;gt; 10% -&amp;gt; 50% -&amp;gt; 100% with success rates controls&lt;br&gt;
✓ &lt;strong&gt;MQTT message retain&lt;/strong&gt; - device receives push even if disconnects during OTA push&lt;br&gt;
✓ &lt;strong&gt;Version pinning&lt;/strong&gt; - the ability to push update for particular device or fleet by specified version number&lt;br&gt;
✓ &lt;strong&gt;Update logs&lt;/strong&gt; - keep record of update process for every single device firmware&lt;br&gt;
✗ &lt;strong&gt;NEVER push update to whole fleet at once&lt;/strong&gt; - one bad update can brick thousands of units&lt;br&gt;
✗ &lt;strong&gt;NEVER ignore hash verification&lt;/strong&gt; - data integrity problems during download happens way more often than you think on cellular&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommended stack
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;MQTT (Mosquitto)&lt;/li&gt;
&lt;li&gt;AWS S3 / Cloudflare R2&lt;/li&gt;
&lt;li&gt;Node.js backend&lt;/li&gt;
&lt;li&gt;Python (device)&lt;/li&gt;
&lt;li&gt;OpenSSL (signing)&lt;/li&gt;
&lt;li&gt;PostgreSQL (audit log)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When it comes to large-scale OTA that is managed for you, AWS IoT Jobs or Azure IoT Hub take care of all the orchestration of the OTA process. If you’re hosting your own systems, the above combination is sufficient to support OTA to thousands of devices.&lt;/p&gt;

&lt;p&gt;Our own IoT device fleet uses the dual-partition OTA model, rolling out to our GPS and Bluetooth Low Energy tracking devices using a fully automated process where we don’t have to touch any of the devices in the field manually. &lt;a href="https://assettrackpro.com/" rel="noopener noreferrer"&gt;Check out our platform → &lt;br&gt;
&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Are you working with a device fleet of IoT trackers? Then AssetTrackPro will provide you with OTA functionality, device management, and firmware updates. Discover &lt;a href="https://assettrackpro.com/" rel="noopener noreferrer"&gt;AssetTrackPro →&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>ota</category>
      <category>iot</category>
      <category>assettracking</category>
      <category>lorawan</category>
    </item>
    <item>
      <title>BLE Indoor Positioning: How RSSI Trilateration Works (With Math and Code)</title>
      <dc:creator>Smrati</dc:creator>
      <pubDate>Mon, 25 May 2026 15:02:43 +0000</pubDate>
      <link>https://dev.to/smrati_verma/ble-indoor-positioning-how-rssi-trilateration-works-with-math-and-code-1l2p</link>
      <guid>https://dev.to/smrati_verma/ble-indoor-positioning-how-rssi-trilateration-works-with-math-and-code-1l2p</guid>
      <description>&lt;h2&gt;
  
  
  RSSI and distance
&lt;/h2&gt;

&lt;p&gt;Each BLE beacon sends out a signal which is picked up by fixed points and then the strength of the signal measured, which gives the Received Signal Strength Indicator (RSSI), measured in dBm. Higher strength means higher proximity. According to the Log-Distance Path Loss Equation:&lt;br&gt;
d = 10 ^ ((TxPower − RSSI) / (10 × n))&lt;br&gt;
Where:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;d = Distance in metres&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;TxPower = RSSI at 1 m&lt;/strong&gt; (usually calibrated, around −59 to −65 dBm)&lt;br&gt;
&lt;strong&gt;RSSI = current RSSI in dBm&lt;br&gt;
n = path loss exponent&lt;/strong&gt; (typically 2.0 for free space, 2.5 to 4.0 indoors)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;rssiToDistance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rssi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;txPower&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;59&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;2.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rssi&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ratio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;txPower&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;rssi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ratio&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;rssiToDistance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;59&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// → ~1.0m&lt;/span&gt;
&lt;span class="nf"&gt;rssiToDistance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// → ~3.5m&lt;/span&gt;
&lt;span class="nf"&gt;rssiToDistance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;85&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// → ~14m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ &lt;strong&gt;RSSI is noisy&lt;/strong&gt;. A single reading varies ±10 dBm from interference. Never feed raw RSSI into trilateration — smooth first with a Kalman filter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;KalmanFilter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;R&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.125&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;R&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;R&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Q&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;P&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;P&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;P&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;R&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;P&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;P&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Q&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// One filter per anchor per tag&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;KalmanFilter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;distance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rssiToDistance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawRssi&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Trilateration — the math
&lt;/h2&gt;

&lt;p&gt;With distances from 3+ anchors at known positions, solve:&lt;br&gt;
(x − x₁)² + (y − y₁)² = d₁²&lt;br&gt;
(x − x₂)² + (y − y₂)² = d₂²&lt;br&gt;
(x − x₃)² + (y − y₃)² = d₃²&lt;br&gt;
Subtract equation 1 from equations 2 and 3 → eliminates x² and y² → two linear equations → solve for x and y.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full implementation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;anchors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;trilaterate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;anchors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;distances&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;anchors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;A&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;anchors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;anchors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;A&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
    &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;distances&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;distances&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;
      &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;
      &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;leastSquares&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;A&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;leastSquares&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;A&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;AtA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt; &lt;span class="nx"&gt;Atb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;A&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;a0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;a1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;A&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;AtA&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="nx"&gt;a0&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;a0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;AtA&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="nx"&gt;a0&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;a1&lt;/span&gt;
    &lt;span class="nx"&gt;AtA&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="nx"&gt;a1&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;a0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;AtA&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="nx"&gt;a1&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;a1&lt;/span&gt;
    &lt;span class="nx"&gt;Atb&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="nx"&gt;a0&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;  &lt;span class="nx"&gt;Atb&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="nx"&gt;a1&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;det&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;AtA&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;AtA&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;AtA&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;AtA&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AtA&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Atb&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;AtA&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Atb&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;det&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AtA&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Atb&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;AtA&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Atb&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;det&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Real World Accuracy
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Environment&lt;/th&gt;
&lt;th&gt;Path Loss n&lt;/th&gt;
&lt;th&gt;Accuracy&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Open Office&lt;/td&gt;
&lt;td&gt;2.0 - 2.5&lt;/td&gt;
&lt;td&gt;1 – 2m&lt;/td&gt;
&lt;td&gt;Best conditions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warehouse&lt;/td&gt;
&lt;td&gt;2.5 – 3.0&lt;/td&gt;
&lt;td&gt;2 – 4m&lt;/td&gt;
&lt;td&gt;Metal shelving multipath&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hospital&lt;/td&gt;
&lt;td&gt;3.0 – 3.5&lt;/td&gt;
&lt;td&gt;2 – 5m&lt;/td&gt;
&lt;td&gt;Lots of dense walls, equipment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manufacturing&lt;/td&gt;
&lt;td&gt;3.0 – 4.0&lt;/td&gt;
&lt;td&gt;3 – 6m&lt;/td&gt;
&lt;td&gt;Heavier machinery, RF-dense&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Tip for Calibration&lt;/strong&gt;: calibrate txPower individually at each anchor point in your environment. Stand 1m away, take 100 RSSI samples. Calibration increases accuracy by 30-40% over manufacture default values.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to Build on Top
&lt;/h2&gt;

&lt;p&gt;Place the trilaterated coordinate system onto a SVG floor plan – correlate the meters from the real world to pixels using scale factor. Asset pin locations are updated based on incoming RSSI coordinates received via Server-Sent Events or WebSocket.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://assettrackpro.com/" rel="noopener noreferrer"&gt;AssetTrackPro&lt;/a&gt;'s BLE indoor positioning technology is based on calibrated RSSI trilateration + Kalman Filtering, delivering accurate positions &lt;/p&gt;

&lt;p&gt;Explore &lt;a href="https://assettrackpro.com/" rel="noopener noreferrer"&gt;AssetTrackPro ↗&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>ble</category>
      <category>javascript</category>
      <category>java</category>
      <category>iot</category>
    </item>
    <item>
      <title>Why your warehouse is losing money you can’t see on any report</title>
      <dc:creator>Smrati</dc:creator>
      <pubDate>Fri, 22 May 2026 14:52:05 +0000</pubDate>
      <link>https://dev.to/smrati_verma/why-your-warehouse-is-losing-money-you-cant-see-on-any-report-3n05</link>
      <guid>https://dev.to/smrati_verma/why-your-warehouse-is-losing-money-you-cant-see-on-any-report-3n05</guid>
      <description>&lt;p&gt;&lt;em&gt;Why is Financial Loss in Warehousing Not Recognized by Traditional Accounting?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;While the profit and loss account seems good enough, and the operations staff is actively involved, money is slowly slipping away from the business in the time interval between these two events.&lt;/p&gt;

&lt;p&gt;The warehousing managers understand the problem of shrinkage, damaged products, and delayed shipments. The problem leads to an investigation, process adjustment, and changes in the accounting data.&lt;/p&gt;

&lt;p&gt;On the contrary, there are losses that are not measured; in other words, if one does not measure these losses, their number will only increase over the years. Such losses will not appear in any row of the balance sheet and continue to exist due to the difference between theoretical and real performance of the business.&lt;/p&gt;

&lt;h2&gt;
  
  
  These are the places where the financial leakage occurs.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  The Five Invisible Drains
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;01&lt;br&gt;
Unproductive Equipment Unmonitored&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In regular warehousing procedures, equipment usage averages between 60–70%, which means that the leftover percentage of equipment remains unutilized at the warehouse site. Otherwise, there could be extra costs associated with the rental of the forklifts and the pallet jacks due to the lack of tracking. Owned equipment is simply idle in a specific bay of the company such as bay #7. Lack of tracking hampers any improvement processes since the purchases are done based on hearsay.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;02&lt;br&gt;
Hours Spent by Employees Looking Instead of Working&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;According to some research studies, employees waste about 1–2 hours daily on the search for equipment and tools in warehouses that do not use real-time tracking. In a workforce of 50 people, up to 100 hours daily will be lost. This cost is not shown as an expense but is manifested as busyness among the staff.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;03&lt;br&gt;
Cost of carrying inventory because it cannot be located&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When inventory cannot be located immediately, the default procedure is to reorder it. This way, there will be two identical items: located and ordered one, hence, double spending on it. The first item will become accessible, become overstocked, and will either be stored or written down. Phantom inventory ordering is one of the most popular and undertracked sources of losses in medium-scale warehouse businesses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;04&lt;br&gt;
Reacting to problems that could have been predicted&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If an unexpected stoppage occurs on a conveyor belt, then the cost does not include only the expense of repairs but also the loss of every hour of disruption — undelivered orders, trucks waiting, and labor wasted. Reaction to problems is three times more costly than preventive maintenance. Without usage meters, preventive maintenance can only happen either calendar or reactionary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;05&lt;br&gt;
Inefficient cycle counting, causing production time loss&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To conduct a full cycle inventory count can take up to 2–5 days when it comes to a big warehouse, which causes significant downtime. Thus, many warehouses decide to conduct such a count once a quarter or once a year because of the disruption it causes. With continuous monitoring, this count can be done routinely, and inventory will always be visible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The biggest expenses incurred in a warehouse aren’t the ones listed on any official report; instead, they’re the ones that go unseen.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why are these losses invisible?
&lt;/h2&gt;

&lt;p&gt;The underlying issue with them not being reported in the first place is very simple: Reporting is based on measurement. Hours worked are not recorded as “time spent looking for a pallet jack.” Costs associated with reordering products are not reported as “phantom inventory.” Repair bills don’t differentiate between repairs that could have been prevented and repairs that were inevitable.&lt;/p&gt;

&lt;p&gt;Therefore, the income statement can reflect a realistic picture of how well or poorly the company is doing financially while completely ignoring an entire category of operational loss. Financial experts will see the numbers and think they make sense, while the operations department sees problems constantly cropping up, yet nobody sees the need to analyze the situation because there are no integrative processes in place to identify the cause-and-effect relationships.&lt;/p&gt;

&lt;p&gt;In other words, real-time visibility into assets doesn’t just reduce the losses but allows them to be seen for what they are so that steps can be taken to address them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when you can see it all
&lt;/h2&gt;

&lt;p&gt;Warehouses that can monitor assets in real time see gains in all five areas: utilization rates increase as idle equipment is reallocated, labour hours are moved from searching to productive work, reorder decisions are made using actual inventory data, maintenance is planned instead of reactive, and cycle counts are reduced from days to hours.&lt;/p&gt;

&lt;p&gt;The compounding effect is what counts. If a warehouse gains just 30 minutes per worker per day, across a team of 50 people, it gains 25 hours of productive labour weekly — the equivalent of one full-time employee’s work, every week, forever. That was never listed as a cost on any report. It won’t show up as a saving either, unless you start measuring it.&lt;/p&gt;

&lt;p&gt;AssetTrackPro enables warehouse and logistics operations to build real-time asset visibility from the ground up. RFID, BLE and IoT sensor based tracking that converts invisible losses into measurable, fixable numbers. How it works -&amp;gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where to begin&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Choose any one of the five drains above, whichever takes your fancy, and spend a fortnight measuring it by hand. Count the hours, record the incidents, monitor the reorders. No matter what number you come up with, it is almost certainly larger than you think. That is your business case. So build the tracking system around closing that gap first and then grow from there.&lt;/p&gt;

&lt;p&gt;There have always been losses. Now you know where to find it.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;Ready to find out what your warehouse is really losing? AssetTrackPro develops real-time tracking systems for warehouses and distribution centers, beginning with the metrics that matter most for your operations.&lt;br&gt;
&lt;a href="https://assettrackpro.com/" rel="noopener noreferrer"&gt;&amp;gt; Learn more about AssetTrackPro&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>warehouse</category>
      <category>productivity</category>
      <category>monitoring</category>
      <category>iot</category>
    </item>
    <item>
      <title>WebSockets vs Server-Sent Events: which is better for live asset dashboards?</title>
      <dc:creator>Smrati</dc:creator>
      <pubDate>Fri, 22 May 2026 14:40:33 +0000</pubDate>
      <link>https://dev.to/smrati_verma/websockets-vs-server-sent-events-which-is-better-for-live-asset-dashboards-jk</link>
      <guid>https://dev.to/smrati_verma/websockets-vs-server-sent-events-which-is-better-for-live-asset-dashboards-jk</guid>
      <description>&lt;p&gt;Both technologies transfer real-time data to your browser; both work great. However, when it comes to developing dashboards for tracking assets in real time, which is constantly receiving data from thousands of devices and where there is only one direction of data flow, the decision should be made wisely. Here is an overview of both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Functionality of each technology
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;WebSockets&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Full-duplex persistent connection&lt;br&gt;
One TCP connection which is opened both ways. Data exchange between client and server is possible anytime. Protocol upgrade over HTTP at the very beginning of connection; then works independently.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bidirectional&lt;/li&gt;
&lt;li&gt;Binary or text&lt;/li&gt;
&lt;li&gt;Custom protocol&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;SSE&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One-directional HTTP event stream&lt;br&gt;
HTTP connection is kept open, but there is only the server pushing the events to the client. Client can not send data to the server over this connection, but sends HTTP requests instead.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Server -&amp;gt; client only&lt;/li&gt;
&lt;li&gt;Text only&lt;/li&gt;
&lt;li&gt;HTTP-native&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Comparing Asset Dashboards
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;WebSockets&lt;/th&gt;
&lt;th&gt;SSE&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Message direction&lt;/td&gt;
&lt;td&gt;Bidirectional — greater flexibility&lt;/td&gt;
&lt;td&gt;Single-direction (Server to Client)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Overhead&lt;/td&gt;
&lt;td&gt;Larger overhead due to framing (2–14 bytes per message)&lt;/td&gt;
&lt;td&gt;Small HTTP overhead (~100 bytes) one-time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-reconnect&lt;/td&gt;
&lt;td&gt;Manual configuration needed&lt;/td&gt;
&lt;td&gt;Built-in browser functionality — automatic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firewalls/proxies&lt;/td&gt;
&lt;td&gt;May not work through proxies&lt;/td&gt;
&lt;td&gt;Equivalent to standard HTTP — guaranteed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load balancing&lt;/td&gt;
&lt;td&gt;Needs sticky session support&lt;/td&gt;
&lt;td&gt;Browser does the job well&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Support for binary data&lt;/td&gt;
&lt;td&gt;Native support&lt;/td&gt;
&lt;td&gt;Basic support — no binary messages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser compatibility&lt;/td&gt;
&lt;td&gt;All browsers&lt;/td&gt;
&lt;td&gt;All browsers except IE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server implementation&lt;/td&gt;
&lt;td&gt;difficult — stateful sessions&lt;/td&gt;
&lt;td&gt;easy — standard HTTP handlers gains&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  Implementing SSE – asset location stream
&lt;/h2&gt;

&lt;p&gt;SSE is vastly easier to implement compared to WebSockets for read-only dashboards. Here's how to create an SSE stream on Node.js, where we emit events as assets are updated by our Redis service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Node.js SSE endpoint — streams live asset state&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/stream/assets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// SSE headers — keep connection open&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/event-stream&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cache-Control&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no-cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Connection&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;keep-alive&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flushHeaders&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="c1"&gt;// Subscribe to Redis pub/sub channel&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;duplicate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;asset-updates&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`data: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n\n`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// SSE format: data: {json}\n\n&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// Cleanup on client disconnect&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;close&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unsubscribe&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here's the same stream in action on the client side in React:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// React — consuming SSE asset stream&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setAssets&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;({})&lt;/span&gt;

&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;EventSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/stream/assets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;asset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;setAssets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nx"&gt;asset&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Browser auto-reconnects on disconnect — no code needed&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  WebSockets implementation
&lt;/h2&gt;

&lt;p&gt;– when two-way communication is needed&lt;/p&gt;

&lt;p&gt;When your read-only dashboard starts sending requests – panning, zooming a map, requesting asset groups, firing events – you start needing the full WebSockets setup. Here it is on the server and client sides:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Node.js WebSocket server with selective subscriptions&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;WebSocketServer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ws&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WebSocketServer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;wss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;connection&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;subscribedFleets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

  &lt;span class="c1"&gt;// Client tells server which fleets to subscribe to&lt;/span&gt;
  &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fleets&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;subscribe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;subscribedFleets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fleets&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// Push updates only for subscribed fleets&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;updates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getFleetUpdates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subscribedFleets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;readyState&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OPEN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;updates&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;close&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;SSE reconnection tip&lt;/strong&gt;: When reconnecting after a network outage, the browser will attempt reconnect using the same EventSource instance – and pass the Last-Event-ID request header containing the most recent event ID number. Use it on your backend to fetch missed events from your cache and close potential gaps in data streams between reconnects. Just add id: {eventId}\n to each SSE event.&lt;/p&gt;

&lt;h2&gt;
  
  
  The asset dashboard-specific answer
&lt;/h2&gt;

&lt;p&gt;With a normal asset tracking dashboard that consists of a live mapping display, location pins for each tracked asset, statuses, alert feed – almost all data transfer is from server to client. The server receives the updates to asset locations via IoT events, and these get consumed by the dashboard that displays them to the user.&lt;/p&gt;

&lt;p&gt;A classic scenario for Server-Sent Events. Built-in reconnect functionality. Regular HTTP load balancer support. No need for sticky sessions. No custom protocol required. Way simpler server-side logic compared to keeping thousands of stateful WebSocket connections.&lt;/p&gt;

&lt;p&gt;WebSockets become a necessity once you introduce interactivity into the picture - filtering data on behalf of the users, issuing commands for selected assets, displaying alerts based on the user's roles. For data display purposes, SSE beats WebSockets hands down, but for user interaction, the latter is better suited.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fast guide to decision making
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Map, board, alerts with read-only interface → &lt;strong&gt;SSE&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Easier deployment with no proxies involved → &lt;strong&gt;SSE&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Auto-reconnect without developing anything yourself → &lt;strong&gt;SSE&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;User sending commands and subscribing to feeds → &lt;strong&gt;WebSockets&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Binary data transfer required or chat-like interactions → &lt;strong&gt;WebSockets&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Dashboard doing read/write operations → &lt;strong&gt;SSE&lt;/strong&gt; for stream, &lt;strong&gt;REST&lt;/strong&gt; for control&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Preferred Stack&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Node.js / Express&lt;/li&gt;
&lt;li&gt;EventSource API&lt;/li&gt;
&lt;li&gt;ws (WebSockets)&lt;/li&gt;
&lt;li&gt;Redis pub/sub&lt;/li&gt;
&lt;li&gt;React + useState&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The most practical approach for most asset dashboards is SSE for the live data stream plus REST POST for all user-driven actions. You have the easy approach of SSE for 95% of the use cases, while REST allows for sending any data to the server in the 5% of actions that require such actions.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;AssetTrackPro’s live dashboard follows a SSE-first approach to handle asset location streaming with WebSockets acting as the secondary approach for fleet management. &lt;br&gt;
Check out the live asset dashboard of &lt;a href="https://assettrackpro.com/" rel="noopener noreferrer"&gt;AssetTrackPro →&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Building your own live asset dashboard? With AssetTrackPro’s solution, you just focus on building the user interface, not setting up the infrastructure.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;&lt;a href="https://assettrackpro.com/" rel="noopener noreferrer"&gt;Discover more about AssetTrackPro →&lt;/a&gt;&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>websockets</category>
      <category>frontend</category>
      <category>iot</category>
    </item>
    <item>
      <title>The ROI of smart asset tracking: what businesses in logistics are getting wrong</title>
      <dc:creator>Smrati</dc:creator>
      <pubDate>Thu, 21 May 2026 13:12:44 +0000</pubDate>
      <link>https://dev.to/smrati_verma/the-roi-of-smart-asset-tracking-what-businesses-in-logistics-are-getting-wrong-bd8</link>
      <guid>https://dev.to/smrati_verma/the-roi-of-smart-asset-tracking-what-businesses-in-logistics-are-getting-wrong-bd8</guid>
      <description>&lt;p&gt;Most businesses tend to measure the wrong metrics — hence their confusion and inability to achieve satisfactory results.&lt;/p&gt;

&lt;p&gt;When logistics firms invest in smart asset tracking technology, they have one goal in mind: stopping their losses. Sure enough, they will succeed in doing that — but that’s not where the biggest ROI is. Most businesses tend to miss out on the bigger picture by focusing solely on asset recovery metrics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here’s the discussion you need to have regarding ROI calculation, and what most business leaders get wrong.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The three ways in which you fail to calculate your ROI
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;01&lt;br&gt;
Calculating only the return on asset recovery&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Recovery is quantifiable and tangible. However, greater value lies in hours of labor recovery, lower maintenance costs, and reduced billing times, which don’t appear in a mere “recovered assets” tally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;02&lt;br&gt;
Overlooking utilization information&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Fleet operations average utilization of 60–70%. Tracking allows you to know which assets are underutilized, which are overloaded, and which assets you’re renting when you already have them, making the tracking system’s cost exceed its price tag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;03&lt;br&gt;
Neglecting the savings in subsequent time&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The hours saved by dispatchers, warehouse supervisors, and drivers not looking for assets multiply quickly. A ten-person operations team saving thirty minutes a day is equivalent to recovering twenty-five hours weekly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjwjy5hpkx0yadep86zqn.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjwjy5hpkx0yadep86zqn.jpg" alt=" " width="612" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the real money is made
&lt;/h2&gt;

&lt;p&gt;The logistics companies maximizing the use of smart tracking technology have the following KPIs in place: asset utilization ratio, maintenance expenses per asset, dwell time at each point, and invoicing precision. Once the assets are tracked from pick-up through delivery, disputes on invoicing virtually disappear — and this factor alone helps to recoup thousands of dollars monthly for even mid-sized fleet owners.&lt;/p&gt;

&lt;p&gt;Another underestimated approach to maximizing profitability is predictive maintenance. By having sensors continuously monitoring usage hours and environmental factors, maintenance becomes an issue of scheduling instead of emergency repairs. The industry standard ratio between emergency repair vs. preventive maintenance costs is 3:1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How AssetTrackPro does it differently&lt;/strong&gt;: Unlike other tracking solutions providers who sell generic trackers, AssetTrackPro develops customized end-to-end IoT tracking systems for logistics, manufacturing, and healthcare facilities. Its implementations focus on delivering tangible results starting Day One, rather than merely installing tracking hardware.&lt;/p&gt;

&lt;h2&gt;
  
  
  The right way to make your business case
&lt;/h2&gt;

&lt;p&gt;Before committing to any tracking solution, create a list of the top five pain points in your operations, including financial estimates. Asset loss may be third on your list. The first two are likely tied to underutilized assets or unexpected equipment downtime. Base your business case on that list, rather than on a vendor’s marketing materials.&lt;/p&gt;

&lt;p&gt;The logistics companies that are seeing ROI from their smart tracking solutions are not the ones using the fanciest technology. They are the ones who openly admitted to their financial losses and measured accordingly.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Looking to achieve ROI with your asset tracking solution? AssetTrackPro helps logistics and manufacturing facilities across North America deploy tracking solutions based on measurable business results.&lt;br&gt;
&lt;a href="https://assettrackpro.com/" rel="noopener noreferrer"&gt;Visit AssetTrackPro ↗&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>iot</category>
      <category>roi</category>
      <category>assettracking</category>
      <category>logistics</category>
    </item>
    <item>
      <title>How to design a fault-tolerant IoT data pipeline for asset tracking at scale</title>
      <dc:creator>Smrati</dc:creator>
      <pubDate>Thu, 21 May 2026 13:02:15 +0000</pubDate>
      <link>https://dev.to/smrati_verma/how-to-design-a-fault-tolerant-iot-data-pipeline-for-asset-tracking-at-scale-6k4</link>
      <guid>https://dev.to/smrati_verma/how-to-design-a-fault-tolerant-iot-data-pipeline-for-asset-tracking-at-scale-6k4</guid>
      <description>&lt;p&gt;A pipeline that is designed to work with 100 devices is not a pipeline that can work with 10,000 devices. Furthermore, a pipeline that works on Tuesday morning is not a pipeline that works in a case of network disruption, database failover, or MQTT broker crash. Fault tolerance should be taken into consideration when developing the architecture. Here’s how.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes to take into account while designing fault tolerant infrastructure
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Loss of device connection&lt;/strong&gt;&lt;br&gt;
Devices lose network connections because of network disruptions. The data has to be buffered and then replayed upon connection restoration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Crash of a broker&lt;/strong&gt;&lt;br&gt;
Restarting an MQTT broker during processing. Persistent sessions and Quality-of-Service levels 1 &amp;amp; 2 provide the solution to this issue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lagging consumer&lt;/strong&gt;&lt;br&gt;
Processing is delayed by various reasons. In the absence of a backpressure mechanism, the pipeline will stop working due to increasing pressure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Database failure&lt;/strong&gt;&lt;br&gt;
The failover process occurs while writing events. Absence of a retry mechanism will cause event loss forever.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiyg1pxbujjryqxcdsyvl.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiyg1pxbujjryqxcdsyvl.jpg" alt=" " width="612" height="344"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The fault-tolerant pipeline architecture
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Layer 1&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Device-side local buffer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Tags cache events when disconnected. Events cached will be replayed sequentially prior to new events being sent. No data loss occurs when there is no network connectivity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;MQTT with persistent sessions &amp;amp; QoS = 1&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Persistent sessions preserve broker session state upon disconnection. QoS 1 provides at-least-once guarantee, meaning broker retries till an acknowledgement from the device is received. No message drops happen silently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 3&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Kafka with replication &amp;amp; DLQ&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Event writes into Kafka with replication factor three. If any event fails to be processed, the event is sent to the DLQ - no data gets lost here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 4&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Idempotent consumers with retry&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Idempotent consumers handle events exactly once with help of deduplication keys. If writing to database fails, exponential backoff is used to retry. Nothing gets silently dropped or duplicated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 5&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;TimescaleDB with read replicas&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;All writes happen to the primary DB. Reads and dashboard are served by read replicas. The primary DB failing does not affect the running dashboard due to read replica.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1 – Buffering on Device Side
&lt;/h2&gt;

&lt;p&gt;Buffer the events locally in the device (Raspberry Pi, or embedded MCU), in case of broker unavailability, and send them back to the broker on reconnect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Python device client with local SQLite buffer
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;paho&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mqtt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;mqtt&lt;/span&gt;

&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;buffer.db&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CREATE TABLE IF NOT EXISTS queue (id INTEGER PRIMARY KEY, payload TEXT, ts REAL)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;buffer_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
  &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO queue (payload, ts) VALUES (?, ?)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()])&lt;/span&gt;
  &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;flush_buffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
  &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SELECT id, payload FROM queue ORDER BY ts ASC&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;assets/+/telemetry&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qos&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rc&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;mqtt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MQTT_ERR_SUCCESS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;DELETE FROM queue WHERE id = ?&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;row_id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2 – MQTT Persistent Sessions
&lt;/h2&gt;

&lt;p&gt;Use the clean_session=False broker setting, making sure that the broker buffers unacknowledged messages and sends them back on reconnect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Persistent session — broker retains state across disconnects
&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mqtt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;asset-tag-7821&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;clean_session&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;  &lt;span class="c1"&gt;# key setting — retain session on reconnect
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reconnect_delay_set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min_delay&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_delay&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userdata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rc&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;session present&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Session resumed — broker has queued messages&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;flush_buffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# replay local buffer on reconnect
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3 – Kafka DLQ
&lt;/h2&gt;

&lt;p&gt;Any event that cannot be processed after N attempts lands up in a Kafka DLQ instead of getting dropped altogether. The operations team can then examine, fix, and replay them without losing any data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_RETRIES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;processWithRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;producer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;MAX_RETRIES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;processEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;  &lt;span class="c1"&gt;// success — exit loop&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;attempts&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;  &lt;span class="c1"&gt;// exponential backoff&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// All retries failed — send to DLQ&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;producer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;iot-events-dlq&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;failedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 4 – Idempotent Writes &amp;amp; Dedupe&lt;br&gt;
Your QoS1 messages will lead to duplicate messages. Therefore, your write calls need to be idempotent; i.e., the same message processed twice needs to give you the same output, no duplicates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Idempotent insert using ON CONFLICT DO NOTHING&lt;/span&gt;
&lt;span class="c1"&gt;-- event_id is a hash of (asset_id + timestamp)&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;asset_locations&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;asset_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temp_c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;NOTHING&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Generate event_id in application layer&lt;/span&gt;
&lt;span class="c1"&gt;-- const eventId = crypto.createHash('sha256')&lt;/span&gt;
&lt;span class="c1"&gt;-- .update(`${assetId}:${timestamp}`).digest('hex')&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Tip for deduplication window&lt;/strong&gt;: When you have assets that update every few seconds and produce events continuously, cache latest event_ids in Redis with 60 sec TTL to avoid database look-up for deduplication on each insert operation. You can use Redis SET NX command to do sub-millisecond deduplication checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production Fault Tolerance Checklist
&lt;/h2&gt;

&lt;p&gt;✓ Device local buffer: SQLite or Flash-based storage; will replay events when reconnecting in order.&lt;br&gt;
✓ MQTT sessions with clean_session=False, and minimum QoS 1.&lt;br&gt;
✓ Kafka replication factor = 3. Survives single broker failure with zero messages lost.&lt;br&gt;
✓ Dead Letter Queue. Failed events are captured, inspectable and re-playable.&lt;br&gt;
✓ Exponential back-off during retries to avoid thundering herd effect.&lt;br&gt;
✓ Idempotent database writes ON CONFLICT DO NOTHING. Deduplicate by event_id&lt;br&gt;
✓ Read Replicas for dashboards. Write and read paths don’t interfere with each other&lt;br&gt;
✓ Health Checks and alerts: Consumer lag, DLQ depth, and broker health are always monitored.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The most frequent mistake&lt;/strong&gt;: Implementing retry mechanism without implementing idempotent writes. Retry mechanism without idempotence turns transient error into duplicate data - a much more difficult problem to solve than the one you've faced before.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stack recommendations
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Mosquitto / HiveMQ&lt;/li&gt;
&lt;li&gt;Kafka (RF=3)&lt;/li&gt;
&lt;li&gt;KafkaJS / confluent-kafka&lt;/li&gt;
&lt;li&gt;Redis (for deduplication)&lt;/li&gt;
&lt;li&gt;TimescaleDB &amp;amp; replica&lt;/li&gt;
&lt;li&gt;Prometheus &amp;amp; Grafana&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Three key metrics to watch for in production: Kafka consumer lag (higher lag means a bottleneck in consumption), DLQ queue size (all messages in here require immediate attention), and MQTT reconnection rates (high numbers mean device connectivity problems). You should set alerts on all three.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The IoT data pipeline of AssetTrackPro is based on this exact fail-proof architecture — device-side buffering, Kafka ingestion, idempotent writes, and DLQ monitoring. &lt;a href="https://assettrackpro.com/" rel="noopener noreferrer"&gt;Find out more →&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Working on your IoT pipeline? Get the reliability and scalability of AssetTrackPro's infrastructure that takes care of everything under the hood, allowing you to focus on product development.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;&lt;a href="https://assettrackpro.com/" rel="noopener noreferrer"&gt;Learn more about AssetTrackPro ↗&lt;/a&gt;&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>iot</category>
      <category>backend</category>
      <category>kafka</category>
      <category>architecture</category>
    </item>
    <item>
      <title>From reactive to predictive: how AI-powered asset tracking reduces downtime</title>
      <dc:creator>Smrati</dc:creator>
      <pubDate>Wed, 20 May 2026 14:16:48 +0000</pubDate>
      <link>https://dev.to/smrati_verma/from-reactive-to-predictive-how-ai-powered-asset-tracking-reduces-downtime-2i5e</link>
      <guid>https://dev.to/smrati_verma/from-reactive-to-predictive-how-ai-powered-asset-tracking-reduces-downtime-2i5e</guid>
      <description>&lt;p&gt;&lt;em&gt;Most importantly, the maintenance decision you make too late is also your most expensive maintenance decision.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For most of the era of industrialization, maintenance was an operation that proceeded from a basic model: things break; someone repairs. If you were really advanced, then you might perform preventative maintenance based on a set schedule — you changed your oil every 3,000 miles and inspected the conveyor belt every three months. Not bad but still, ultimately, reactionary.&lt;/p&gt;

&lt;p&gt;What artificial intelligence-based asset monitoring offers is the ability to shift this paradigm completely — &lt;strong&gt;which neither calendars nor Excel sheets have ever been able to do, because they require pre-planning.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Reactive maintenance versus predictive: what’s really different
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Reactive maintenance&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You know when it breaks&lt;br&gt;
Unexpected failure occurs&lt;br&gt;
Spontaneous downtime and emergency maintenance&lt;br&gt;
Scrambling for replacement parts and stopping production&lt;br&gt;
Guesses made about the root of the issue&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Predictive maintenance&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You know before it breaks&lt;br&gt;
Anomalies detected weeks in advance&lt;br&gt;
Maintenance planned for convenient times&lt;br&gt;
Parts obtained before needed&lt;br&gt;
Exact chain of events documented&lt;/p&gt;

&lt;h2&gt;
  
  
  Transforming Tracking Data into Predictions through AI
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1&lt;br&gt;
Collecting continuous sensor data&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;IoT tags provide data on vibration, temperature, runtime hours, pressure, current load, from each and every asset in real-time — not sporadic checks but constant data flow to establish operational history for each machine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2&lt;br&gt;
Creating a baseline&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AI-based models learn normal operational patterns for each individual asset and create the corresponding baseline, which takes into account the specifics of its work at hand — it learns the unique characteristics of your 7-year-old conveyor motor, not your factory’s standards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3&lt;br&gt;
Detecting anomalies&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once sensor values deviate from those recorded during baseline creation — an insignificant vibration shift or a rise in temperature up by 3°C compared to the norm — it will be detected. Much earlier than a human being.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4&lt;br&gt;
Predicting failure probability&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Failure probability and time-to-failure range are assigned for each anomaly, providing maintenance personnel with prioritized assets. So, they know which asset is most likely to fail next — say, an 87% probable versus 12%.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flv23wffjvfyy20j35lsf.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flv23wffjvfyy20j35lsf.jpg" alt=" " width="612" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“Predictive maintenance is about more than minimizing downtime; it changes the nature of operations team and machines relationships.”&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers which make the business case
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;30–50%&lt;/strong&gt; cost savings on maintenance versus reactive approach&lt;br&gt;
&lt;strong&gt;70%&lt;/strong&gt; decrease in equipment failures reported by early adopters&lt;br&gt;
&lt;strong&gt;3–5x&lt;/strong&gt; return on investment on predictive maintenance spend in 18 months&lt;br&gt;
&lt;strong&gt;$260K&lt;/strong&gt; cost of each hour of planned downtime for manufacturers&lt;/p&gt;

&lt;h2&gt;
  
  
  Most things operation departments get wrong
&lt;/h2&gt;

&lt;p&gt;What’s the common error companies commit trying to go predictive? Looking at it as a tech challenge and not a data challenge. It’s impossible to make sense of any machine learning algorithms with insufficient coverage of sensors and lack of historical data. Applying predictive maintenance on only 20% of your assets will be like summarizing a book without having read it yourself.&lt;/p&gt;

&lt;p&gt;The other thing to remember is the people component. Any predictive system would be generating alarms and scores for the operator or maintenance person to react on. What sets apart those who derive maximum value out of the predictive solutions is that they have set up solid alarm management protocols and achieved initial alignment with their maintenance personnel.&lt;/p&gt;

&lt;p&gt;AssetTrackPro’s IoT-enabled tracking leverages real-time sensor data with predictive analytics, providing operations teams with failure likelihood metrics, maintenance windows, and asset health dashboards in logistics, manufacturing, and healthcare solutions. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Getting started&lt;/strong&gt;&lt;br&gt;
You don’t need to install sensors on all your assets from the get-go. You can start with the assets that represent the highest criticality and the highest cost of failure. It is those pieces of equipment where an unplanned downtime will cause the most harm. Collect at least 6–12 months of sensor data on those assets, allow predictive models to create baselines, and assess the results before rolling out further. Predictive maintenance builds up momentum as your history grows.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Are you tired of reactionary management? AssetTrackPro’s IoT tracking systems come with predictive capabilities built-in, allowing your maintenance team to go beyond solving problems and start preventing them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://assettrackpro.com/" rel="noopener noreferrer"&gt;Learn more about AssetTrackPro ↗&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>coldchain</category>
      <category>iot</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Integrating asset tracking data with ERP systems: a developer's guide</title>
      <dc:creator>Smrati</dc:creator>
      <pubDate>Wed, 20 May 2026 14:01:30 +0000</pubDate>
      <link>https://dev.to/smrati_verma/integrating-asset-tracking-data-with-erp-systems-a-developers-guide-2kb5</link>
      <guid>https://dev.to/smrati_verma/integrating-asset-tracking-data-with-erp-systems-a-developers-guide-2kb5</guid>
      <description>&lt;p&gt;Data for asset tracking in its own silo is valuable. But asset tracking data entering your ERP – updating inventory management data, generating purchase orders, creating maintenance workflows – is truly revolutionary. This is how you can make such an integration without adding to the already heavy maintenance burden.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why you should consider such integration
&lt;/h2&gt;

&lt;p&gt;Most often asset tracking systems and ERP systems are created independently of each other and serve entirely different goals. Asset tracking systems have information about locations of assets and their condition. ERP systems, in turn, contain all data on cost, ownership, and workflows associated with those assets. Both types of systems complement each other perfectly.&lt;/p&gt;

&lt;p&gt;Properly done, such integration makes it possible to automatically reconcile inventory, do usage-based depreciations, generate maintenance workflows, and kick off purchasing processes.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The four touchpoints of integration&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Location synchronization&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Positioning data being synchronized into ERP inventory management — exact positioning in real time of the asset based on SKU or asset identifier&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maintenance initiation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Thresholds or usage hours automatically initiating a maintenance order on the ERP CMMS module&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inventory sync&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Tracking data continuously synchronizing inventory counts to the ERP — no cycle counts necessary&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Depreciation &amp;amp; lifecycle&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Usage hours automatically populating ERP asset register records — actual usage depreciation rather than linear&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3dik8uj9zv7uf5mesgk2.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3dik8uj9zv7uf5mesgk2.jpg" alt=" " width="612" height="382"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways of achieving the integration
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Implementation&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Direct API&lt;/td&gt;
&lt;td&gt;Tracking system invokes ERP API REST/SOAP on each event&lt;/td&gt;
&lt;td&gt;Small number of events, simple environment, high coupling tolerable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Event queue&lt;/td&gt;
&lt;td&gt;Tracking events written to Kafka or RabbitMQ event queue&lt;/td&gt;
&lt;td&gt;Large numbers of events, ERP rate limits, replay required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Middleware&lt;/td&gt;
&lt;td&gt;ERP connection done via MuleSoft/Boomi or custom middleware&lt;/td&gt;
&lt;td&gt;multiple ERP modules, enterprise-level requirements&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Step 1 – Normalize the Asset Data Model
&lt;/h2&gt;

&lt;p&gt;There has to be a single standard data model for your assets before you start making API calls. The ERP system IDs and tracking system IDs of your assets are most likely different. Create a mapping layer first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Asset ID mapping — tracking system ID → ERP asset number&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;assetMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tag-7821&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;erpId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ASSET-00421&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;erpModule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fixed-assets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tag-7822&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;erpId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ASSET-00422&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;erpModule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;inventory&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;normalizeEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trackingEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mapping&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;assetMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;trackingEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;assetId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;mapping&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`No ERP mapping for &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;trackingEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;assetId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;erpAssetId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;mapping&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;erpId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;erpModule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;mapping&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;erpModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;trackingEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;trackingEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sensors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;trackingEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;asset-tracking&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2 – Post asset location updates to ERP inventory
&lt;/h2&gt;

&lt;p&gt;ERP systems have evolved enough today to allow RESTful API calls for inventory updates (SAP, Oracle, Microsoft Dynamics, etc.). Here is a way of pushing the asset location information through API calls to your ERP system’s warehouse management module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Push location update to ERP — SAP-style REST endpoint&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;syncLocationToERP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;normalizedEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;erpAssetId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;normalizedEvent&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ERP_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/v1/assets/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;erpAssetId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/location`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PATCH&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ERP_TOKEN&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Source-System&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;asset-tracking&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;warehouseLocation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;coordinates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;lng&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lng&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;lastSeen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;queueForRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;normalizedEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// never drop failed syncs&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 3 – Create maintenance work orders automatically&lt;br&gt;
Upon the occurrence of breaches of the threshold values of the sensors or service intervals due to usage hour counts of your assets, the software integration solution can automatically create work orders in the ERP system’s maintenance module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Triggered when asset tracking fires a maintenance alert&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createWorkOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;erpAssetId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalizeEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;workOrder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;assetId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;erpAssetId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PREVENTIVE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;severity&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;critical&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HIGH&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MEDIUM&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Auto-generated: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;triggerReason&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;triggeredBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;iot-sensor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sensorData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sensorSnapshot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;scheduledFor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getNextMaintenanceWindow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ERP_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/v1/work-orders`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ERP_TOKEN&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;workOrder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Idempotence tip&lt;/strong&gt;: Most ERP systems’ API endpoints are not designed to tolerate duplicate API requests. Be sure to include an idempotency key such as “X-Idempotency-Key: ” on each request.&lt;/p&gt;

&lt;p&gt;Step 4 - Manage ERP rate limits through a queue&lt;br&gt;
ERPs are not designed to withstand the ingestion volume from IoT systems. The default API rate limit from SAP is usually 100-500 requests per minute, while your IoT tracking system might be generating 10x more. Queue layer will buffer spikes and write records intelligently.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Queue-based ERP sync — batches events, respects rate limits&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;erpQueue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;processing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;flushToERP&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;processing&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;erpQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="nx"&gt;processing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;erpQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// process 50 at a time&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allSettled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;syncLocationToERP&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

  &lt;span class="nx"&gt;processing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;flushToERP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// 1 req/sec rate limiting&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Called every time a tracking event arrives&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;queueERPSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;erpQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;normalizeEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="nf"&gt;flushToERP&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Do NOT call ERP API from your MQTT handler directly. If the ERP is slow or rate-limited, your entire IoT data ingestion process would be halted. You should always decouple your ingestion with a queue, even if it is an in-memory queue for small-scale deployment.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Popular ERPs and their APIs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;SAP S/4HANA&lt;/strong&gt;&lt;br&gt;
RESTful OData v4 endpoints. Well documented, rate limited. Use SAP Integration Suite for complex mapping. Asset Management module supports usage-based record update.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Oracle ERP Cloud&lt;/strong&gt;&lt;br&gt;
REST APIs for the Fixed Assets and Inventory module. Allows bulk import using FBDI endpoint for location update at scale. Offers webhook support for outgoing messages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MS Dynamics 365&lt;/strong&gt;&lt;br&gt;
Dataverse (OData) API. Asset Management module works well with IoT Hub out-of-the-box. Use Power Automate for IoT tracking -&amp;gt; ERP processes with no code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NetSuite&lt;/strong&gt;&lt;br&gt;
SuiteScript REST API. Lightweight and friendly to developers. Good choice for midmarket solutions, when other ERPs have too much overhead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommended technology stack
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Node.js / Python&lt;/li&gt;
&lt;li&gt;Kafka / RabbitMQ&lt;/li&gt;
&lt;li&gt;Redis (de-duplication cache)&lt;/li&gt;
&lt;li&gt;REST / OData&lt;/li&gt;
&lt;li&gt;MuleSoft / Boomi&lt;/li&gt;
&lt;li&gt;PostgreSQL (audit logs)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Always keep an audit log of all ERP sync attempts made — whatever was synced, what did the ERP reply with, and whether a retry was required. If a data discrepancy in your ERP is detected six months down the line, this is how you will be able to track what went on and when.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;AssetTrackPro's integrations come with out-of-the-box connectors for SAP, Oracle, and Microsoft Dynamics, so all your tracking data is connected to your ERP right off the bat. &lt;a href="https://assettrackpro.com/" rel="noopener noreferrer"&gt;Check out our integrations →&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Are you connecting asset tracking with your ERP? Leave the integration layer to AssetTrackPro.&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;&lt;em&gt;&lt;a href="https://assettrackpro.com/" rel="noopener noreferrer"&gt;Learn more about AssetTrackPro →&lt;/a&gt;&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>erp</category>
      <category>iot</category>
      <category>backend</category>
      <category>api</category>
    </item>
  </channel>
</rss>
