<?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: Phil Yeh</title>
    <description>The latest articles on DEV Community by Phil Yeh (@philyeh).</description>
    <link>https://dev.to/philyeh</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%2F3617170%2Fe807fbe4-882c-42ae-a879-bb10f1193c08.jpeg</url>
      <title>DEV Community: Phil Yeh</title>
      <link>https://dev.to/philyeh</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/philyeh"/>
    <language>en</language>
    <item>
      <title>My Local RAG article went viral. The product it promoted sold 1 copy in 6 months.</title>
      <dc:creator>Phil Yeh</dc:creator>
      <pubDate>Wed, 20 May 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/philyeh/my-local-rag-article-went-viral-the-product-it-promoted-sold-1-copy-in-6-months-64o</link>
      <guid>https://dev.to/philyeh/my-local-rag-article-went-viral-the-product-it-promoted-sold-1-copy-in-6-months-64o</guid>
      <description>&lt;p&gt;Six months ago, I published a Dev.to article called &lt;a href="https://dev.to/philyeh/how-i-built-a-100-offline-second-brain-for-engineering-docs-using-docker-llama-3-no-openai-4gcj"&gt;&lt;em&gt;"How I built a 100% offline Second Brain for engineering docs using Docker + Llama 3 (No OpenAI)."&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It worked.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;380 reads on day one&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;7 bookmarks, 11 reactions, the works&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Top Docker Author Badge of the week&lt;/strong&gt; 🏆&lt;/li&gt;
&lt;li&gt;A comment thread that actually went somewhere — someone suggested &lt;a href="https://github.com/docling-project/docling" rel="noopener noreferrer"&gt;docling&lt;/a&gt;, someone else brought up the EU AI Act&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a brand-new Dev.to account with two articles to its name, this is the kind of result that makes you think you've cracked something.&lt;/p&gt;

&lt;p&gt;The product it linked to — a $59 Dockerized RAG toolkit &lt;a href="https://philyeh.gumroad.com/l/self-hosted-rag-llama3" rel="noopener noreferrer"&gt;on Gumroad&lt;/a&gt; — has sold &lt;strong&gt;1 copy&lt;/strong&gt; in the six months since.&lt;/p&gt;

&lt;p&gt;This isn't a "how I failed" post. The article didn't fail. The product didn't really fail either — it just didn't do what the article suggested it would. The gap between those two outcomes is the entire post.&lt;/p&gt;




&lt;h2&gt;
  
  
  How the article went viral
&lt;/h2&gt;

&lt;p&gt;Looking back, the article hit three buzzwords that the Dev.to algorithm loves stacked together: &lt;strong&gt;Llama 3&lt;/strong&gt;, &lt;strong&gt;Docker&lt;/strong&gt;, and &lt;strong&gt;"No OpenAI."&lt;/strong&gt; Add &lt;code&gt;#python&lt;/code&gt;, &lt;code&gt;#ai&lt;/code&gt;, &lt;code&gt;#docker&lt;/code&gt;, &lt;code&gt;#automation&lt;/code&gt; as tags and you're hitting four high-traffic feeds at once.&lt;/p&gt;

&lt;p&gt;The contrarian framing helped. &lt;em&gt;"No OpenAI"&lt;/em&gt; isn't a neutral technical choice — it's a position. Positions get bookmarks.&lt;/p&gt;

&lt;p&gt;The structure was clean: a relatable pain (you can't paste NDA-protected schematics into ChatGPT), a clear stack (Ollama + ChromaDB + Streamlit), a docker-compose snippet that looked copy-pasteable, and an honest section about the hard parts (PDF parsing, context window limits, Docker networking on GPU).&lt;/p&gt;

&lt;p&gt;I'm not going to pretend the article was lucky. It was structurally good content. The reads were earned.&lt;/p&gt;

&lt;p&gt;What I want to talk about is what I thought those reads &lt;em&gt;meant&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Here's the part most "building in public" posts skip
&lt;/h2&gt;

&lt;p&gt;I didn't start with a problem. I started with a market.&lt;/p&gt;

&lt;p&gt;As an indie maker with a full-time job and two kids, I have maybe 8–12 hours a week for side projects. I needed a product that could actually sell. I noticed Local LLM tutorials were getting strong traction on Dev.to, and I made what felt like an obvious assumption:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Engineers handle sensitive datasheets every day. Privacy must be a real pain point. So a self-hosted RAG with no cloud dependency must be something they'd pay for.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That assumption felt obvious. It wasn't.&lt;/p&gt;

&lt;p&gt;I want to be specific about what was wrong with it, because the error wasn't "Local LLM is a bad market" — it might be a fine market for someone else. The error was that I confused &lt;strong&gt;a problem I could imagine engineers having&lt;/strong&gt; with &lt;strong&gt;a problem engineers actually pay to solve.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Those aren't the same thing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three signals I missed at the time
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Signal 1: I wasn't using my own product
&lt;/h3&gt;

&lt;p&gt;The first signal I should have noticed was the most embarrassing one. I built a Local RAG to keep engineering datasheets off the cloud. My own company has zero IT restrictions. We paste datasheets into ChatGPT every day. Nobody cares.&lt;/p&gt;

&lt;p&gt;If I had genuinely needed the product, I would have used it for six months and refined it from real friction. I didn't. After the initial demos, the Docker stack sat there.&lt;/p&gt;

&lt;p&gt;This is the founder version of writing a recipe you've never cooked.&lt;/p&gt;

&lt;h3&gt;
  
  
  Signal 2: Six months, one sale
&lt;/h3&gt;

&lt;p&gt;The product launched alongside the article. Six months later: one paying customer at &lt;strong&gt;$59&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;One.&lt;/p&gt;

&lt;p&gt;Not "low-conversion-funnel" one. Just one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Signal 3: The feedback from that one customer was sharper than the badge
&lt;/h3&gt;

&lt;p&gt;A few months in, that customer sent me an email. Two points, paraphrased but accurate to his original frustration:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The demo video shows a Mandarin UI, but the Python product is in English. They don't match up.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;"Enterprise-grade"? I assumed it included auth and permission management. It doesn't. This is just for solo / small-office use, right?&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now, here's the thing: I never literally used the word "enterprise-grade" in the title. The product is called &lt;em&gt;"Local AI Knowledge Base: Dockerized RAG — Lite Edition."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;But "Knowledge Base" + "Lite Edition" implies a Pro / Enterprise tier exists. He's not wrong to expect that. The naming wrote a check the product couldn't cash.&lt;/p&gt;

&lt;p&gt;He wasn't being hostile. He was correcting me. And he was right on both counts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Mandarin demo video for a product sold globally on Gumroad is a localization gap I never even thought about.&lt;/li&gt;
&lt;li&gt;"Lite Edition" implies a ladder. There was no ladder. There was just one rung labeled "Lite."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That email taught me more about my product than the Top Docker Author Badge did.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the data actually says
&lt;/h2&gt;

&lt;p&gt;Let me lay both metrics side by side, because once you see them together the lesson is hard to miss:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Reads&lt;/td&gt;
&lt;td&gt;380+ (day one), still trickling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reactions&lt;/td&gt;
&lt;td&gt;11 (7 bookmarks)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Top Docker Author Badge&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reading list saves&lt;/td&gt;
&lt;td&gt;11 logged readers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Paying customers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;That customer's price point&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$59&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;That customer's feedback&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Naming was misleading, localization was off&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first five rows measure &lt;strong&gt;how good the article is&lt;/strong&gt;. The last three measure &lt;strong&gt;whether the product is worth paying for.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;These are unrelated experiments. Bookmarks don't predict sales. Badges don't predict sales. Reading list saves don't predict sales. A great article can sit on top of a product nobody wants, and a mediocre article can promote a product people actually need.&lt;/p&gt;

&lt;p&gt;I knew this in theory before I wrote the article. I didn't &lt;em&gt;know it&lt;/em&gt; know it until I had six months of data showing the gap.&lt;/p&gt;




&lt;h2&gt;
  
  
  The cognitive trap I want to name
&lt;/h2&gt;

&lt;p&gt;Here is the trap, stated plainly, so the next indie maker writing a Local LLM Dev.to article can skip it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Content engagement validates that &lt;strong&gt;the article resonates&lt;/strong&gt;. It does not validate that &lt;strong&gt;the product solves a problem people pay for.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What would have actually validated the product? A few things I didn't do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pre-sell before building.&lt;/strong&gt; Post the landing page and a waitlist before writing a line of code. If 50 engineers from the article's traffic don't drop their email, the demand probably isn't there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch for unprompted referrals.&lt;/strong&gt; Did any of the 11 bookmarkers share the product with a colleague &lt;em&gt;without me asking?&lt;/em&gt; No. That's the cleanest demand signal you can get, and it was silent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Talk to the one customer who bought.&lt;/strong&gt; I waited until he emailed me. I should have reached out the day his receipt was opened.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are clever. They're standard indie-maker advice. I skipped them because the article numbers felt like enough validation. They weren't.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'm doing differently now
&lt;/h2&gt;

&lt;p&gt;I'm not pulling the product down. The one paying customer is still using it, and the article still brings in occasional reads that send the right people to my Gumroad. It's a small, real, working channel.&lt;/p&gt;

&lt;p&gt;But the next product won't start from "what's trending on Dev.to." It'll start from a problem I personally hit at work, often enough that I'd pay $59 to make it go away.&lt;/p&gt;

&lt;p&gt;My latest tool, an &lt;a href="https://philyeh.gumroad.com/l/iiot-alarm-engine" rel="noopener noreferrer"&gt;IIoT Alarm Engine&lt;/a&gt;, came out of that filter. I needed real-time alerts from Modbus and MQTT devices, routed to Slack and Telegram. I built it because I was missing the feature myself, not because the tag was trending. It's too early to say whether it will sell better. But at least the assumption it's built on is one I can verify by using it.&lt;/p&gt;




&lt;h2&gt;
  
  
  If you take one thing from this post
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Reads, bookmarks, and badges measure your writing.&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Sales measure your product.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;They aren't the same axis. Don't read one and feel validated about the other.&lt;/p&gt;

&lt;p&gt;That's a six-month, one-sale lesson written down so you can have it for free.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;By &lt;a href="https://dev.to/philyeh"&gt;Phil Yeh&lt;/a&gt; — Senior Automation Engineer specializing in Industrial Python and developer tools. I publish post-mortems and engineering case studies on real problems shipping to factories — not the hype. If you want future post-mortems in your inbox: &lt;a href="https://philsindustrialnotes.beehiiv.com/" rel="noopener noreferrer"&gt;Phil's Industrial Notes&lt;/a&gt;. If you've made the same mistake, the comments are open.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Tools I've built since: &lt;a href="https://philyeh.gumroad.com/l/iiot-alarm-engine" rel="noopener noreferrer"&gt;IIoT Alarm Engine&lt;/a&gt;, &lt;a href="https://philyeh.gumroad.com/l/python-can-j1939-tool" rel="noopener noreferrer"&gt;J1939 Decoder GUI&lt;/a&gt;, &lt;a href="https://philyeh.gumroad.com/l/python-modbus-logger" rel="noopener noreferrer"&gt;Modbus Logger&lt;/a&gt;. Browse the rest: &lt;a href="https://philyeh.gumroad.com" rel="noopener noreferrer"&gt;philyeh.gumroad.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>indiehacker</category>
      <category>buildinpublic</category>
      <category>postmortem</category>
    </item>
    <item>
      <title>I bought the most expensive cable I could—and it still died. Welcome to RS485 vs 1000V DC.</title>
      <dc:creator>Phil Yeh</dc:creator>
      <pubDate>Wed, 13 May 2026 02:35:36 +0000</pubDate>
      <link>https://dev.to/philyeh/i-bought-the-most-expensive-cable-i-could-and-it-still-died-welcome-to-rs485-vs-1000v-dc-3ope</link>
      <guid>https://dev.to/philyeh/i-bought-the-most-expensive-cable-i-could-and-it-still-died-welcome-to-rs485-vs-1000v-dc-3ope</guid>
      <description>&lt;p&gt;A Class A solar pyranometer, the original shielded cable, 10 meters. Dies every 1-2 hours. In the end, I switched to 4-20mA.&lt;/p&gt;

&lt;p&gt;A few years ago at a solar farm site, I learned something the hard way: &lt;strong&gt;EMI doesn't care how expensive your cable is.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's what happened. We needed to bring data from an &lt;strong&gt;EKO MS-80S pyranometer&lt;/strong&gt; (Class A — the kind academic labs use) into our system. The device had RS485 / Modbus RTU. The manufacturer supplied a shielded twisted pair cable. 10 meters distance, standard industrial environment. By textbook standards, this should have been a plug-and-play setup.&lt;/p&gt;

&lt;p&gt;Reality: &lt;strong&gt;The first 1-2 hours worked perfectly. Then comms died. Completely died.&lt;/strong&gt; Every poll from the software side returned timeout — not corrupted values, not CRC errors, just no response.&lt;/p&gt;

&lt;p&gt;The weirdest part was the recovery: just unplug the RS485 connector and plug it back in, and comms returned to normal. But &lt;strong&gt;1-2 hours later, it died again&lt;/strong&gt;. Periodic, reproducible, like it was mocking me.&lt;/p&gt;

&lt;p&gt;This is the full story of that case — &lt;strong&gt;including why I eventually gave up on RS485 and switched to 4-20mA&lt;/strong&gt;. If you're also fighting industrial RS485 issues, this might help — not because I solved it, but because I'm admitting that &lt;strong&gt;some environments just aren't right for RS485&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  First wrong guess: device failure
&lt;/h2&gt;

&lt;p&gt;My first instinct was "the device is broken."&lt;/p&gt;

&lt;p&gt;I swapped in another unit of the same model. &lt;strong&gt;Same symptoms&lt;/strong&gt; — works for 1-2 hours, then permanent disconnect.&lt;/p&gt;

&lt;p&gt;Second instinct: "the cable is bad."&lt;/p&gt;

&lt;p&gt;Swapped in another OEM shielded cable. &lt;strong&gt;Still the same.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's when I realized: &lt;strong&gt;the problem isn't the device or the cable. It's the environment.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Re-checking the wiring: the culprit was at the cable entry
&lt;/h2&gt;

&lt;p&gt;Going back to look at the panel wiring, I noticed something I'd missed before.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;RS485 signal line and a 1000V DC line&lt;/strong&gt; were passing through the same panel entry hole, &lt;strong&gt;almost touching each other&lt;/strong&gt;. The separation was only about &lt;strong&gt;10-20 cm&lt;/strong&gt;, but at the moment they squeezed through the same metallic hole, they were practically in contact.&lt;/p&gt;

&lt;p&gt;Worse: the 1000V DC line behind that point connected to the site's &lt;strong&gt;DC-DC converter&lt;/strong&gt; — the kind of device that, in operation, produces MOSFET switching transients with &lt;strong&gt;dV/dt in the kV/μs range&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In the EMI world, this is called &lt;strong&gt;capacitive coupling at panel entry&lt;/strong&gt;. It's the textbook case study, chapter 3.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The original contractor hadn't considered EMI routing when designing this panel — RS485 signal lines and high-voltage DC lines just shared the same entry. On the drawing they looked "separate." Physically, they were stuck together.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why the OEM shielded cable couldn't save me
&lt;/h2&gt;

&lt;p&gt;My first question after this discovery: &lt;strong&gt;EKO's OEM cable is shielded twisted pair. Theoretically it should resist EMI. Why is it still failing?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The answer is in the physics of EMI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shielded cable handles radiated EMI&lt;/strong&gt; — external electromagnetic waves hitting the shield are diverted to ground. But &lt;strong&gt;the situation at the panel entry isn't radiated EMI — it's capacitive coupling&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When the 1000V DC line has high dV/dt, a strong transient electric field appears around it&lt;/li&gt;
&lt;li&gt;The RS485 line is almost in contact with it — effectively, there's a tiny capacitor between the two lines&lt;/li&gt;
&lt;li&gt;That capacitor couples the high-voltage side's transient voltage onto the RS485 signal&lt;/li&gt;
&lt;li&gt;The shield can't help in this case, because &lt;strong&gt;the ground reference itself is being disturbed&lt;/strong&gt; — both shield ends ground to a point that's now bouncing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In short: &lt;strong&gt;shielding protects against "electromagnetic waves coming from outside," but in this case the ground potential itself was jumping.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The disturbances accumulate. The RS485 transceiver IC internally latches up. After enough accumulation, it stops responding entirely. &lt;strong&gt;Unplugging and replugging resets the IC&lt;/strong&gt;, so it temporarily recovers — but the physical interference is still there, and 1-2 hours later it latches up again.&lt;/p&gt;




&lt;h2&gt;
  
  
  Things I tried, considered, and gave up on
&lt;/h2&gt;

&lt;p&gt;After understanding the cause, I tried a few approaches:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Improve shield grounding&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Re-grounded both ends of the shield. Marginal improvement. &lt;strong&gt;Reason&lt;/strong&gt;: the problem is capacitive coupling at the panel entry — improving shield grounding doesn't address that mechanism.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Rerun the cable away from the high-voltage line&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sounds straightforward, but &lt;strong&gt;the cable tray and panel entries were already installed&lt;/strong&gt;. Rerouting meant redoing the panel entry. &lt;strong&gt;Not feasible under site time pressure.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Add an optically isolated RS485 converter&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Technically viable — convert RS485 from "electrical transmission" to "optical transmission" and break the capacitive coupling path entirely. But:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Required procurement, testing, and re-wiring&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The procurement timeline didn't match the site deadline&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. More aggressive software retry / heartbeat&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I considered it, but the problem here wasn't "occasional errors," it was &lt;strong&gt;IC latch-up&lt;/strong&gt; — software retry can't recover from a hardware latch-up.&lt;/p&gt;

&lt;p&gt;In the end, I took the most low-tech path available: &lt;strong&gt;give up on RS485 and use the pyranometer's 4-20mA analog output instead&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why 4-20mA won in the end
&lt;/h2&gt;

&lt;p&gt;The EKO MS-80S has &lt;strong&gt;a 4-20mA analog output&lt;/strong&gt; in addition to RS485 (standard for industrial pyranometers).&lt;/p&gt;

&lt;p&gt;I wired it into the site PLC's Analog Input module. &lt;strong&gt;It never disconnected again.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Why does 4-20mA win in this environment?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Current signals are insensitive to capacitive coupling&lt;/strong&gt; — coupling is an electric field phenomenon, and electric fields disturb voltage, but 4-20mA transmits current&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI modules have industrial-grade isolation built in&lt;/strong&gt;, with much stronger surge immunity than RS485 transceivers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No IC latch-up risk&lt;/strong&gt; — even if the AI module reads a momentary glitch, the next sample period returns to normal&lt;/li&gt;
&lt;li&gt;Resolution drops from "Modbus floating-point" to "4-20mA mapped to 0-1600 W/m²" — but &lt;strong&gt;for a pyranometer, that's more than enough precision&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;A few takeaways stuck with me after this case:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. EMI isn't "just buy a shielded cable" simple&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Shielding handles radiation. &lt;strong&gt;It doesn't handle capacitive coupling at weak points like panel entries.&lt;/strong&gt; Expensive cable is necessary, not sufficient.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Panel entries are an underrated EMI weak point&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Metal panel enclosures themselves shield well, but &lt;strong&gt;the entry holes are discontinuities in the metal, and that's where all the EMI concentrates.&lt;/strong&gt; During design, signal lines and high-voltage lines have to be separated &lt;strong&gt;starting from the entry hole&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. RS485 has limits in heavy EMI environments&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;RS485 is a veteran protocol, designed in an era when EMI standards were nothing like today. In solar farms, inverter-heavy floors, motor-dense areas, &lt;strong&gt;RS485 is just a fragile choice.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. 4-20mA isn't outdated — it's the last line of immunity&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Industrial 4-20mA has been around for decades for a reason. &lt;strong&gt;When digital comms can't solve it, analog signals are often the last resort&lt;/strong&gt; — and they work surprisingly well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. EMI routing not considered at design time → nearly impossible to fix later&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This case ended up solved by switching the comm type, &lt;strong&gt;not by fixing the wiring&lt;/strong&gt; — because the site couldn't allow rewiring. &lt;strong&gt;The extra effort at design time really isn't wasted.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Checklist for other engineers
&lt;/h2&gt;

&lt;p&gt;If you're debugging RS485 stability issues on an industrial site, start by checking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Are RS485 signal lines and high-voltage lines (AC power, DC bus) "squeezed together" through the same panel entry?&lt;/li&gt;
&lt;li&gt;[ ] Are both ends of the shield properly grounded? Is there a potential difference between the ground points?&lt;/li&gt;
&lt;li&gt;[ ] Is the cable length within spec? Is the 120Ω termination resistor installed?&lt;/li&gt;
&lt;li&gt;[ ] Have you considered 4-20mA / Modbus TCP / LoRa as alternatives?&lt;/li&gt;
&lt;li&gt;[ ] Are you using a continuous monitoring tool to catch accumulating disturbance early?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one matters a lot — &lt;strong&gt;a lot of EMI problems don't "die instantly," they "die gradually."&lt;/strong&gt; If you only use ping / occasional polling to check, you won't see the pattern.&lt;/p&gt;

&lt;p&gt;When I debug this kind of issue, I usually use a Python tool to log comm state and raw frames over long periods, so I can review patterns afterwards. If you need something similar: &lt;a href="https://github.com/PhilYeh1212/Python-Modbus-Serial-Logger-GUI" rel="noopener noreferrer"&gt;Python-Modbus-Serial-Logger-GUI&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;RS485 is a veteran. &lt;strong&gt;Not an all-purpose hammer.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The biggest lesson for me: &lt;strong&gt;an industrial integration engineer's value isn't just in "solving problems" — it's also in "knowing which problems shouldn't be force-solved."&lt;/strong&gt; If the environment fundamentally isn't right for RS485, &lt;strong&gt;switching comm type can be far more meaningful than fighting it out.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Next time RS485 acts up on you, go check the panel entry first. The culprit might just be sitting there, squeezed in next to your signal line.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you've had a similar "RS485 dying in industrial environment" experience, drop a comment — these war stories are worth more than textbooks.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>modbus</category>
      <category>industrial</category>
      <category>emi</category>
    </item>
    <item>
      <title>Why Your Schneider PLC's Float32 Reads 1.4e-41 Instead of 25.5 C</title>
      <dc:creator>Phil Yeh</dc:creator>
      <pubDate>Tue, 05 May 2026 08:28:47 +0000</pubDate>
      <link>https://dev.to/philyeh/why-your-schneider-plcs-float32-reads-14e-41-instead-of-255degc-44lg</link>
      <guid>https://dev.to/philyeh/why-your-schneider-plcs-float32-reads-14e-41-instead-of-255degc-44lg</guid>
      <description>&lt;p&gt;You've connected your Python script to a Schneider M221, requested a holding&lt;br&gt;
register pair where you &lt;em&gt;know&lt;/em&gt; the temperature is &lt;strong&gt;25.5°C&lt;/strong&gt;, and you get back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1.401298464324817e-45
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not 25.5. Not even close. Some weird denormal float so small it might as&lt;br&gt;
well be zero.&lt;/p&gt;

&lt;p&gt;Welcome to the &lt;strong&gt;Modbus float32 byte-swap trap&lt;/strong&gt;. It's the single most&lt;br&gt;
common reason engineers think &lt;code&gt;pymodbus&lt;/code&gt; is broken when it isn't. The fix&lt;br&gt;
is 5 lines of code once you know what's happening — and that's what this&lt;br&gt;
article is about.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why this happens
&lt;/h2&gt;

&lt;p&gt;The Modbus specification defines registers as &lt;strong&gt;16-bit unsigned integers&lt;/strong&gt;.&lt;br&gt;
Period. That's it. The spec was written in 1979 and at the time, that's&lt;br&gt;
all anyone needed.&lt;/p&gt;

&lt;p&gt;But modern PLCs need to send floats, 32-bit integers, doubles, strings.&lt;br&gt;
The "solution" the industry adopted was: &lt;strong&gt;just split the larger value&lt;br&gt;
across multiple 16-bit registers&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The problem? &lt;strong&gt;The Modbus spec says nothing about the order&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Each PLC vendor decided independently whether to put the high word first&lt;br&gt;
or the low word first. Whether to swap bytes within a word. Whether to&lt;br&gt;
flip everything. The result is &lt;strong&gt;four different ways to encode the same&lt;br&gt;
float&lt;/strong&gt;, and there's no in-band way to know which one you're getting.&lt;/p&gt;

&lt;p&gt;This is why your PLC software shows 25.5°C while your Python script reads&lt;br&gt;
1.4e-41 — same bytes, different decoding order.&lt;/p&gt;


&lt;h2&gt;
  
  
  The 4 byte orders you'll meet
&lt;/h2&gt;

&lt;p&gt;A 32-bit float &lt;code&gt;25.5&lt;/code&gt; in IEEE-754 is the byte sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nasm"&gt;&lt;code&gt;&lt;span class="err"&gt;0&lt;/span&gt;&lt;span class="nf"&gt;x41&lt;/span&gt; &lt;span class="mh"&gt;0xCC&lt;/span&gt; &lt;span class="mh"&gt;0x00&lt;/span&gt; &lt;span class="mh"&gt;0x00&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now watch what happens when different PLCs put these 4 bytes into 2 Modbus&lt;br&gt;
registers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Order&lt;/th&gt;
&lt;th&gt;Register N&lt;/th&gt;
&lt;th&gt;Register N+1&lt;/th&gt;
&lt;th&gt;Common in&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;ABCD&lt;/strong&gt; (big-endian, no swap)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x41CC&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x0000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Allen-Bradley, ABB, "clean" implementations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;CDAB&lt;/strong&gt; (word-swap)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x0000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x41CC&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Schneider M221/M241&lt;/strong&gt;, some Siemens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;BADC&lt;/strong&gt; (byte-swap)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0xCC41&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x0000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rare — some old controllers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;DCBA&lt;/strong&gt; (byte + word swap)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x0000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0xCC41&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Mostly legacy hardware&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you decode bytes in &lt;code&gt;ABCD&lt;/code&gt; order when the device sent &lt;code&gt;CDAB&lt;/code&gt;, you get&lt;br&gt;
a tiny denormal float (the &lt;code&gt;1.4e-41&lt;/code&gt; you're seeing), or &lt;code&gt;nan&lt;/code&gt;, or &lt;code&gt;inf&lt;/code&gt;,&lt;br&gt;
or just nonsense.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schneider's &lt;code&gt;CDAB&lt;/code&gt; (word-swap) is the trap that catches most people.&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Code: decode all 4 orders in Python
&lt;/h2&gt;

&lt;p&gt;Pure stdlib, no dependencies. Drop this into your project:&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;struct&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;decode_float32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reg_high&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reg_low&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;byte_order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ABCD&lt;/span&gt;&lt;span class="sh"&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="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Decode two 16-bit Modbus registers into a float32.

    byte_order:
        ABCD - big-endian, no swap (default IEEE-754)
        CDAB - word-swap (Schneider, some Siemens)
        BADC - byte-swap within each word
        DCBA - byte + word swap
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# Pack the two registers into 4 bytes (big-endian)
&lt;/span&gt;    &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;HH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reg_high&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reg_low&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;byte_order&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ABCD&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="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unpack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw&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;elif&lt;/span&gt; &lt;span class="n"&gt;byte_order&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CDAB&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Swap the two 16-bit words
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unpack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw&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="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;raw&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;2&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;elif&lt;/span&gt; &lt;span class="n"&gt;byte_order&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BADC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Swap bytes within each word
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unpack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw&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;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;raw&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="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;raw&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="mi"&gt;3&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;elif&lt;/span&gt; &lt;span class="n"&gt;byte_order&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DCBA&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Reverse all 4 bytes
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unpack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;[::&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&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;Unknown byte order: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;byte_order&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="c1"&gt;# Example: read a value with pymodbus and decode all 4 ways
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pymodbus.client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ModbusTcpClient&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ModbusTcpClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;192.168.1.10&lt;/span&gt;&lt;span class="sh"&gt;"&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;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;response&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;read_holding_registers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;address&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="n"&gt;count&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="n"&gt;reg_high&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reg_low&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;registers&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="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;registers&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="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;ABCD: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;decode_float32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reg_high&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reg_low&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ABCD&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="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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CDAB: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;decode_float32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reg_high&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reg_low&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CDAB&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="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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BADC: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;decode_float32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reg_high&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reg_low&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;BADC&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="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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DCBA: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;decode_float32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reg_high&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reg_low&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;DCBA&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="sh"&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;The trick&lt;/strong&gt;: print all four when you're integrating a new device. The&lt;br&gt;
one that gives a sensible value (e.g. 25.5 when you know the temperature&lt;br&gt;
is 25.5) tells you the device's encoding. Lock it in for that device.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real-world examples by vendor
&lt;/h2&gt;

&lt;p&gt;Based on years of integration work, here's what I've seen most often:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Schneider M221, M241, M340&lt;/strong&gt; → Almost always &lt;strong&gt;CDAB&lt;/strong&gt; (word-swap).
This is the #1 source of "my Python script is broken" tickets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Siemens S7-1200, S7-1500&lt;/strong&gt; → Depends on how the float is stored. If
exposed via Modbus TCP wrapper, often &lt;strong&gt;CDAB&lt;/strong&gt; too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allen-Bradley CompactLogix&lt;/strong&gt; → Usually &lt;strong&gt;ABCD&lt;/strong&gt; when exposed through
Prosoft Modbus modules. Clean.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mitsubishi FX series&lt;/strong&gt; → &lt;strong&gt;ABCD&lt;/strong&gt; in most configurations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delta DVP&lt;/strong&gt; → &lt;strong&gt;ABCD&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Energy meters (Schneider PM5xxx)&lt;/strong&gt; → &lt;strong&gt;CDAB&lt;/strong&gt; typically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VFDs (ABB ACS, Schneider Altivar)&lt;/strong&gt; → Mixed — always test all 4.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The lesson: &lt;strong&gt;never assume&lt;/strong&gt;. Even within one vendor, different product&lt;br&gt;
lines pick different orders. Always do the "print all 4" test on a new&lt;br&gt;
device.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Some libraries do the swap silently — and silently wrong&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pymodbus&lt;/code&gt;'s built-in &lt;code&gt;BinaryPayloadDecoder&lt;/code&gt; lets you specify&lt;br&gt;
&lt;code&gt;byteorder=Endian.BIG, wordorder=Endian.LITTLE&lt;/code&gt; (which is &lt;code&gt;CDAB&lt;/code&gt;). But&lt;br&gt;
the API is confusing enough that I've seen people set both to &lt;code&gt;BIG&lt;/code&gt; and&lt;br&gt;
spend hours debugging. &lt;strong&gt;Decode manually with &lt;code&gt;struct&lt;/code&gt; — it's clearer.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The "negative number" red flag&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your decoded value is &lt;code&gt;-1.5e+38&lt;/code&gt; when you expect &lt;code&gt;25.5&lt;/code&gt;, you've&lt;br&gt;
&lt;em&gt;almost certainly&lt;/em&gt; got the byte order wrong. Single-digit positive&lt;br&gt;
temperatures don't accidentally encode as huge negative numbers under any&lt;br&gt;
normal scaling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. int32 has the same problem&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This article focuses on float32 because it's the most painful, but &lt;strong&gt;all&lt;br&gt;
multi-register types have this issue&lt;/strong&gt;. int32, uint32, int64, double —&lt;br&gt;
they all get split across registers and they all need the byte-order&lt;br&gt;
treatment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Some devices store the swap setting in a register&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Annoying but real: a few high-end PLCs let the &lt;em&gt;user&lt;/em&gt; configure&lt;br&gt;
endianness. Always check the device manual for any "byte order" or "word&lt;br&gt;
order" parameter before running test code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;The Modbus float32 byte-swap is one of those things that looks like a&lt;br&gt;
bug in your code but is actually a quirk of the protocol's history. Once&lt;br&gt;
you know to test all 4 orders, you can integrate any device in a few&lt;br&gt;
minutes instead of a few hours.&lt;/p&gt;

&lt;p&gt;If you're using this in production work and want a more polished version&lt;br&gt;
with a CSV recorder and Modbus TCP support, I sell a commercial license&lt;br&gt;
at &lt;a href="https://github.com/PhilYeh1212" rel="noopener noreferrer"&gt;Github&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;I write about industrial Python and protocol internals at&lt;br&gt;
&lt;strong&gt;&lt;a href="https://dev.to/philyeh"&gt;dev.to/philyeh&lt;/a&gt;&lt;/strong&gt; — new article every two&lt;br&gt;
weeks. If you've got a specific PLC integration story you want to read&lt;br&gt;
about, drop a comment.&lt;/p&gt;

</description>
      <category>python</category>
      <category>modbus</category>
      <category>plc</category>
      <category>automation</category>
    </item>
    <item>
      <title>From Theory to Practice: Digital Twin Core Concepts and Implementation Ideas for Engineers</title>
      <dc:creator>Phil Yeh</dc:creator>
      <pubDate>Fri, 12 Dec 2025 07:19:31 +0000</pubDate>
      <link>https://dev.to/philyeh/from-theory-to-practice-digital-twin-core-concepts-and-implementation-ideas-for-engineers-3f0l</link>
      <guid>https://dev.to/philyeh/from-theory-to-practice-digital-twin-core-concepts-and-implementation-ideas-for-engineers-3f0l</guid>
      <description>&lt;p&gt;🌐 The Bridge: Why Digital Twins Matter Now&lt;br&gt;
Have you ever wished you could predict a machine failure before it happens, or simulate the impact of a change in your supply chain without risking real-world downtime? That's the power of the Digital Twin.&lt;/p&gt;

&lt;p&gt;A Digital Twin is more than just a fancy 3D model. It's a live, virtual replica of a physical asset, system, or process that is constantly synchronized with real-world data. It serves as a testing ground, a crystal ball, and a diagnostic tool all rolled into one.&lt;/p&gt;

&lt;p&gt;For engineers, understanding Digital Twins is crucial for mastering the next phase of IoT and predictive analytics in fields like manufacturing, smart cities, and energy management.&lt;/p&gt;

&lt;p&gt;🔬 Step 1: Deconstructing the Digital Twin (The Three Core Layers)&lt;br&gt;
To build a Twin, we must first understand its three fundamental components.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Physical Asset Layer (The Source)
This layer includes the real-world equipment and the infrastructure used to gather data:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Key Technologies: IoT sensors, PLCs, and Edge Computing devices.&lt;/p&gt;

&lt;p&gt;Data Types: Real-time metrics like temperature, pressure, vibration, and energy consumption.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Virtual Model Layer (The Brain)
This is where the magic happens—the calculations, simulations, and predictions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Behavioral Models:&lt;/p&gt;

&lt;p&gt;Physics-Based: Uses known equations (thermodynamics, fluid dynamics) to predict behavior.&lt;/p&gt;

&lt;p&gt;Data-Driven (ML/AI): Uses historical data to train models that predict failures or optimal settings.&lt;/p&gt;

&lt;p&gt;Data Structure: Requires robust databases, often Time Series Databases (e.g., InfluxDB), to efficiently handle high-velocity, timestamped sensor data.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Connection &amp;amp; Services Layer (The Data Flow)
This is the communication pipeline that ensures the Twin is alive. It requires bi-directional data flow.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Inbound Flow (Physical to Virtual): Sensors push data to the cloud/edge (often via MQTT).&lt;/p&gt;

&lt;p&gt;Outbound Flow (Virtual to Physical): The Twin sends control commands or optimization suggestions back to the physical asset (e.g., throttling a motor speed).&lt;/p&gt;

&lt;p&gt;🛠️ Step 2: The Engineer's Starting Guide (A POC Blueprint)&lt;br&gt;
Ready to start building your first Twin? Here is a practical, two-phase approach focusing on open-source tools.&lt;/p&gt;

&lt;p&gt;Phase A: Data Ingestion and Basic Shadowing&lt;br&gt;
Your goal here is to create a "Shadow Twin"—a basic model that mirrors the live state.&lt;/p&gt;

&lt;p&gt;Set up MQTT Broker: Start a lightweight message broker (e.g., Mosquitto or a cloud service like AWS IoT Core).&lt;/p&gt;

&lt;p&gt;The Python Data Emitter: Use Python to simulate or collect sensor readings and publish them to the broker.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Python

# python_emitter.py - Simulating sensor data publishing
import paho.mqtt.client as mqtt
import time
import random

broker_url = "your_mqtt_broker"
topic = "asset/motor/temperature"

client = mqtt.Client()
client.connect(broker_url, 1883, 60)

while True:
    temp = 70 + random.uniform(-2, 2)  # Simulate temp fluctuation
    client.publish(topic, f"{time.time()},{temp:.2f}")
    print(f"Published: {temp:.2f}")
    time.sleep(5)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Visualization: Use Grafana to subscribe to the MQTT topic and display the data on a dashboard. This is your first visual Twin!&lt;/p&gt;

&lt;p&gt;Phase B: Integrating Predictive Intelligence&lt;br&gt;
Now, let's add the "intelligence" to the Twin using a simple Machine Learning model.&lt;/p&gt;

&lt;p&gt;Model Training (Hypothetical RUL Model): Assume you've trained a classification model (using Scikit-learn or similar) to predict the Remaining Useful Life (RUL) of your motor based on its temperature and vibration history.&lt;/p&gt;

&lt;p&gt;The Prediction Service: A dedicated Python service reads the latest data and feeds it into the trained model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Python
# prediction_service.py - The Twin's intelligence
import pandas as pd
from joblib import load
# Assume 'rul_predictor.joblib' is a trained ML model

model = load('rul_predictor.joblib')

def predict_rul(latest_data):
    # Process latest_data (e.g., features for the last 1 hour)
    features_df = pd.DataFrame([latest_data]) 
    prediction = model.predict(features_df)

    # 0 = Normal, 1 = Caution, 2 = Failure imminent
    return prediction[0]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  (This service would run continuously, reading from the Time Series DB)
&lt;/h1&gt;

&lt;p&gt;By connecting this prediction service to your live data stream, your Twin starts providing actionable insights (e.g., sending an alert when the RUL drops below 10 days).&lt;/p&gt;

&lt;p&gt;🚀 Step 3: Challenges and The Future&lt;br&gt;
Key Challenges in Implementation&lt;br&gt;
Data Quality: Twins are only as good as the data they receive. Dealing with sensor drift, gaps, and noise is a massive engineering challenge.&lt;/p&gt;

&lt;p&gt;Synchronization Latency: For real-time control applications (like self-driving cars), the delay between the physical event and the virtual update must be minimal.&lt;/p&gt;

&lt;p&gt;Scalability: Managing the data synchronization and simulation load for millions of individual Twins (e.g., every turbine in a wind farm).&lt;/p&gt;

&lt;p&gt;Looking Ahead&lt;br&gt;
The future of Digital Twins is exciting:&lt;/p&gt;

&lt;p&gt;XR Integration: Using AR/VR headsets to overlay live Twin data onto the physical asset during maintenance (e.g., seeing a projected temperature reading overlaid on the actual motor).&lt;/p&gt;

&lt;p&gt;Edge Twins: Shifting more simulation and predictive processing to Edge devices to reduce latency and cloud costs.&lt;/p&gt;

&lt;p&gt;📢 What’s Your Twin?&lt;br&gt;
Digital Twin technology transforms maintenance from reactive to predictive.&lt;/p&gt;

&lt;p&gt;What process or asset in your current engineering domain do you think is ripest for Digital Twin development? Share your ideas and challenges in the comments below!&lt;/p&gt;

</description>
      <category>iot</category>
      <category>python</category>
      <category>machinelearning</category>
      <category>devops</category>
    </item>
    <item>
      <title>The Architecture of Implicit Messaging: Implementing Raw CIP I/O in Python</title>
      <dc:creator>Phil Yeh</dc:creator>
      <pubDate>Wed, 03 Dec 2025 03:26:51 +0000</pubDate>
      <link>https://dev.to/philyeh/the-architecture-of-implicit-messaging-implementing-raw-cip-io-in-python-1o0c</link>
      <guid>https://dev.to/philyeh/the-architecture-of-implicit-messaging-implementing-raw-cip-io-in-python-1o0c</guid>
      <description>&lt;p&gt;The Challenge of Class 1 I/O&lt;br&gt;
Ethernet/IP (EIP) is based on the Common Industrial Protocol (CIP), which defines two primary messaging types:&lt;/p&gt;

&lt;p&gt;Explicit Messaging (TCP 44818): Request/Response—used for configuration and diagnostics.&lt;/p&gt;

&lt;p&gt;Implicit Messaging (UDP 2222): Cyclic I/O—used for high-speed, repetitive data exchange (Class 1 Connections).&lt;/p&gt;

&lt;p&gt;The architectural challenge lies in managing resource contention and time determinism when setting up these complex connections using raw sockets, rather than relying on commercial drivers.&lt;/p&gt;

&lt;p&gt;🏗️ The 4-Step Connection Sequence&lt;br&gt;
A robust implementation requires careful management of the TCP setup and the subsequent UDP I/O lifecycle. Our architecture follows these four steps within a cyclic process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Register Session (TCP)&lt;br&gt;
The process begins with an Explicit Message to the target device's Encapsulation Layer (TCP 44818). This step establishes a Session Handle, which identifies the connection for subsequent requests.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Forward Open (TCP)&lt;br&gt;
This is the most critical step. The client sends a Forward Open command containing all parameters necessary for the Class 1 I/O connection, including:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Requested Packet Interval (RPI).&lt;/p&gt;

&lt;p&gt;Connection Path (Assembly Instance IDs for the I/O data).&lt;/p&gt;

&lt;p&gt;Connection Timeout parameters. The device returns the O2T (Originator to Target) and T2O (Target to Origin) Connection IDs.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Cyclic I/O Exchange (UDP)&lt;br&gt;
Once the connection is established via TCP, the system shifts to high-speed UDP 2222. The client sends periodic data using the established O2T connection ID, and the device responds with the T2O data. This ensures minimal latency for cyclic data updates.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Forward Close &amp;amp; Teardown (TCP)&lt;br&gt;
To prevent the target device from eventually timing out and reporting a fault, the client must explicitly send a Forward Close command (TCP). This gracefully releases the resources allocated by the device before the socket is closed.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;💻 Architecture for Deterministic Testing&lt;br&gt;
To reliably test this intricate sequence, our study kit employs a dual-application structure:&lt;/p&gt;

&lt;p&gt;Raw Socket Client: Implements the full TCP and UDP state machine, managing the cyclic Open/Close sequence.&lt;/p&gt;

&lt;p&gt;Mock PLC Server: A separate Python application running on localhost that listens on TCP 44818 and UDP 2222. The mock server is essential for deterministic testing, as it guarantees correct, instantaneous responses during the handshake, allowing developers to isolate logic errors from physical layer noise.&lt;/p&gt;

&lt;p&gt;Python I/O Loop Structure&lt;br&gt;
The raw socket implementation requires precise packet assembly. Below is the conceptual structure used to manage the cyclic UDP exchange:&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Conceptual Structure for I/O Loop

while running:
    # 1. Open Session (TCP) &amp;amp; Forward Open (TCP) is performed here...

    # 2. UDP Exchange Phase:
    try:
        # Construct and send O2T packet using raw bytes
        udp_socket.sendto(o2t_packet, (target_ip, 2222))

        # Receive T2O packet (Target to Origin)
        received_data, addr = udp_socket.recvfrom(1024)

        # Log and parse the raw data...

    except socket.timeout:
        log("Warning: UDP I/O Timeout occurred.")

    # 3. Forward Close (TCP) &amp;amp; TCP Disconnect is performed here...

    time.sleep(RPI_WAIT_TIME)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🔒 Conclusion&lt;br&gt;
Understanding the raw socket implementation of CIP is critical for developers working in industrial cybersecurity, custom SCADA integration, or embedded systems where external libraries are too large or unavailable.&lt;/p&gt;

&lt;p&gt;We have documented the complete architecture and technical insights for this project on GitHub. If you are interested in acquiring the full, ready-to-deploy Python source code for this framework and diving into the raw packet structure for your own custom industrial solutions:&lt;/p&gt;

&lt;p&gt;View the complete project and detailed architecture: &lt;a href="https://github.com/PhilYeh1212/Python-EthernetIP-Raw-Socket-Client" rel="noopener noreferrer"&gt;Link&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By Phil Yeh | Senior Automation Engineer&lt;/p&gt;

</description>
      <category>ethernetip</category>
      <category>cip</category>
      <category>python</category>
      <category>sockets</category>
    </item>
    <item>
      <title>How I Fixed Python's Serial Freezing Issue: A Multi-threaded Tkinter Solution</title>
      <dc:creator>Phil Yeh</dc:creator>
      <pubDate>Fri, 28 Nov 2025 01:51:40 +0000</pubDate>
      <link>https://dev.to/philyeh/how-i-fixed-pythons-serial-freezing-issue-a-multi-threaded-tkinter-solution-2n21</link>
      <guid>https://dev.to/philyeh/how-i-fixed-pythons-serial-freezing-issue-a-multi-threaded-tkinter-solution-2n21</guid>
      <description>&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%2Fxzgyjojvogikz5lpjf9i.png" 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%2Fxzgyjojvogikz5lpjf9i.png" alt="FlowChart" width="634" height="726"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As automation engineers, we hit a frustrating wall: writing a simple Modbus GUI tool with Tkinter, only to have the entire application freeze ("Not Responding") while waiting for the slow RS485 sensor to reply.&lt;/p&gt;

&lt;p&gt;If you are dealing with &lt;strong&gt;serial I/O&lt;/strong&gt; and &lt;strong&gt;real-time UI updates&lt;/strong&gt;, the solution lies in proper threading.&lt;/p&gt;




&lt;h2&gt;
  
  
  🛠️ The Fix: Multi-threading for Stability
&lt;/h2&gt;

&lt;p&gt;The core issue is &lt;strong&gt;blocking I/O&lt;/strong&gt;. Since the main Tkinter loop handles the UI, waiting for the serial port stops the entire window from updating.&lt;/p&gt;

&lt;p&gt;The professional solution is to use the &lt;code&gt;threading&lt;/code&gt; module to separate the UI rendering thread from the I/O polling thread.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Threading Logic
&lt;/h3&gt;

&lt;p&gt;We implemented a daemon thread dedicated solely to reading the Modbus device. This ensures the UI is responsive, even if the polling takes several seconds.&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;# The UI remains responsive because the heavy lifting runs in a separate thread.
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;thread_helper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;thread&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_log&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;daemon&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="n"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c1"&gt;# Tkinter mainloop continues to run smoothly
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;📘 Modbus Protocol Primer: Deciphering the Hex (Tutorial Value)&lt;br&gt;
To effectively debug Modbus, you need to understand the structure of the command frame. The tool guides users through this structure directly on the screen:&lt;br&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%2F0yfkujf72fztwzz95xx1.png" 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%2F0yfkujf72fztwzz95xx1.png" alt="Sheet" width="523" height="170"&gt;&lt;/a&gt;&lt;br&gt;
(The default command in the tool is 01 03 00 00 00 03)&lt;/p&gt;

&lt;p&gt;✨ Production Features (The True Value)&lt;br&gt;
This project provides a robust, reusable template that is ready for industrial deployment. The source code handles all the time-consuming integration headaches:&lt;/p&gt;

&lt;p&gt;Universal Serial Setup: Supports custom Data Bits (5-8), Parity (N, E, O), and Stop Bits (1, 1.5, 2).&lt;/p&gt;

&lt;p&gt;Auto CRC-16: Automatically calculates and appends the Modbus checksum.&lt;/p&gt;

&lt;p&gt;Raw Hex Logging: Logs both the raw response and parsed data to a CSV file.&lt;/p&gt;

&lt;p&gt;📥 Get the Fully Integrated Solution&lt;br&gt;
This article shares the architectural solution. If you want the complete, multi-threaded GUI source code—including the full UI logic, auto-CRC calculation, and CSV logging feature—you can acquire the packaged files &lt;a href="https://pokhts.gumroad.com/l/python-modbus-logger" rel="noopener noreferrer"&gt;here-$9.9&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;By Phil Yeh | Senior Automation Engineer&lt;/p&gt;

</description>
      <category>python</category>
      <category>modbus</category>
      <category>tkinter</category>
      <category>threading</category>
    </item>
    <item>
      <title>Stop decoding Hex manually. I built a Python J1939 Sniffer with a GUI (No Hardware Needed)</title>
      <dc:creator>Phil Yeh</dc:creator>
      <pubDate>Tue, 25 Nov 2025 00:57:06 +0000</pubDate>
      <link>https://dev.to/philyeh/stop-decoding-hex-manually-i-built-a-python-j1939-sniffer-with-a-gui-no-hardware-needed-1p8o</link>
      <guid>https://dev.to/philyeh/stop-decoding-hex-manually-i-built-a-python-j1939-sniffer-with-a-gui-no-hardware-needed-1p8o</guid>
      <description>&lt;p&gt;After receiving the &lt;strong&gt;Top Docker Author badge&lt;/strong&gt; last week for my &lt;a href="https://dev.to/philyeh/how-i-built-a-100-offline-second-brain-for-engineering-docs-using-docker-llama-3-no-openai-4gcj"&gt;Offline AI post&lt;/a&gt; (thanks everyone! 🙏), many of you asked about my workflow for hardware and vehicle networks.&lt;/p&gt;

&lt;p&gt;So today, I'm switching gears from &lt;strong&gt;AI&lt;/strong&gt; to &lt;strong&gt;Heavy Duty Vehicles&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you work with &lt;strong&gt;CAN Bus&lt;/strong&gt; or &lt;strong&gt;SAE J1939&lt;/strong&gt; (Trucks, Buses, Machinery), you know the pain:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Professional tools are expensive:&lt;/strong&gt; A Vector CANalyzer license costs thousands of dollars.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Hex dumps are unreadable:&lt;/strong&gt; Seeing &lt;code&gt;18FEF100&lt;/code&gt; means nothing unless you memorize the J1939 spec.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Hardware dependency:&lt;/strong&gt; You usually need a physical adapter (PCAN, Kvaser) just to test your code.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To solve this, I built a &lt;strong&gt;Python-based J1939 Sniffer&lt;/strong&gt; that decodes PGNs automatically and includes a &lt;strong&gt;Simulation Mode&lt;/strong&gt; for hardware-free development.&lt;/p&gt;

&lt;h2&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%2Frbitq434bkdb78dwro1d.png" alt=" " width="800" height="643"&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  🏗️ The Challenge: Parsing 29-bit IDs
&lt;/h2&gt;

&lt;p&gt;Standard CAN (11-bit) is simple. But J1939 uses &lt;strong&gt;29-bit Extended Identifiers&lt;/strong&gt;, which pack a lot of data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Priority (3 bits)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PGN (Parameter Group Number) (18 bits)&lt;/strong&gt; &amp;lt;--- &lt;em&gt;The most important part&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Source Address (8 bits)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you get a raw ID like &lt;code&gt;0x18FEF100&lt;/code&gt;, you need to extract the &lt;strong&gt;PGN&lt;/strong&gt; to know what the message actually is.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Python Logic
&lt;/h3&gt;

&lt;p&gt;Here is the core logic I used to extract the PGN and Source Address from a raw integer ID:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parse_j1939_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;can_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Extract PGN and Source Address from a 29-bit CAN ID.
    Format: [Priority(3)] [Reserved(1)] [Data Page(1)] [PDU Format(8)] [PDU Specific(8)] [Source Address(8)]
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# Shift right by 8 bits to drop Source Address
&lt;/span&gt;    &lt;span class="c1"&gt;# Mask with 0x3FFFF to keep only the 18-bit PGN
&lt;/span&gt;    &lt;span class="n"&gt;pgn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;can_id&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x3FFFF&lt;/span&gt;

    &lt;span class="c1"&gt;# Mask with 0xFF to get the last 8 bits
&lt;/span&gt;    &lt;span class="n"&gt;source_address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;can_id&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xFF&lt;/span&gt;

    &lt;span class="c1"&gt;# Shift right by 26 bits to get Priority
&lt;/span&gt;    &lt;span class="n"&gt;priority&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;can_id&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x7&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pgn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source_address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;priority&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🛠️ The Solution: A GUI Sniffer&lt;br&gt;
I wrapped this logic into a Tkinter GUI using the python-can library. It listens to the bus, parses the ID, and looks up the PGN in a built-in dictionary.&lt;/p&gt;

&lt;p&gt;The Result&lt;br&gt;
Instead of staring at 18FEF100, the tool tells you: 👉 CCVS - Vehicle Speed&lt;/p&gt;

&lt;p&gt;Instead of 0CF00400, it shows: 👉 EEC1 - Engine Speed (RPM)&lt;/p&gt;

&lt;p&gt;Features&lt;br&gt;
🚛 Auto-Decode: Built-in dictionary for common PGNs (RPM, Temp, Speed, Battery).&lt;/p&gt;

&lt;p&gt;🎮 Simulation Mode: Click "Start Demo" to generate fake J1939 traffic. Perfect for testing UI logic without sitting in a truck.&lt;/p&gt;

&lt;p&gt;🔌 Universal Support: Works with Vector, Peak-System (PCAN), Kvaser, and slcan via python-can.&lt;/p&gt;

&lt;p&gt;📥 Try it yourself&lt;br&gt;
I have open-sourced the project structure and the J1939 parsing logic on GitHub. You can use it as a template for your own ECU tools.&lt;/p&gt;

&lt;p&gt;🔗 GitHub Repository: &lt;a href="https://github.com/PhilYeh1212/Python-CAN-Bus-J1939-Sniffer-GUI" rel="noopener noreferrer"&gt;Python-CAN-Bus-J1939-Sniffer-GUI&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🎁 For those who want the full package: If you want the complete, production-ready source code (including the GUI, Simulation Mode, and Multi-threading), I've made it available on Gumroad.&lt;/p&gt;

&lt;p&gt;🔥 Black Friday Deal: Use code BLACKFRIDAY for 15% OFF all my engineering tools.&lt;/p&gt;

&lt;p&gt;👉 Get the Full Source Code&lt;a href="https://pokhts.gumroad.com/l/python-can-j1939-tool?_gl=1*hjiu20*_ga*OTI5NDQ0OTEuMTc2MzM2MzI3OQ..*_ga_6LJN6D94N6*czE3NjQwMzIxNDIkbzE2JGcxJHQxNzY0MDMyMTUxJGo1MSRsMCRoMA.." rel="noopener noreferrer"&gt;(link)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy Hacking! 🚛&lt;/p&gt;

</description>
      <category>python</category>
      <category>automation</category>
      <category>engineering</category>
      <category>opensource</category>
    </item>
    <item>
      <title>5 Python Tools I Built to Automate My Industrial IoT Workflow (Open Source)</title>
      <dc:creator>Phil Yeh</dc:creator>
      <pubDate>Thu, 20 Nov 2025 10:00:43 +0000</pubDate>
      <link>https://dev.to/philyeh/5-python-tools-i-built-to-automate-my-industrial-iot-workflow-open-source-h3p</link>
      <guid>https://dev.to/philyeh/5-python-tools-i-built-to-automate-my-industrial-iot-workflow-open-source-h3p</guid>
      <description>&lt;p&gt;Update: 🏷️ Black Friday Sale is ON! Use code BLACKFRIDAY for 15% OFF on the Ultimate Toolkit.&lt;/p&gt;

&lt;p&gt;As a Senior Automation Engineer, I spend half my life debugging communication protocols. Modbus, MQTT, CAN Bus, Ethernet/IP... you name it.&lt;/p&gt;

&lt;p&gt;The problem is, most professional tools (like Vector CANalyzer or proprietary PLC software) are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Expensive&lt;/strong&gt; (Thousands of dollars).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Windows-only&lt;/strong&gt; (I love Docker/Linux).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Closed Source&lt;/strong&gt; (I can't customize them).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So, over the past few weekends, I decided to build my own &lt;strong&gt;"Survival Toolkit"&lt;/strong&gt; using Python.&lt;/p&gt;

&lt;p&gt;Here are the 5 open-source tools I created to replace expensive software, all available on my GitHub.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The "Privacy-First" AI Datasheet Reader
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; I have hundreds of PDF datasheets to read, but I can't upload them to ChatGPT due to NDA/Privacy concerns.&lt;br&gt;
&lt;strong&gt;Solution:&lt;/strong&gt; A 100% offline RAG system using Docker + Llama 3.&lt;/p&gt;

&lt;p&gt;It runs entirely on my local machine (RTX 3060). No data leaves the building.&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%2Fwaaa2mlsbhaduk3tpsfa.png" 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%2Fwaaa2mlsbhaduk3tpsfa.png" alt="AI Architecture" width="772" height="598"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stack:&lt;/strong&gt; &lt;code&gt;Docker&lt;/code&gt;, &lt;code&gt;Ollama&lt;/code&gt;, &lt;code&gt;ChromaDB&lt;/code&gt;, &lt;code&gt;Streamlit&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/PhilYeh1212/Local-AI-Knowledge-Base-Docker-Llama3" rel="noopener noreferrer"&gt;Local-AI-Knowledge-Base-Docker-Llama3&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2. The J1939 &amp;amp; CAN Bus Sniffer
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; Debugging vehicle ECUs usually requires a $300+ hardware adapter just to see the data.&lt;br&gt;
&lt;strong&gt;Solution:&lt;/strong&gt; A Python GUI that works with cheap USB-CAN adapters (slcan) and automatically decodes J1939 PGNs.&lt;/p&gt;

&lt;p&gt;It even has a &lt;strong&gt;"Demo Mode"&lt;/strong&gt; to simulate traffic if you don't have hardware.&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%2Fqjflhvnizjafrpmxtdx1.png" 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%2Fqjflhvnizjafrpmxtdx1.png" alt="CAN Bus Demo" width="800" height="643"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stack:&lt;/strong&gt; &lt;code&gt;Python&lt;/code&gt;, &lt;code&gt;tkinter&lt;/code&gt;, &lt;code&gt;python-can&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feature:&lt;/strong&gt; Decodes Engine Speed, Temp, and other PGNs instantly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/PhilYeh1212/Python-CAN-Bus-J1939-Sniffer-GUI" rel="noopener noreferrer"&gt;Python-CAN-Bus-J1939-Sniffer-GUI&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. The "Anti-Freezing" Modbus Logger
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; Writing a simple Python script to read RS485 is easy, but the GUI always freezes (blocks) while waiting for the sensor to reply.&lt;br&gt;
&lt;strong&gt;Solution:&lt;/strong&gt; A multi-threaded Modbus RTU Master.&lt;/p&gt;

&lt;p&gt;It separates the UI thread from the Serial polling thread, so the app remains responsive 100% of the time.&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%2F2k7zzbpwh9r6mu6dje4t.png" 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%2F2k7zzbpwh9r6mu6dje4t.png" alt="Modbus Logger" width="744" height="910"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stack:&lt;/strong&gt; &lt;code&gt;Python&lt;/code&gt;, &lt;code&gt;tkinter&lt;/code&gt;, &lt;code&gt;pyserial&lt;/code&gt;, &lt;code&gt;threading&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/PhilYeh1212/Python-Modbus-Serial-Logger-GUI" rel="noopener noreferrer"&gt;Python-Modbus-Serial-Logger-GUI&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4. The MQTT Data Recorder
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; Sometimes I just want to save MQTT sensor data to an Excel file for analysis, without setting up a database.&lt;br&gt;
&lt;strong&gt;Solution:&lt;/strong&gt; A lightweight MQTT Client that logs everything to CSV automatically.&lt;/p&gt;

&lt;p&gt;Updated to support the latest &lt;strong&gt;Paho-MQTT v2.0&lt;/strong&gt; standard.&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%2Fongdkkdp6yhn7lqf2oxs.png" 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%2Fongdkkdp6yhn7lqf2oxs.png" alt="MQTT Logger" width="598" height="780"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stack:&lt;/strong&gt; &lt;code&gt;Python&lt;/code&gt;, &lt;code&gt;paho-mqtt&lt;/code&gt;, &lt;code&gt;csv&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/PhilYeh1212/Python-MQTT-Data-Logger-GUI" rel="noopener noreferrer"&gt;Python-MQTT-Data-Logger-GUI&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  5. The Virtual Ethernet/IP Lab
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; Learning the CIP protocol (used by Rockwell/Omron PLCs) is hard without physical hardware.&lt;br&gt;
&lt;strong&gt;Solution:&lt;/strong&gt; A Mock PLC Server + Raw Socket Client.&lt;/p&gt;

&lt;p&gt;It simulates the full &lt;code&gt;Forward Open&lt;/code&gt; handshake and Implicit Messaging on your localhost.&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%2Ftqr5ytggqjnu1pqpe0kj.png" 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%2Ftqr5ytggqjnu1pqpe0kj.png" alt="Ethernet IP Demo" width="800" height="770"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stack:&lt;/strong&gt; &lt;code&gt;Pure Python&lt;/code&gt;, &lt;code&gt;socket&lt;/code&gt;, &lt;code&gt;struct&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/PhilYeh1212/Python-EthernetIP-Raw-Socket-Client" rel="noopener noreferrer"&gt;Python-EthernetIP-Raw-Socket-Client&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🎁 Conclusion
&lt;/h2&gt;

&lt;p&gt;Building your own tools is the best way to learn. Not only do you save money on licenses, but you also get full control over the source code.&lt;/p&gt;

&lt;p&gt;I have open-sourced the documentation and basic architecture for all these projects on GitHub.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you want the complete, production-ready source code for ALL these tools (5-in-1), I've bundled them together here:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://pokhts.gumroad.com/l/senior-engineer-toolkit" rel="noopener noreferrer"&gt;The Ultimate Senior Engineer Toolkit (Gumroad)&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>python</category>
      <category>automation</category>
      <category>productivity</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Local RAG with Llama 3 &amp; Docker: Build an Offline Second Brain (No OpenAI)</title>
      <dc:creator>Phil Yeh</dc:creator>
      <pubDate>Tue, 18 Nov 2025 09:03:19 +0000</pubDate>
      <link>https://dev.to/philyeh/how-i-built-a-100-offline-second-brain-for-engineering-docs-using-docker-llama-3-no-openai-4gcj</link>
      <guid>https://dev.to/philyeh/how-i-built-a-100-offline-second-brain-for-engineering-docs-using-docker-llama-3-no-openai-4gcj</guid>
      <description>&lt;p&gt;[UPDATE: Dec 2025] 🚀 Due to the overwhelming interest in this Local RAG setup, I’ve officially released the production-ready toolkit on Gumroad to help you save hours of configuration time!&lt;/p&gt;

&lt;p&gt;Based on your feedback, I’ve created two versions to suit your needs:&lt;/p&gt;

&lt;p&gt;Option 1: The "Lite" Edition ($59) – Perfect for developers! Get the full Dockerized source code, Streamlit UI, and PDF pipeline. Ideal if you want to deploy it yourself and own the code.&lt;/p&gt;

&lt;p&gt;Option 2: The "Pro" Solution ($299) – For enterprises and busy professionals. Includes the full suite PLUS a 1-on-1 Remote Setup Service. I will personally ensure the system is perfectly tuned and running on your hardware.&lt;/p&gt;

&lt;p&gt;Why use this?&lt;/p&gt;

&lt;p&gt;100% Private: No data ever leaves your machine (No OpenAI/Cloud APIs).&lt;/p&gt;

&lt;p&gt;One-Click Setup: Move from "dependency hell" to a functional RAG system in minutes using Docker.&lt;/p&gt;

&lt;p&gt;Proven Results: Check out the new Demo Video on the product page to see it in action!&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://pokhts.gumroad.com/l/ai-knowledge-docker" rel="noopener noreferrer"&gt;Get the Local RAG Toolkit here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🎉 Update: Wow! This post was awarded the Top Docker Author Badge of the week! Thanks to everyone for the amazing support and feedback. 🙏&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%2F4ipfyzqtu7guohbozp83.png" 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%2F4ipfyzqtu7guohbozp83.png" alt=" " width="776" height="582"&gt;&lt;/a&gt;&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%2F4nx3aaczxx2oiplkup4y.png" 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%2F4nx3aaczxx2oiplkup4y.png" alt="Offline AI System Architecture Docker Llama 3" width="772" height="598"&gt;&lt;/a&gt;&lt;br&gt;
Stop sending your sensitive datasheets to the cloud. Here is how I deployed a private, enterprise-grade RAG system.&lt;/p&gt;

&lt;p&gt;As a Senior Automation Engineer, I deal with hundreds of technical documents every month — datasheets, schematics, internal protocols, and legacy codebases.&lt;/p&gt;

&lt;p&gt;We all know the power of LLMs like GPT-4. Being able to ask, “What is the maximum voltage for the RS485 module on page 42?” and getting an instant answer is a game-changer.&lt;/p&gt;

&lt;p&gt;But there is a problem: Privacy.&lt;/p&gt;

&lt;p&gt;I cannot paste proprietary schematics or NDA-protected specs into ChatGPT. The risk of data leakage is simply too high.&lt;/p&gt;

&lt;p&gt;So, I set out to build a solution. I wanted a “Second Brain” that was:&lt;/p&gt;

&lt;p&gt;100% Offline: No data leaves my local network.&lt;/p&gt;

&lt;p&gt;Free to run: No monthly API subscriptions (bye-bye, OpenAI bills).&lt;/p&gt;

&lt;p&gt;Dockerized: Easy to deploy without “dependency hell.”&lt;/p&gt;

&lt;p&gt;Here is the architecture I built using Llama 3, Ollama, and Docker.&lt;/p&gt;

&lt;p&gt;The Architecture: Why this Tech Stack?&lt;br&gt;
Building a RAG (Retrieval-Augmented Generation) system locally used to be a nightmare of Python dependencies and CUDA driver issues. To solve this, I designed a containerized microservices architecture.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The Brain: Ollama + Llama 3&lt;br&gt;
I chose Ollama as the inference engine because it’s lightweight and efficient. For the model, Meta’s Llama 3 (8B) is the current sweet spot — it’s surprisingly capable of reasoning through technical documentation and runs smoothly on consumer GPUs (like an RTX 3060).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Memory: ChromaDB&lt;br&gt;
For the vector database, I used ChromaDB. It runs locally, requires zero setup, and handles vector retrieval incredibly fast.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Glue: Python &amp;amp; Streamlit&lt;br&gt;
The backend is written in Python, handling the “Ingestion Pipeline”:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Parsing: Extracting text from PDFs.&lt;/p&gt;

&lt;p&gt;Chunking: Breaking text into manageable pieces.&lt;/p&gt;

&lt;p&gt;Embedding: Converting text into vectors using the mxbai-embed-large model.&lt;/p&gt;

&lt;p&gt;UI: A clean Streamlit interface for chatting with the data.&lt;/p&gt;

&lt;p&gt;How It Works (The “Happy Path”)&lt;br&gt;
The beauty of this system is the Docker implementation. Instead of installing Python libraries manually, the entire system spins up with a single command.&lt;/p&gt;

&lt;p&gt;The docker-compose.yml orchestrates the communication between the AI engine, the database, and the UI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;YAML

# Simplified concept of the setup
services:
  ollama:
    image: ollama/ollama:latest
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]

  backend:
    build: ./app
    depends_on:
      - ollama
      - chromadb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once running, the workflow is simple:&lt;/p&gt;

&lt;p&gt;Drop your PDF files into the knowledge_base folder.&lt;/p&gt;

&lt;p&gt;Click “Update Knowledge Base” in the UI.&lt;/p&gt;

&lt;p&gt;Start chatting.&lt;/p&gt;

&lt;p&gt;The system automatically vectorizes your documents. When you ask a question, it retrieves the most relevant paragraphs and feeds them to Llama 3 as context.&lt;/p&gt;

&lt;p&gt;The Challenge: It’s Not Just About “Running” the Model&lt;br&gt;
While the concept sounds simple, getting it to production-grade stability took me weeks of debugging.&lt;/p&gt;

&lt;p&gt;Here is what most “Hello World” tutorials don’t tell you:&lt;/p&gt;

&lt;p&gt;PDF Parsing is messy: Tables in engineering datasheets often break standard parsers.&lt;/p&gt;

&lt;p&gt;Context Window limits: Llama 3 has a limit. You need a smart “Sliding Window” strategy for chunking large documents.&lt;/p&gt;

&lt;p&gt;Docker Networking: Getting the Python container to talk to the Ollama container on the host GPU requires specific networking configurations.&lt;/p&gt;

&lt;p&gt;I spent countless nights fixing connection timeouts, optimizing embedding models, and ensuring the UI doesn’t freeze during large file ingestions.&lt;/p&gt;

&lt;p&gt;Want to Build Your Own?&lt;br&gt;
If you are an engineer or developer who wants to own your data, I highly recommend building a local RAG system. It’s a great way to learn about GenAI architecture.&lt;/p&gt;

&lt;p&gt;However, if you value your time and want to skip the configuration headaches, I have packaged my entire setup into a ready-to-deploy solution.&lt;/p&gt;

&lt;p&gt;It includes:&lt;/p&gt;

&lt;p&gt;✅ The Complete Source Code (Python/Streamlit).&lt;/p&gt;

&lt;p&gt;✅ Production-Ready Docker Compose file.&lt;/p&gt;

&lt;p&gt;✅ Optimized Ingestion Logic for technical docs.&lt;/p&gt;

&lt;p&gt;✅ Setup Guide for Windows/Linux.&lt;/p&gt;

&lt;p&gt;You can download the full package and view the detailed documentation on my GitHub.&lt;/p&gt;

&lt;p&gt;👉 View the Project &amp;amp; Download Source Code on GitHub &lt;a href="https://github.com/PhilYeh1212/Local-AI-Knowledge-Base-Docker-Llama3/blob/main/README.md" rel="noopener noreferrer"&gt;link&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By Phil Yeh Senior Automation Engineer specializing in Industrial IoT and Local AI solutions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://pokhts.gumroad.com/l/python-mqtt-logger" rel="noopener noreferrer"&gt;Python MQTT Data Logger&lt;/a&gt;&lt;/strong&gt; - A clean GUI to debug brokers &amp;amp; auto-save data to CSV.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://pokhts.gumroad.com/l/python-can-j1939-tool" rel="noopener noreferrer"&gt;Python CAN Bus &amp;amp; J1939 Sniffer&lt;/a&gt;&lt;/strong&gt; - Decode vehicle data without expensive hardware.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://pokhts.gumroad.com/l/python-modbus-logger" rel="noopener noreferrer"&gt;Python Modbus Data Logger&lt;/a&gt;&lt;/strong&gt; - Debug RS485 devices with a multi-threaded GUI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://pokhts.gumroad.com/l/ethernet-ip-study-kit" rel="noopener noreferrer"&gt;Ethernet/IP Study Kit&lt;/a&gt;&lt;/strong&gt; - Learn CIP protocol with a Python-based mock PLC.
****
👉 &lt;strong&gt;Get the Source Code for all these tools:&lt;/strong&gt; &lt;a href="https://gumroad.com/products" rel="noopener noreferrer"&gt;Visit my Gumroad Store&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>docker</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
