<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[My Awesome Publications]]></title><description><![CDATA[My Awesome Publications]]></description><link>https://tiaforge.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Fri, 30 Oct 2026 16:09:49 GMT</lastBuildDate><atom:link href="https://tiaforge.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[AI Writes the PLC Code. Proving It Works Is the Hard Part — 7 Free Hands-On Lessons]]></title><description><![CDATA[A free 7-lesson PLC AI course: write SCL into a real TIA Portal project, read a legacy project, do an IO]]></description><link>https://tiaforge.hashnode.dev/ai-writes-the-plc-code-proving-it-works-is-the-hard-part-7-free-hands-on-lessons</link><guid isPermaLink="true">https://tiaforge.hashnode.dev/ai-writes-the-plc-code-proving-it-works-is-the-hard-part-7-free-hands-on-lessons</guid><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><category><![CDATA[automation]]></category><category><![CDATA[Programming Tips]]></category><category><![CDATA[PLC]]></category><dc:creator><![CDATA[tia-forge]]></dc:creator><pubDate>Tue, 20 Oct 2026 03:08:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6accfca9505c37174f41282f/12d99d01-2700-4bf7-bcd3-0a53c47ada7a.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>The missing piece in AI-assisted PLC programming isn't code generation — it's proof.</strong> <a href="https://tia.hubwiz.com/learn-lad/ai/?utm_source=hashnode&amp;utm_medium=syndication&amp;utm_campaign=ai-plc-programming-7-lessons-verification">PLC AI 实战</a> is a free, seven-lesson, text-and-screenshots course that breaks that problem into seven checkable endpoints: a compile with 0 errors and 0 warnings, a read-back that matches the spec, a scenario report that says PASS/FAIL, and a regression run that shows behavior didn't change. No videos, no course upsell, and nothing to download.</p>
<p>I went through all seven lessons and pulled out the parts that matter if you already write PLC code for a living — plus the three failures the course deliberately shows instead of hiding.</p>
<h2>1. The bottleneck moved from "write it" to "prove it"</h2>
<p>Almost every engineer's first reaction to AI-generated SCL is the same: <em>looks about right, but I don't dare use it.</em></p>
<p>"Looks about right" comes from the model's strengths — interfaces, variable names, IF/THEN structure. "I don't dare" comes from the plant floor: a wrong PLC program doesn't cost you a compile error, it costs you a machine doing the wrong thing.</p>
<p>The course is valuable because it shows three real failures rather than a demo where the AI gets it right the first time:</p>
<ol>
<li><p><strong>Compiles clean, spec not implemented.</strong> The requirement said "after the sensor triggers, output for 2 seconds." The implementation used <code>TON</code>. The sensor is only high for one scan cycle, so the timer's <code>ET</code> never reaches 2000 ms and the output never turns on once.</p>
</li>
<li><p><strong>Compiles clean, nothing moves.</strong> While refactoring a three-motor delayed-start sequence, <code>TP</code> was written as <code>TON</code>. Compile: 0 errors, 0 warnings. Press the button: none of the three motors start.</p>
</li>
<li><p><strong>Compiles clean, the field never reads it.</strong> When changing an IO address, the TIA compiler <strong>does not check whether a physical channel exists</strong>. Point a tag at <code>I30.0</code>, which this project doesn't have, and you still get 0 errors and 0 warnings.</p>
</li>
</ol>
<p>All three have the same shape: <strong>the compiler cannot see them.</strong> So the only defensible criterion is: every step needs evidence someone else can re-check — not the agent saying "done."</p>
<h2>2. Four layers of verification, and what each one does <em>not</em> prove</h2>
<p>This is the table worth remembering. Each lesson tells you which layer you're on:</p>
<table>
<thead>
<tr>
<th>Layer</th>
<th>Who judges</th>
<th>Proves</th>
<th>Does not prove</th>
</tr>
</thead>
<tbody><tr>
<td>Structural validation (<code>scl_validate</code>)</td>
<td>The cloud simulator's parser</td>
<td>Source parses, structure is legal</td>
<td>TIA compile rules, behavior</td>
</tr>
<tr>
<td>TIA compile</td>
<td>The TIA Portal compiler</td>
<td>The block compiles in <em>your</em> project (0 errors, 0 warnings)</td>
<td>Behavior matches the spec; the address has a physical channel</td>
</tr>
<tr>
<td>Cloud simulation</td>
<td>A scan-level simulator</td>
<td>Under a given input sequence, output assertions hold (PASS/FAIL)</td>
<td>Hardware, bus, HMI, safety circuits; also not the TIA compiler</td>
</tr>
<tr>
<td>Virtual PLC / real hardware</td>
<td>S7-PLCSIM Advanced / the machine</td>
<td>Real scan behavior after download, diagnostics, faults</td>
<td>Safety functions, interlocks, e-stops still need human review</td>
</tr>
</tbody></table>
<p>A practical detail: <code>scl_validate</code> returning <code>ok: true</code> only means the source <strong>parses</strong>. The timer call from lesson 1 that omits <code>PT</code> will likely pass structural validation, while TIA reports <code>Parameter 'PT' has to be used.</code></p>
<h2>3. How the seven lessons are ordered</h2>
<table>
<thead>
<tr>
<th>Stage</th>
<th>Lesson</th>
<th>Tool</th>
<th>Time</th>
<th>Endpoint (what you walk away with)</th>
</tr>
</thead>
<tbody><tr>
<td>Beginner</td>
<td>Write SCL: from requirement to 0 errors, 0 warnings</td>
<td>TiaLink</td>
<td>30 min</td>
<td>A clean compile plus a requirement → implementation → verdict table</td>
</tr>
<tr>
<td>Beginner</td>
<td>Modify the first example in the library</td>
<td>TiaLink</td>
<td>20 min</td>
<td>One added tag, one series contact, clean compile, structural read-back</td>
</tr>
<tr>
<td>Intermediate</td>
<td>Read an undocumented legacy TIA project</td>
<td>TiaLink (read-only)</td>
<td>25 min</td>
<td>A project specification document + a human review list; zero changes</td>
</tr>
<tr>
<td>Intermediate</td>
<td>Change one IO address — what breaks?</td>
<td>TiaLink (read-only + dryRun)</td>
<td>25 min</td>
<td>Reference list, risk list, change plan, preflight result</td>
</tr>
<tr>
<td>Intermediate</td>
<td>Simulate before commissioning: download to a virtual PLC</td>
<td>TiaLink + PLCSIM Advanced</td>
<td>35 min</td>
<td>A scenario table with <strong>measured</strong> values</td>
</tr>
<tr>
<td>Intermediate</td>
<td>No TIA install: turn SCL into PASS/FAIL in the cloud</td>
<td>Cloud simulation MCP</td>
<td>35 min</td>
<td>An ALL PASS report plus the scan count</td>
</tr>
<tr>
<td>Hands-on</td>
<td>Refactor without changing behavior: 3 copies → 1 FB</td>
<td>TiaLink + PLCSIM</td>
<td>40 min</td>
<td>Baseline vs regression, point by point</td>
</tr>
</tbody></table>
<p>About <strong>3.5 hours</strong> total. The order isn't by topic — it's the order work actually happens: write → modify → read → assess impact → simulate → verify → regress. Every lesson ends with a verification checklist; if you can't tick it, you're not done.</p>
<h2>4. The seven things that actually trip people up</h2>
<h3>4.1 First compile: 2 errors, 2 warnings</h3>
<p>The only input is one requirement: an <code>FB_Sorter</code> with <code>Start</code> / <code>SensorA</code> / <code>SensorB</code> / <code>Reset</code> in, <code>DiverterA</code> / <code>DiverterB</code> / <code>Running</code> out, interlocked, 2-second outputs, reset clears everything. Everything after that — read the project, add a CPU, write the block, import, compile, fix, read back, save — is the agent's job.</p>
<p>Measured on <strong>TiaLink 1.1.0.0 + TIA Portal V19 + S7-1200 (CPU 1214C, V4.6)</strong>. The first version reads like it satisfies the spec, and compiles to <strong>2 errors and 2 warnings</strong>:</p>
<ul>
<li><p><code>[Error] Parameter 'PT' has to be used.</code> — a timer call must supply <code>PT</code> <strong>every time</strong>, including the reset branch. The correct shape is one call per scan with reset folded into <code>IN</code>.</p>
</li>
<li><p><code>[Warning] The parameter '#Running' might not be initialized.</code> — the output is read before it is definitely assigned. Carry the state in a Static variable with an initial value and assign the output unconditionally at the end.</p>
</li>
</ul>
<p>Two engineering disciplines hide here as well: every import leaves behind an <strong>external source file</strong> in the project (they pile up over iterations), and <strong>import ≠ save</strong> — a successful import is not a saved project.</p>
<h3>4.2 Modifying an example: don't import it into a live project</h3>
<p>The second lesson practices the more common job: <strong>take existing logic and change it.</strong> The simplest library example (a single-button toggle, 4 networks in LAD) gets one new input: <code>Enable</code> at <code>%I0.1</code>, forcing the output off when 0 and preserving the toggle when 1.</p>
<p>Two edits total: one new tag, and one series contact after the power rail in Network 2. After a clean compile, structural read-back confirms <code>Motor = Enable AND (Tag_1 OR Motor) AND NOT Tag_2</code>.</p>
<p>Two genuinely annoying traps:</p>
<ul>
<li><p><code>UId</code> <strong>must be a decimal integer</strong> — hex is rejected outright;</p>
</li>
<li><p><strong>TIA renumbers</strong> <code>UId</code> <strong>after import</strong>, so you cannot assert on <code>UId</code>; assert on structure instead.</p>
</li>
</ul>
<p>And one precondition: examples ship with <code>Main (OB1)</code> and <code>Default tag table</code>, so importing <strong>overwrites same-named objects</strong>. Get a clean baseline compile first, so that when something breaks later you know whose fault it is.</p>
<h3>4.3 Reading a legacy project: separate confirmed / inferred / unconfirmed</h3>
<p>The third lesson is read-only against an MIT-licensed sample project: 5 blocks (<code>OB1</code> + <code>FB2/FB3</code> + <code>DB12/DB13</code>), 51 tags, 3 HMI screens with 40 variables. The output is a project specification, with zero changes to the project.</p>
<p>Three classic legacy shapes show up immediately:</p>
<ul>
<li><p><strong>The CPU is an F-type</strong> (<code>1513F-1 PN</code>, <code>6ES7 513-1FL02-0AB0</code>, firmware V2.9) but there are no F-blocks. That doesn't mean the hardware is wrong, but it has to be listed as a safety item for a human to confirm.</p>
</li>
<li><p><strong>An FB with an empty interface is not an empty block</strong>: <code>Auto_mode</code> / <code>Manu_mode</code> have no parameters at all; the logic reads and writes global variables (<code>ZS-*</code>, <code>CV-*</code>, <code>HMI_*</code>, <code>SIM_*</code>). Which means: track change impact through <strong>variables</strong>, not through interfaces.</p>
</li>
<li><p><strong>One giant network</strong>: <code>Auto_mode</code> has a single network with 36 operands; 2 of <code>Main</code>'s 4 networks are empty. Treating that as "one rung" is guaranteed to be wrong — split it into functional segments (sensor chain, actuator chain, alarm/simulation, MATLAB handshake).</p>
</li>
</ul>
<p>One more reconciliation: <strong>6 of the 51 tags are not referenced by any PLC block</strong>, and <code>HMI_PID</code> deserves special mention — the HMI has a PID screen, but there is no PID block and the tag is unused. "PID may not be implemented" is a human-confirm item. A document that doesn't distinguish confirmed / inferred / unconfirmed is fiction.</p>
<h3>4.4 Change-impact analysis: a clean compile does not mean the field can read it</h3>
<p>The fourth lesson answers "can I touch it?" One candidate change: <code>ZS-101</code> (Infeed Sensor) from <code>%I29.0</code> to <code>%I29.4</code>. Read-only throughout, with a <code>dryRun</code> preflight at the end.</p>
<p>The result is cleaner than expected — and more dangerous:</p>
<ul>
<li><p><strong>References</strong>: 6 in <code>Auto_mode</code> + 2 in <code>Manu_mode</code>, <strong>all reads</strong>, and <strong>zero absolute addresses</strong> inside blocks. So no block needs editing; only the tag table changes. That's what "impact assessed" means.</p>
</li>
<li><p><strong>The HMI is symbolic</strong> (<code>AddressAccessMode=Symbolic</code>), so the address change follows automatically — but address strings hard-coded in screens or comments are <strong>not visible to the compiler</strong> and need a human.</p>
</li>
<li><p><strong>Hardware mapping</strong>: <code>%I29.0</code> belongs to the ET200SP <code>DI 8x24VDC ST_2</code> (Input29); <code>I29.0–3</code> are <code>ZS-101..104</code>, so <code>%I29.4</code> is a free channel on the same module.</p>
</li>
<li><p><strong>The dangerous part</strong>: TIA <strong>does not validate that the address has a physical channel</strong>. Point it at a non-existent address and you still get a clean compile, while the field never sees the signal. Changing an address also means changing the <em>wiring ↔ address</em> mapping — moving a terminal is something only a person or a drawing can confirm.</p>
</li>
<li><p><code>dryRun</code> <strong>returns</strong> <code>{action: "overwrite", kind: "tagTable", tagCount: 27}</code> — you thought you changed one line; the whole 27-tag table gets rewritten. Without this preflight, you find out after other tags silently revert.</p>
</li>
</ul>
<h3>4.5 Virtual PLC: behavior finally becomes visible</h3>
<p>The fifth lesson runs the program: the library example "exhaust fan control" (one network, <code>TOF</code> of 1 minute) is downloaded into an <strong>S7-PLCSIM Advanced 6.0</strong> instance, driven by a scenario table, with <strong>measured</strong> values recorded.</p>
<p>The first whole-project compile is <strong>0 errors, 1 warning</strong>: <code>Inputs or outputs are used that do not exist in the configured hardware</code> — the 1516 has no onboard I/O, so <code>%I0.0</code> / <code>%Q0.0</code> don't exist. Insert a DI and a DQ module (starting at 0) and it's 0/0.</p>
<p>The order is fixed: <strong>register instance → power on → wait for Stop → download → RUN</strong>. A freshly registered instance is empty, which is why you wait for Stop before downloading. <code>/sim/status</code> returning 403 means exactly two things: TiaLink was started with <code>--no-sim</code>, or PLCSIM Advanced isn't installed.</p>
<p>Four scenarios surface something the compiler can't see and the diagram reads wrong:</p>
<p><strong>Press the stop button and the fan doesn't stop immediately.</strong> The <code>TOF</code> input is <code>Turn_On_PB AND NOT Turn_Off_PB</code> — for a <code>TOF</code>, releasing the start button and pressing stop are <strong>the same falling edge</strong>, so the fan keeps running for another minute either way. Reading the ladder, "stop means stop" is the natural (and wrong) conclusion.</p>
<p>Which changes the deliverable: not "I looked at it, it's fine," but <strong>scenario table + measured values + one conclusion</strong>.</p>
<h3>4.6 Cloud simulation: define the word "trigger"</h3>
<p>The sixth lesson takes a lighter path: <strong>no TIA, no PLCSIM, no Windows.</strong> The same SCL goes to a cloud simulation MCP endpoint, executed scan by scan, turned into PASS/FAIL by a scenario table.</p>
<p>Setup is three steps: sign in, create a <code>sim_live_</code> API key, add <code>https://tia.hubwiz.com/sim/mcp</code> to any Streamable HTTP MCP client. The endpoint exposes six tools (<code>lad_*</code> / <code>scl_*</code>); this lesson uses <code>scl_validate</code>, <code>scl_simulate</code>, <code>scl_run_scenario</code>.</p>
<p>Billing and fields worth knowing up front:</p>
<ul>
<li><p><code>initialize</code>, <code>tools/list</code>, <code>resources/read</code> are <strong>free</strong>; each accepted <code>tools/call</code> costs one credit. New accounts get 100; this lesson uses roughly 8–12.</p>
</li>
<li><p><code>tickMs</code> defaults to 10 ms, so <strong>100 ticks = 1 second</strong>.</p>
</li>
<li><p><code>scl_simulate</code> steps in <code>scans</code>; <code>scl_run_scenario</code> steps in <code>ticks</code>. That's the easiest mistake to make.</p>
</li>
</ul>
<p>The first acceptance run produces the course's signature failure (shown below; the source page prints the full report):</p>
<pre><code class="language-text">PASS  start latches, reset clears (3 step(s), 5 scan(s))
FAIL  sensor A pulse gives a 2 s diverter pulse: step 3 scan 100: DiverterA expected true, got false
FAIL  both sensors: A wins, only one output: step 2 scan 1: DiverterA expected true, got false
RESULT: FAIL (2 failure(s), 109 scan(s))
</code></pre>
<p>The same root cause as lesson 1: a <code>TON</code> requires <code>IN</code> to stay true for the full <code>PT</code>, and the sensor is high for exactly one scan. "Trigger, then output for 2 seconds" was implemented as "hold it for 2 seconds, then output" — <strong>two different requirements</strong>.</p>
<p>Switching to <code>TP</code> (fixed pulse width) passes the first three scenarios, and then a new boundary case fails: a <code>TP</code> pulse is <strong>not</strong> cut short by <code>IN := FALSE</code>. <code>DiverterA := PulseA.Q AND NOT Reset</code> merely <strong>masks</strong> the output while the pulse keeps running, so the diverter comes back on when reset is released. The final design is <strong>latch +</strong> <code>TON</code> <strong>to end the pulse</strong>: make "currently diverting" an explicit <code>ActiveA</code>/<code>ActiveB</code> state, let <code>Reset</code> clear the state, and interlock on the other side's <code>Active</code> rather than its output.</p>
<p>The ALL PASS run: <strong>8 scenarios + 16 sweep combinations + a 5,000-scan fuzz = 5,735 scan cycles</strong>. The report replays identically on local <code>scl-sim 0.1.0</code> and the hosted endpoint (2026-10-19): same SCL plus the same scenario file yields the same PASS/FAIL and scan counts.</p>
<h3>4.7 Refactor with regression: <code>TP</code> typed as <code>TON</code>, clean compile, zero motors</h3>
<p>The last lesson covers "change the structure, not the behavior." The target is a three-motor delayed-start sequence: one button opens a 20-second window, three motors start at +5 / +10 / +15 s, and everything stops after the window. The original writes <code>TON</code> + coil three times in one network, differing only in <code>PT</code> and output — classic copy-paste.</p>
<p>The refactor collapses it into <strong>one parameterized FB</strong> (<code>Enable</code> / <code>Pt</code> in, <code>Q</code> out) instantiated three times, with tag table, symbols and addresses untouched.</p>
<p>The author also crashed once, which is the point: <code>IEC_Timer_0_DB</code> was called as <code>TON</code> instead of <code>TP</code>. Compile: 0 errors, 0 warnings. Press the button: no motor moves. Switching back to <code>TP</code> and replaying the same scenario table brings behavior back point for point.</p>
<p>Which is why the two disciplines matter more than the code:</p>
<ul>
<li><p><strong>Store a behavior baseline first.</strong> Same scenario table, run before and after, compare cell by cell. Refactoring without a baseline means using the plant as a test environment.</p>
</li>
<li><p><strong>Trust only output bits and the scenario table.</strong> If a structured member (<code>DB.Ton.ET</code>) can't be read, change the evidence — don't fill the gap with "looks right."</p>
</li>
</ul>
<h2>5. Who does what: the agent acts, you judge</h2>
<p>The course deliberately minimizes the human's actions, because in industrial work the interesting question isn't "can the AI write code" but "which actions have to stay with a person."</p>
<table>
<thead>
<tr>
<th>Step</th>
<th>The agent does</th>
<th>You must do</th>
</tr>
</thead>
<tbody><tr>
<td>Write SCL</td>
<td>Read the project, add a CPU, generate the block, import, compile, self-fix from compiler output, clean up source files, save</td>
<td>State the requirement; approve the "report before change" step; spot-check the read-back table</td>
</tr>
<tr>
<td>Modify an example</td>
<td>Update the tag table, insert a contact, compile, read back structure</td>
<td>Confirm the boundary (only <code>Main</code> + tag table); confirm it's not a live project</td>
</tr>
<tr>
<td>Read a legacy project</td>
<td>Inventory devices/blocks/tags/HMI, export, summarize per block and network, flag unreferenced tags</td>
<td>Decide which findings escalate to safety items; review inferred vs unconfirmed</td>
</tr>
<tr>
<td>Change impact</td>
<td>Scan references, classify read/write and symbolic/absolute, map to hardware channels, <code>dryRun</code></td>
<td>Decide whether to change it; confirm terminals and drawings; confirm saving</td>
</tr>
<tr>
<td>Virtual PLC</td>
<td>Add device, insert modules, import, compile, register, power on, download, read/write, fill the table</td>
<td>Click the TIA authorization prompt; check that "measured" values really came from reads</td>
</tr>
<tr>
<td>Cloud simulation</td>
<td>Write scenario JSON, run assertions, locate the failing scan</td>
<td>Verify the scenario table covers the requirement — especially the definition of "trigger"</td>
</tr>
<tr>
<td>Refactor + regression</td>
<td>Save the baseline, write the parameterized FB, replace, rerun, diff</td>
<td>Decide which blocks must not be "refactored while you're in there"; confirm replacing the original</td>
</tr>
</tbody></table>
<p>Short version: <strong>actions can be delegated, judgment cannot.</strong> Your part boils down to three kinds of moves: state the requirement and the boundary, approve before a change is written and before it is saved, and verify the evidence instead of the claim. The checklists exist to turn "a human took a look" into "a human verified something."</p>
<h2>6. Four questions people ask</h2>
<p><strong>Do I need TIA Portal installed?</strong> Not for the whole course. One of the seven lessons (cloud PASS/FAIL) never touches TIA — browser, account, and any Streamable HTTP MCP client. The other six need Windows plus TIA Portal V19.</p>
<p><strong>Can TiaLink change my project on its own?</strong> It doesn't save silently. It exposes Openness operations as MCP tools, so writes are explicit: preflight before import, no overwrite of same-named objects by default, no auto-save afterwards. "Import ≠ save" appears more than once in the seven lessons.</p>
<p><strong>If cloud simulation passes, is the program fine?</strong> No. Cloud simulation runs SCL scan semantics only — no bus, HMI, drives, safety circuits, and it is not the TIA compiler. What it is very good at is exactly the "compiles clean, behavior broken" class of bug, like <code>TON</code> where the requirement said "trigger."</p>
<p><strong>Can AI-written blocks go straight into production?</strong> I wouldn't. The course's stance is consistent: AI assists, the compiler and read-back decide, every change gets human review, and safety functions, interlocks and e-stops are not delegated. What ships isn't "AI-written code" — it's code that passed the acceptance chain.</p>
<h2>7. Two ways to start</h2>
<p><strong>Path A — Windows + TIA Portal V19</strong></p>
<ol>
<li><p>Open a sandbox project, start TiaLink on the same machine; the page header auto-detects the connection and project name.</p>
</li>
<li><p>Add a Streamable HTTP MCP server at <code>http://localhost:8080/mcp</code> with a <code>Bearer</code> token; on another machine, replace <code>localhost</code> with the TIA PC's LAN address.</p>
</li>
<li><p>Start with "modify the first example" (20 minutes, smallest diff, fastest verification), then go back to "write SCL to 0/0" (30 minutes).</p>
</li>
<li><p>If the health check flags the allow-list, the first write triggers a TIA authorization dialog that only a human can confirm; running <code>TiaLink.Helper.exe</code> once avoids it.</p>
</li>
</ol>
<p><strong>Path B — no TIA available yet</strong></p>
<ol>
<li><p>Open the cloud simulator, sign in with a QR code, create an API key (shown once — don't commit it).</p>
</li>
<li><p>Set the MCP endpoint to <code>https://tia.hubwiz.com/sim/mcp</code>.</p>
</li>
<li><p>Do the "no TIA install" lesson with the same <code>FB_Sorter</code> that compiled clean in path A — and watch it fail twice before it passes.</p>
</li>
</ol>
<h2>8. What the course deliberately does not provide</h2>
<ul>
<li><p><strong>No downloads.</strong> By design: no files land on disk; import goes through TiaLink with a preflight, and nothing is auto-saved.</p>
</li>
<li><p><strong>Cloud simulation only accepts examples or desensitized code.</strong> Don't paste a customer project; lessons that touch TIA run entirely on your machine, and project data stays in-house.</p>
</li>
<li><p><strong>Passing simulation is not commissioning.</strong> PLCSIM only simulates CPU behavior; bus, HMI, drives and safety circuits are out of scope. Cloud simulation is narrower still.</p>
</li>
<li><p><strong>Every change needs human review.</strong> Safety functions, interlocks, e-stops are not delegated; AI output is "unverified" by default.</p>
</li>
<li><p><strong>Version-specific.</strong> Everything was measured on TiaLink 1.1.0.0 + TIA Portal V19 + S7-PLCSIM Advanced 6.0. Different versions change error text and tool names — which is exactly why the course insists on structured results instead of matching error strings.</p>
</li>
</ul>
<h2>9. Who it's for, and who it isn't</h2>
<p><strong>For</strong>: engineers who know TIA Portal but don't trust AI-written blocks in a real project; people inheriting a legacy program who want a specification before touching it; anyone training juniors who needs a checkable practice path; technical leads deciding how far AI can go.</p>
<p><strong>Not for</strong>: people who have never opened TIA Portal (start with the <a href="https://tia.hubwiz.com/learn-lad/?utm_source=hashnode&amp;utm_medium=syndication&amp;utm_campaign=ai-plc-programming-7-lessons-verification">60-example ladder library</a>); anyone looking for "one-click ladder generation, download the project"; anyone who wants to skip human review and safety validation.</p>
<h2>10. Takeaway</h2>
<p>Read together, the seven lessons aren't about how much the AI can write. They describe <strong>an auditable delivery chain</strong>:</p>
<p>write → compile → read back → assess impact → simulate → PASS scenarios → regression shows behavior unchanged.</p>
<p>Every step has a criterion that doesn't depend on the AI claiming success. That's the part worth copying, even if you never use TiaLink or the cloud simulator: put evidence at every step, and AI actually gets to enter an engineering workflow.</p>
<p>Start with the 20-minute <a href="https://tia.hubwiz.com/learn-lad/ai/modify-example/?utm_source=hashnode&amp;utm_medium=syndication&amp;utm_campaign=ai-plc-programming-7-lessons-verification">modify-an-example lesson</a>, or the 35-minute <a href="https://tia.hubwiz.com/learn-lad/ai/cloud-sim-scenarios/?utm_source=hashnode&amp;utm_medium=syndication&amp;utm_campaign=ai-plc-programming-7-lessons-verification">cloud PASS/FAIL lesson</a>.</p>
<hr />
<h2>Verification notes</h2>
<ul>
<li><p>Course structure, lesson steps and all measured numbers (2 errors / 2 warnings, 36 operands, 51 tags with 6 unreferenced, <code>dryRun</code> rewriting 27 tags, 5,735 scan cycles, <code>1513F-1 PN</code>, <code>1516-3 PN/DP</code>) come from the official course pages at <a href="https://tia.hubwiz.com/learn-lad/ai/?utm_source=hashnode&amp;utm_medium=syndication&amp;utm_campaign=ai-plc-programming-7-lessons-verification">tia.hubwiz.com/learn-lad/ai/</a> and its seven sub-pages, checked on 2026-10-20.</p>
</li>
<li><p>The stated test environment is TiaLink 1.1.0.0 + TIA Portal V19 + S7-PLCSIM Advanced 6.0; the cloud simulation report replays identically on <code>scl-sim 0.1.0</code> and the hosted endpoint (2026-10-19).</p>
</li>
<li><p>This article does not independently reproduce the measurements; numbers and conclusions follow the official pages and may change with tool versions.</p>
</li>
</ul>
<h2>Further reading (all in Chinese)</h2>
<ul>
<li><p><a href="https://www.hubwiz.com/blog/ai-industrial-stack/">AI × Industrial / PLC guide</a></p>
</li>
<li><p><a href="https://www.hubwiz.com/blog/siemens-plc-ladder-example-library/">Siemens PLC ladder logic example library: 60 importable exercises</a></p>
</li>
<li><p><a href="https://www.hubwiz.com/blog/taking-over-legacy-plc-program-4-steps/">Taking over someone else's PLC program: 4 evidence steps before the first edit</a></p>
</li>
<li><p><a href="https://www.hubwiz.com/blog/lad-to-scl-migration-guide-tia-portal/">Ladder to SCL migration guide: what converts, what must be refactored, how to verify</a></p>
</li>
<li><p><a href="https://www.hubwiz.com/blog/virtual-plc-lab-factory-io-openplc-modbus-tcp/">Practicing PLC without hardware: Factory I/O + OpenPLC virtual lab</a></p>
</li>
</ul>
<p>This article was originally published in Chinese on <a href="https://www.hubwiz.com/blog/ai-plc-programming-7-lessons-verification/">hubwiz.com</a>.</p>
]]></content:encoded></item><item><title><![CDATA[An AI Agent Programmed a Siemens S7-1500 Through an MCP Bridge]]></title><description><![CDATA[Here's the SCL and the race conditions it caught — and the three things it refused to do]]></description><link>https://tiaforge.hashnode.dev/an-ai-agent-programmed-a-siemens-s7-1500-through-an-mcp-bridge</link><guid isPermaLink="true">https://tiaforge.hashnode.dev/an-ai-agent-programmed-a-siemens-s7-1500-through-an-mcp-bridge</guid><dc:creator><![CDATA[tia-forge]]></dc:creator><pubDate>Mon, 12 Oct 2026 15:35:07 GMT</pubDate><content:encoded><![CDATA[<p>We built an MCP server that drives <strong>Siemens TIA Portal V19 through the Openness API</strong>, pointed an LLM agent at it, and gave it a real engineering task: implement a conveyor-sorting control program for an <strong>S7-1500 (CPU 1511-1 PN)</strong>, verified in <strong>S7-PLCSIM</strong>.</p>
<p>Not "generate a snippet." The whole loop:</p>
<p><strong>add CPU → compile → start simulator → create FB/UDTs → write SCL → compile → download → verify.</strong></p>
<p>I expected the code to be the interesting part. It wasn't. The interesting part is <em>which</em> engineering decisions the agent made, and where it drew a line and refused to cross it.</p>
<h2>The toolchain</h2>
<ul>
<li><p><strong>Agent</strong>: an LLM agent (WorkBuddy) that reads the spec, plans steps, writes code and tests.</p>
</li>
<li><p><strong>Bridge</strong>: an MCP server (TiaLink) exposing TIA Portal operations over MCP — add device, create block, write SCL, compile, download, read back.</p>
</li>
<li><p><strong>Target</strong>: TIA Portal V19 with Openness, S7-PLCSIM for verification.</p>
</li>
</ul>
<p>The bridge runs on the same machine as TIA Portal (an industrial PC); the agent can run elsewhere as long as it can reach the bridge.</p>
<h2>The prompt</h2>
<p>Hardware and network configuration were already done. The task:</p>
<ol>
<li><p>SCL <strong>state machine</strong>, with state numbers identical to the spec (so HMI, alarms, tests and field debugging all reference the same numbering)</p>
</li>
<li><p>Logic as a <strong>reusable FB with its own instance DB</strong> (line #2 becomes a second instance DB, not copy-paste)</p>
</li>
<li><p>Clear separation of <code>i_</code> inputs / <code>q_</code> outputs / <code>stat_</code> internals</p>
</li>
<li><p>Use <strong>IEC timers</strong> (<code>TON_TIME</code>) — no hand-rolled scan-cycle accumulation</p>
</li>
<li><p>Implement: entry de-dup, quality-result latch, dual-cylinder interlock, fault latch, manual reset, normal stop</p>
</li>
<li><p><strong>Every output must have a deterministic value every scan</strong> — no reliance on last-cycle residue</p>
</li>
<li><p>Emergency stop is only read as a <strong>safety-circuit status input</strong>; ordinary PLC code is not a safety function</p>
</li>
</ol>
<p>Deliverables: FB SCL source, UDTs, state-transition doc, ≥10 automated test cases, boundary/race analysis.</p>
<p>And one line at the end:</p>
<blockquote>
<p>"First add a CPU and check whether the simulator works."</p>
</blockquote>
<p>That line is the part I actually cared about testing. Config tasks are judged from a screenshot. <strong>Programs are judged by behavior.</strong> So: does the agent build the verification environment first, or just start writing code?</p>
<h2>The sequence it chose</h2>
<ol>
<li><p>Check whether a CPU already existed — only then add <code>PLC_1</code> (CPU 1511-1 PN)</p>
</li>
<li><p>Compile, start <strong>S7-PLCSIM</strong>, confirm download/run works</p>
</li>
<li><p>Create <code>FB_ConveyorSort</code> and write the state machine in SCL</p>
</li>
<li><p>Create the needed UDTs</p>
</li>
<li><p>Call the FB in <code>OB1</code> with instance DB <code>DB_ConveyorSort_1</code></p>
</li>
<li><p>Compile + download — no errors</p>
</li>
<li><p>Deliver the state transition table, <strong>14 test cases</strong>, and a race/boundary analysis</p>
</li>
</ol>
<p>It built the verification environment first. Good sign.</p>
<h2>The state machine</h2>
<table>
<thead>
<tr>
<th>State</th>
<th>Name</th>
<th>Enter when</th>
<th>Main action</th>
<th>Exit → target</th>
</tr>
</thead>
<tbody><tr>
<td>0</td>
<td>INIT</td>
<td>first scan</td>
<td>clear latches, fault code, commands</td>
<td>unconditional → 10</td>
</tr>
<tr>
<td>10</td>
<td>READY</td>
<td>power-on / reset / after stop</td>
<td>conveyor stopped, wait for start</td>
<td>start → 20</td>
</tr>
<tr>
<td>20</td>
<td>FEED</td>
<td>after start</td>
<td>conveyor runs; entry de-dup; latch quality; sort timeout</td>
<td>at sort pos → 30; timeout → 90; stop → 100</td>
</tr>
<tr>
<td>30</td>
<td>CHECK</td>
<td>at sort position</td>
<td>stop conveyor, branch on latched result</td>
<td>OK → 50; NG → 60</td>
</tr>
<tr>
<td>50</td>
<td>PUSH_A</td>
<td>judged OK</td>
<td>cylinder A extend, push hold</td>
<td>extended + hold done → 70</td>
</tr>
<tr>
<td>60</td>
<td>PUSH_B</td>
<td>judged NG</td>
<td>cylinder B extend, push hold</td>
<td>extended + hold done → 70</td>
</tr>
<tr>
<td>70</td>
<td>RETRACT</td>
<td>push done</td>
<td>cylinder retract, retract timeout</td>
<td>retracted → 80; timeout → 90</td>
</tr>
<tr>
<td>80</td>
<td>DISCHARGE</td>
<td>retracted</td>
<td>wait for part to leave sort position</td>
<td>sort photo-eye clear → 20</td>
</tr>
<tr>
<td>90</td>
<td>FAULT</td>
<td>any fault / safety circuit open</td>
<td>all outputs off, latch fault</td>
<td>reset + conditions OK → 10</td>
</tr>
<tr>
<td>100</td>
<td>STOPPED</td>
<td>normal stop</td>
<td>outputs off, restartable</td>
<td>start → 20</td>
</tr>
</tbody></table>
<h2>The SCL</h2>
<p>Here's the full function block it delivered. Two things to look at as you read:</p>
<ul>
<li><p><strong>Section 0</strong> writes every output to a deterministic value at the top of the cycle (requirement #6).</p>
</li>
<li><p><strong>Section 7</strong> is the final output mapping with the interlock and the same determinism guarantee.</p>
</li>
</ul>
<pre><code class="language-pascal">FUNCTION_BLOCK "FB_ConveyorSort"
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1

VAR_INPUT
   i_xSafetyOK     : Bool;             // safety circuit healthy (1 = closed, run allowed)
   i_xStart        : Bool;             // start (edge-triggered)
   i_xStop         : Bool;             // normal stop (edge-triggered)
   i_xReset        : Bool;             // fault reset (edge-triggered)
   i_xPartAtEntry  : Bool;             // entry photo-eye
   i_xPartAtSort   : Bool;             // sort-position photo-eye
   i_xQualityOK    : Bool;             // quality result: 1 = OK
   i_xCylA_ExtEnd  : Bool;             // cylinder A extended limit
   i_xCylA_RetEnd  : Bool;             // cylinder A retracted limit
   i_xCylB_ExtEnd  : Bool;             // cylinder B extended limit
   i_xCylB_RetEnd  : Bool;             // cylinder B retracted limit
   i_tSortTimeout  : Time := T#5S;     // sort timeout
   i_tPushHold     : Time := T#800MS;  // push hold time
   i_tRetractTO    : Time := T#3S;     // retract timeout
END_VAR

VAR_OUTPUT
   q_xConveyor     : Bool;
   q_xCylA_Extend  : Bool;
   q_xCylA_Retract : Bool;
   q_xCylB_Extend  : Bool;
   q_xCylB_Retract : Bool;
   q_xFault        : Bool;
   q_xReady        : Bool;
   q_wState        : Word;
END_VAR

VAR
   stat_wState     : Word;             // current state
   stat_wFaultCode : Word;             // fault code
   stat_xFault     : Bool;             // latched fault
   stat_xCmdAExt   : Bool;             // cylinder A extend request
   stat_xCmdBExt   : Bool;             // cylinder B extend request
   stat_xQualityNG : Bool;             // latched quality result (1 = reject)
   stat_xPartSeen  : Bool;             // entry de-dup latch
   stat_xInTransit : Bool;             // part in transit
   stat_xStartEdge : Bool;
   stat_xStopEdge  : Bool;
   stat_xResetEdge : Bool;
   stat_xStartOld  : Bool;
   stat_xStopOld   : Bool;
   stat_xResetOld  : Bool;
   stat_tSortTO    : TON_TIME;         // sort timeout
   stat_tPush      : TON_TIME;         // push hold
   stat_tRetract   : TON_TIME;         // retract timeout
END_VAR

BEGIN
   // ---------- 0. write every output to a deterministic value each cycle ----------
   q_xConveyor     := FALSE;
   q_xCylA_Extend  := FALSE;
   q_xCylA_Retract := FALSE;
   q_xCylB_Extend  := FALSE;
   q_xCylB_Retract := FALSE;
   q_xFault        := FALSE;
   q_xReady        := FALSE;
   q_wState        := stat_wState;

   // ---------- 1. button edge extraction ----------
   stat_xStartEdge := i_xStart AND NOT stat_xStartOld;
   stat_xStopEdge  := i_xStop  AND NOT stat_xStopOld;
   stat_xResetEdge := i_xReset AND NOT stat_xResetOld;
   stat_xStartOld  := i_xStart;
   stat_xStopOld   := i_xStop;
   stat_xResetOld  := i_xReset;

   // ---------- 2. safety circuit: read-only, do NOT implement a safety function ----------
   IF NOT i_xSafetyOK THEN
      stat_wFaultCode := 16#0001;      // safety circuit open
      stat_xFault     := TRUE;
   END_IF;

   // ---------- 3. sensor plausibility checks ----------
   IF i_xCylA_ExtEnd AND i_xCylA_RetEnd THEN
      stat_wFaultCode := 16#0002;      // cylinder A contradictory position sensors
      stat_xFault     := TRUE;
   END_IF;
   IF i_xCylB_ExtEnd AND i_xCylB_RetEnd THEN
      stat_wFaultCode := 16#0003;      // cylinder B contradictory position sensors
      stat_xFault     := TRUE;
   END_IF;

   // ---------- 4. dual-cylinder interlock check (software supplement, NOT a hard interlock) ----------
   IF stat_xCmdAExt AND stat_xCmdBExt THEN
      stat_wFaultCode := 16#0010;      // both cylinders requested to extend
      stat_xFault     := TRUE;
   END_IF;

   // ---------- 5. fault reset (requires human confirmation AND recovered conditions) ----------
   IF stat_xFault THEN
      IF stat_xResetEdge
         AND i_xSafetyOK
         AND NOT (i_xCylA_ExtEnd AND i_xCylA_RetEnd)
         AND NOT (i_xCylB_ExtEnd AND i_xCylB_RetEnd) THEN
         stat_xFault     := FALSE;
         stat_wFaultCode := 16#0000;
         stat_wState     := 10;
      END_IF;
   END_IF;

   // ---------- 6. state machine ----------
   IF stat_xFault THEN
      stat_wState := 90;               // fault has priority, force fault state
   END_IF;

   CASE stat_wState OF
      0:  // INIT on power-up
         stat_xCmdAExt   := FALSE;
         stat_xCmdBExt   := FALSE;
         stat_xQualityNG := FALSE;
         stat_xPartSeen  := FALSE;
         stat_xInTransit := FALSE;
         stat_wFaultCode := 16#0000;
         stat_wState     := 10;

      10: // READY waiting for start
         q_xReady := TRUE;
         stat_xCmdAExt := FALSE;
         stat_xCmdBExt := FALSE;
         IF stat_xStartEdge THEN
            stat_wState := 20;
         END_IF;

      20: // FEED conveying
         q_xConveyor := TRUE;
         q_xReady    := TRUE;
         stat_xCmdAExt := FALSE;
         stat_xCmdBExt := FALSE;
         IF stat_xStopEdge THEN
            stat_wState := 100;
         ELSE
            // entry de-dup: latch only on "new part entered"
            IF i_xPartAtEntry AND NOT stat_xPartSeen THEN
               stat_xPartSeen  := TRUE;
               stat_xInTransit := TRUE;
               stat_xQualityNG := NOT i_xQualityOK;   // latch quality result
            END_IF;
            IF NOT i_xPartAtEntry THEN
               stat_xPartSeen := FALSE;               // part left entry, allow the next one
            END_IF;

            // sort timeout
            stat_tSortTO(IN := stat_xInTransit, PT := i_tSortTimeout);
            IF stat_tSortTO.Q THEN
               stat_wFaultCode := 16#0020;            // sort timeout
               stat_xFault     := TRUE;
               stat_wState     := 90;
            ELSIF i_xPartAtSort THEN
               stat_xInTransit := FALSE;
               stat_tSortTO(IN := FALSE, PT := i_tSortTimeout);
               stat_wState     := 30;
            END_IF;
         END_IF;

      30: // CHECK decide on latched result
         stat_xCmdAExt := FALSE;
         stat_xCmdBExt := FALSE;
         stat_tPush(IN := FALSE, PT := i_tPushHold);
         IF stat_xQualityNG THEN
            stat_wState := 60;                        // reject -&gt; cylinder B
         ELSE
            stat_wState := 50;                        // OK     -&gt; cylinder A
         END_IF;

      50: // PUSH_A extend A for a good part
         stat_xCmdBExt := FALSE;                      // interlock: B stays retracted
         stat_xCmdAExt := i_xCylB_RetEnd;             // only allow A after B is confirmed retracted
         stat_tPush(IN := stat_xCmdAExt, PT := i_tPushHold);
         IF i_xCylA_ExtEnd AND stat_tPush.Q THEN
            stat_xCmdAExt := FALSE;                   // push done, retract
            stat_wState   := 70;
         END_IF;

      60: // PUSH_B extend B for a reject
         stat_xCmdAExt := FALSE;                      // interlock: A stays retracted
         stat_xCmdBExt := i_xCylA_RetEnd;             // only allow B after A is confirmed retracted
         stat_tPush(IN := stat_xCmdBExt, PT := i_tPushHold);
         IF i_xCylB_ExtEnd AND stat_tPush.Q THEN
            stat_xCmdBExt := FALSE;
            stat_wState   := 70;
         END_IF;

      70: // RETRACT confirm retraction
         stat_xCmdAExt := FALSE;
         stat_xCmdBExt := FALSE;
         stat_tRetract(IN := TRUE, PT := i_tRetractTO);
         IF (stat_xQualityNG AND i_xCylB_RetEnd)
            OR ((NOT stat_xQualityNG) AND i_xCylA_RetEnd) THEN
            stat_tRetract(IN := FALSE, PT := i_tRetractTO);
            stat_wState := 80;
         END_IF;
         IF stat_tRetract.Q THEN
            stat_wFaultCode := 16#0030;               // retract timeout
            stat_xFault     := TRUE;
            stat_wState     := 90;
         END_IF;

      80: // DISCHARGE wait for the part to leave the sort position
         stat_xCmdAExt := FALSE;
         stat_xCmdBExt := FALSE;
         IF NOT i_xPartAtSort THEN
            stat_wState := 20;
         END_IF;

      90: // FAULT latched
         stat_xCmdAExt := FALSE;
         stat_xCmdBExt := FALSE;
         stat_tSortTO(IN := FALSE, PT := i_tSortTimeout);
         stat_tPush(IN := FALSE, PT := i_tPushHold);
         stat_tRetract(IN := FALSE, PT := i_tRetractTO);

      100: // STOPPED normal stop
         q_xReady    := TRUE;
         stat_xCmdAExt := FALSE;
         stat_xCmdBExt := FALSE;
         stat_xInTransit := FALSE;
         stat_tSortTO(IN := FALSE, PT := i_tSortTimeout);
         IF stat_xStartEdge THEN
            stat_wState := 20;
         END_IF;

      ELSE
         stat_wFaultCode := 16#00FF;                   // illegal state
         stat_xFault     := TRUE;
         stat_wState     := 90;
   END_CASE;

   // ---------- 7. output mapping: interlock + deterministic value each cycle ----------
   q_xCylA_Extend  := stat_xCmdAExt AND NOT stat_xCmdBExt AND i_xCylB_RetEnd;
   q_xCylB_Extend  := stat_xCmdBExt AND NOT stat_xCmdAExt AND i_xCylA_RetEnd;
   q_xCylA_Retract := NOT q_xCylA_Extend;
   q_xCylB_Retract := NOT q_xCylB_Extend;

   q_xFault := stat_xFault;
   q_wState := stat_wState;
END_FUNCTION_BLOCK
</code></pre>
<blockquote>
<p>Note: this assumes double-acting cylinders where retract is the de-energized safe direction. For spring-return cylinders, the "retract output" should be forced to 0 rather than commanded.</p>
</blockquote>
<h2>The UDTs</h2>
<p>A parameter UDT so multiple lines can reuse the same FB:</p>
<pre><code class="language-pascal">TYPE "UDT_SortParams"
VERSION : 0.1
   STRUCT
      SortTimeout : Time := T#5S;      // sort timeout
      PushHold    : Time := T#800MS;   // push hold time
      RetractTO   : Time := T#3S;      // retract timeout
   END_STRUCT;
END_TYPE
</code></pre>
<p>A status UDT for HMI display and online diagnostics:</p>
<pre><code class="language-pascal">TYPE "UDT_SortStatus"
VERSION : 0.1
   STRUCT
      State        : Word;             // current state
      FaultCode    : Word;             // fault code
      FaultLatched : Bool;             // fault latched
      QualityNG    : Bool;             // quality result latched
      PartSeen     : Bool;             // entry de-dup latch
      InTransit    : Bool;             // part in transit
   END_STRUCT;
END_TYPE
</code></pre>
<h2>The 14 test cases it shipped</h2>
<table>
<thead>
<tr>
<th>#</th>
<th>Name</th>
<th>Precondition</th>
<th>Action</th>
<th>Expected</th>
</tr>
</thead>
<tbody><tr>
<td>TC-01</td>
<td>Power-on init</td>
<td>downloaded to sim</td>
<td>first scan</td>
<td>state 10, all outputs 0, no fault</td>
</tr>
<tr>
<td>TC-02</td>
<td>Normal start</td>
<td>state 10</td>
<td>press start</td>
<td>state 20, conveyor runs</td>
</tr>
<tr>
<td>TC-03</td>
<td>Good part sort</td>
<td>state 20</td>
<td>part at entry, quality OK, reaches sort pos</td>
<td>20→30→50→70→80→20, cylinder A extends then retracts</td>
</tr>
<tr>
<td>TC-04</td>
<td>Bad part sort</td>
<td>state 20</td>
<td>part at entry, quality NG</td>
<td>20→30→60→70→80→20, cylinder B extends then retracts</td>
</tr>
<tr>
<td>TC-05</td>
<td>Entry de-dup</td>
<td>state 20</td>
<td>part sits at entry for 100 scans</td>
<td><code>PartSeen</code> latches once, no repeat triggering</td>
</tr>
<tr>
<td>TC-06</td>
<td>Sort timeout</td>
<td>state 20</td>
<td>part doesn't reach sort pos within 5 s</td>
<td>state 90, fault 16#0020 latched</td>
</tr>
<tr>
<td>TC-07</td>
<td>Fault latch + reset</td>
<td>state 90, conditions recovered</td>
<td>press start, then reset</td>
<td>start ignored; reset + conditions OK → state 10</td>
</tr>
<tr>
<td>TC-08</td>
<td>Reset refused</td>
<td>state 90, safety circuit still open</td>
<td>press reset</td>
<td>reset ineffective, stays in state 90</td>
</tr>
<tr>
<td>TC-09</td>
<td>Normal stop + resume</td>
<td>state 20</td>
<td>press stop, then start</td>
<td>stop → state 100 outputs safe; start → state 20</td>
</tr>
<tr>
<td>TC-10</td>
<td>Dual-cylinder interlock</td>
<td>state 20</td>
<td>force both extend requests = 1</td>
<td>fault 16#0010, neither cylinder extends</td>
</tr>
<tr>
<td>TC-11</td>
<td>Contradictory position sensors</td>
<td>any</td>
<td>force A ext-end and ret-end = 1 together</td>
<td>fault 16#0002 latched, state 90</td>
</tr>
<tr>
<td>TC-12</td>
<td>Safety circuit open</td>
<td>state 20</td>
<td><code>i_xSafetyOK = 0</code></td>
<td>outputs drop immediately, state 90 latched; needs reset after recovery</td>
</tr>
<tr>
<td>TC-13</td>
<td>Retract timeout</td>
<td>state 70</td>
<td>cylinder doesn't retract within 3 s</td>
<td>fault 16#0030, state 90</td>
</tr>
<tr>
<td>TC-14</td>
<td>Output determinism</td>
<td>any</td>
<td>single-step and watch outputs</td>
<td>every scan all outputs have defined values, no residue</td>
</tr>
</tbody></table>
<h2>The race conditions it flagged (the actually useful part)</h2>
<ol>
<li><p><strong>Entry de-dup vs. sensor sticking</strong> — latching until the part leaves the entry photo-eye de-dupes naturally, but <em>only if the part actually leaves</em>; otherwise later parts get missed. The process must guarantee single-piece spacing.</p>
</li>
<li><p><strong>When to latch the quality result</strong> — it latches at the <strong>entry</strong>, not at the sort position, because the quality system's result may not be stable by the time the part arrives, and you'd race the position signal. If quality detection is slow, use a handshake/queue instead of just delaying the read.</p>
</li>
<li><p><strong>Arrival vs. timeout in the same scan</strong> — IEC <code>TON</code> sets <code>Q = TRUE</code> on the scan it reaches <code>PT</code>. It checks timeout <em>before</em> arrival and explicitly resets the timer on arrival. If you want "arrival wins," you must reorder.</p>
</li>
<li><p><strong>Re-trigger after push</strong> — the part can still briefly cover the sort photo-eye. Going 70→20 directly would cause a double-sort. State 80 (DISCHARGE) waits for the eye to clear before re-arming.</p>
</li>
<li><p><strong>Interlock layering</strong> — software only stops "logically simultaneous requests." It does <strong>not</strong> replace pneumatic/electrical hard interlocks or mechanical limits.</p>
</li>
<li><p><strong>Button priority in the same scan</strong> — fault/safety &gt; stop &gt; reset &gt; start, and this must be frozen in the requirements or implementations will diverge.</p>
</li>
<li><p><strong>"Stop" has two meanings</strong> — immediate vs. finish-current-part. It implements immediate (state 100); a controlled finish needs a <code>StopPending</code> flag.</p>
</li>
<li><p><code>PT = 0</code> <strong>boundary</strong> — <code>TON</code> fires immediately, which can turn a timeout into a guaranteed fault. Validate the lower bound if the time is HMI-writable.</p>
</li>
<li><p><strong>Held reset button</strong> — edge-triggered, so a long press doesn't keep masking a recurring fault.</p>
</li>
<li><p><strong>First-scan init</strong> — state 0 does init; a proper project would also use <code>OB100</code>. Kept state 0 so init stays visible and testable inside the state machine.</p>
</li>
<li><p><strong>Multiple instances</strong> — timers and edge memories live in FB statics, so <strong>never share one instance DB across lines</strong>.</p>
</li>
<li><p><strong>Sim ≠ field</strong> — the simulator doesn't model photo-eye chatter, pneumatic delay, or safety-circuit response time. All times must be tuned on the real machine.</p>
</li>
</ol>
<h2>The three things it explicitly refused to do</h2>
<p>This is the part I think matters most, and why I wrote this up at all:</p>
<ul>
<li><p><strong>Emergency stop</strong> must be realized by a hardware safety circuit (safety relay / safety PLC). The program only reads <code>i_xSafetyOK</code>.</p>
</li>
<li><p><strong>Dual-cylinder hard interlock</strong> must be pneumatic or electrical. The software interlock is <em>supplementary</em>.</p>
</li>
<li><p><strong>Time parameters and sensor tuning</strong> must be done on the real machine. The simulator can't give you real chatter or pneumatic delay.</p>
</li>
</ul>
<p>It did the software part and handed the responsibility it can't carry back to a human. It didn't pretend to have "finished" something it hadn't.</p>
<p>That's the property I'd want from any code generator touching industrial control: generate the verifiable part, and be explicit about what it can't verify.</p>
<h2>Honest caveats</h2>
<ul>
<li><p>The simulator passing does <strong>not</strong> mean it's field-ready.</p>
</li>
<li><p>I only checked that the code compiles, downloads, and matches the test cases at a logical level — I did not bench-test on real hardware.</p>
</li>
<li><p>The agent's self-reported step log is in the full write-up; treat model self-reports with the usual skepticism.</p>
</li>
</ul>
<hr />
<p><em>Full write-up with screenshots, the complete source, and the agent's step log:</em> <a href="https://www.hubwiz.com/blog/ai-powered-plc-programming-with-workbuddy-and-tialink/?utm_source=hashnode&amp;utm_medium=syndication"><em>AI-powered PLC programming with WorkBuddy + TiaLink</em></a><em>.</em></p>
<p><em>Disclosure: I work on TiaLink, the MCP bridge that drives TIA Portal, built by our team at 汇智网 (</em><a href="http://hubwiz.com"><em>hubwiz.com</em></a><em>). The agent used in this experiment was Tencent's WorkBuddy.</em></p>
]]></content:encoded></item></channel></rss>