Yet another PLC IDE
So I don't really know where to start with this. I decided to give vibe coding a go for fun, but I didn't want to spend too much money on it. The ChatGPT Plus subscription was free for a month and includes something called Codex, a kind of IDE with a chat interface that you can give access to read and write files in your project folder, search the web, read documentation, run terminal commands, etc.
I started with two small projects. The first was to rewrite something I saw while browsing the web in p5.js: https://alien-sunset.neocities.org/bots/aliens
I asked Codex to rewrite it in p5.js and make a small Space Invaders game with the sprite, and it gave me this: https://editor.p5js.org/speccy88/full/hUoQ8AmcZ
The second attempt was to rewrite a game I like to play on Linux, Kobo Deluxe. I just pointed it to the C++ source code here: https://github.com/olofson/kobodeluxe
and asked if it could make it in JavaScript. It gave me this: https://speccy88.github.io/kobo-js/
So this morning, I was thinking about my next little test project and remembered that I'd always wanted to make a little PLC IDE that worked a bit like the Studio 5000 software I use at work, which connects to Allen-Bradley PLCs. I told Codex to take a look here: https://cq.cx/ladder.pl to get started with a GUI.
I had a Raspberry Pi Pico 2W not too far away, so I told it to make a runtime that runs on the Pico and would allow my PLC IDE to do live viewing of the ladder program running inside the Pico (created with the ladder editor in the GUI). It just worked. So I started asking for timers, counters, and integer instructions (compare, math, etc.).
I got my old P2 Engineering Sample board out and asked Codex to make a runtime for it. I told it the board was connected to a serial port on my Mac and the escape sequence to enter Taqoz to play with the propeller. After about 30 minutes of work, Codex replied that it was ready to test.
I put my source code here: https://github.com/speccy88/RSmicro
I feel both really lazy and really amazed at the same time. In 3–4 hours, I did what could have taken me months to do. Codex just spat out thousands of lines of code without complaining about what I asked it to do.
I know not everyone is keen to use these tools, but I think for a lot of the challenges we have as hobbyists trying to maintain and port a lot of code, these agentic tools are invaluable.
Just to finish, I asked Codex to explain in detail how it did the Propeller 2 runtime. Take a look at what I got:
Propeller 2 TAQOZ Runtime
This document explains how the Propeller 2 runtime works in this repository, how it is generated, how it talks to the host, what is stored on the board, and why the current design looks the way it does.
The implementation described here is the current code in:
- src/plc_runtime/propeller2/runtime.py
- src/plc_runtime/propeller2/runtime.fth
- src/plc_runtime/propeller2/transport.py
Overview
The Propeller 2 backend compiles a ladder program into TAQOZ Forth source, sends that source over a serial connection to the board, and then operates the runtime through TAQOZ commands.
At a high level:
- The host opens the serial port and enters the TAQOZ prompt.
- Python generates a complete TAQOZ runtime specialized for the current ladder program.
- The generated source is loaded into RAM only.
- The runtime initializes its variables, starts a background cog, and waits in
stopmode. - The host uses normal TAQOZ command/response interactions to:
- say hello
- download a new runtime
- upload the current stored ladder program
- take snapshots
- switch between
runandstop - edit tags, timers, counters, and forces
This design intentionally does not depend on BACKUP or RESTORE, and it does not currently depend on the custom PLC.HOST foreground protocol for host communication.
Design Goals
The current runtime design is shaped by a few practical goals:
- Keep execution on the Propeller 2 side simple and deterministic.
- Let TAQOZ act as a reliable bootstrap and command shell.
- Run the ladder scan in a dedicated background cog.
- Keep host interaction fast enough for an IDE watch window and online edits.
- Avoid fragile persistence tricks and RAM aliasing problems.
- Preserve a readable mapping from ladder constructs to generated TAQOZ words.
What The Host Loads
The generated runtime is built from the template in runtime.fth. That template has placeholders such as:
@@DATA_WORDS@@@@CORE_WORDS@@@@TIMER_WORDS@@@@COUNTER_WORDS@@@@RUNG_WORDS@@@@SET_WORDS@@@@HOST_WORDS@@@@SNAPSHOT_LINES@@@@UPLOAD_LINES@@@@INIT_LINES@@@@RUNTIME_WORDS@@
Propeller2Runtime.build_runtime_source() fills those placeholders with a program-specific TAQOZ runtime.
The final generated source contains:
- storage words for runtime state
- helper words such as
PLC.BOOL - one generated word per timer and counter helper
- one generated word per ladder rung
- input and output handling
- force application logic
- online-edit helper words
- snapshot and upload words
- initialization words
- background runtime words for the scan task
The template ends by executing PLC.START.RUNTIME, so loading the source also initializes and starts the runtime automatically.
RAM-Only Runtime
The runtime is RAM-only.
That means:
- every download rebuilds and reloads the runtime into TAQOZ RAM
- power-cycling the board loses the PLC runtime
- there is no flash persistence step in the current implementation
This is deliberate. Earlier approaches that leaned on persistence and more invasive takeover behavior were much less reliable during development and debugging.
Serial Attach And TAQOZ Entry
The serial attach logic lives in runtime.py.
Important constants:
DEFAULT_BAUDRATE = 921600DEFAULT_SCAN_MS = 1FALLBACK_BAUDRATES = (921600, 115200)
open_taqoz_console()
open_taqoz_console() tries to enter the TAQOZ prompt at one or more baud rates.
The attach strategy is:
- try the requested baud first
- then try known fallback baud rates
- reset the port if needed
- confirm the
TAQOZ#prompt before declaring success
TaqozConsole
TaqozConsole is the small serial helper around a pyserial port. It handles:
- entering TAQOZ with reset or a plain carriage return
- reading until the TAQOZ prompt comes back
- sending single commands
- sending source code line by line
The source loader does one important thing: it groups Forth colon definitions into single commands before sending them. That keeps multi-line definitions intact when transmitted to TAQOZ.
Program Analysis And Compile Context
Before generating TAQOZ code, the runtime builds a CompileContext.
This context holds:
- the validated
Program - the configured scan period
- the scalar, timer, and counter variable lists
- generated symbol names for all runtime storage
- input and output bindings
- extra scratch symbols used by branches and stateful instructions
Supported Variable Types
The Propeller 2 runtime currently supports:
boolinttimercounter
Unsupported numeric features such as non-integer float handling raise a Propeller2RuntimeError during code generation.
Generated Storage Symbols
The generator allocates storage names like:
PLCDATA0PLCDATA1PLCDATA2
These are plain TAQOZ VAR cells.
This detail matters. During development, other allocation strategies caused real runtime corruption on hardware. The current implementation uses:
bytesfor the host line bufferVARfor scalar cells and internal state
That arrangement proved stable on the board.
Runtime Memory Model
The generated data section starts with host parser storage:
128 bytes PLCHOSTBUFVAR PLCHOSTLENVAR PLCHOSTPTRVAR PLCHOSTAVAR PLCHOSTBVAR PLCHOSTC
Then it creates one VAR cell for every generated runtime symbol.
Those cells back:
- runtime mode
- scan period
- scalar values
- force enable flags
- force override values
- timer fields such as
pre,acc,dn,en,tt - counter fields such as
pre,acc,dn - branch scratch storage
- edge-detect scratch state for counters
Ladder Execution Model
The runtime is a cyclic PLC scan.
The generated PLC.SCAN word is:
PLC.INPUTSPLC.APPLY.FORCES- all generated
PLC.RUNG.nwords PLC.APPLY.FORCESagainPLC.OUTPUTS
That sequence is important.
Why Forces Are Applied Twice
Forces are applied before rung execution and again after rung execution.
This gives force values priority over ladder logic:
- the first application makes the forced value visible to rung logic
- the second application prevents rung logic from overwriting the forced output/state before outputs are driven
Inputs And Outputs
Inputs
PLC.INPUTS reads each bound input pin with PIN@ and stores the value into the corresponding scalar tag.
Most inputs use:
0<> PLC.BOOL
Pins in DEFAULT_ACTIVE_LOW_OUTPUTS use inverted logic:
0= PLC.BOOL
Outputs
PLC.OUTPUTS writes bound output pins using HIGH or LOW depending on the current tag state.
Pins 56..63 are treated as active-low by default:
- if the tag is true, the runtime drives the pin
LOW - if the tag is false, the runtime drives the pin
HIGH
This matches the Propeller 2 board LED wiring expected by the project.
Rungs And Generated Code
Each ladder rung becomes a word named:
PLC.RUNG.0PLC.RUNG.1- and so on
Each rung starts by pushing 1 as the current rung power. Then the generator emits TAQOZ code for each instruction and branch in sequence.
For example:
- contacts read source tags and gate the current power
- coils store the resulting value
- math and move instructions operate on runtime cells
- timers and counters call generated helper words
The generator handles nested branches through temporary branch symbols stored in the compile context.
Timers
Each timer gets a generated helper word such as:
PLC.TON.0
Timer storage includes:
preaccdnentt
The timer helper:
- stores the enable state
- increments the accumulator by the scan period while enabled
- clamps
acctopre - updates
dn - updates
tt - resets
acc,dn, andttwhen not enabled
The configured scan period used by the timer is stored in a runtime variable, so timer accumulation is tied to the runtime's scan_ms.
Counters
Counters get generated up/down helper words and state cells for edge detection.
Counter storage includes:
- up-edge scratch
- down-edge scratch
preaccdn
The counter helpers perform edge-sensitive counting and update dn when the accumulator meets the preset condition used by the implementation.
Online Editing Helpers
The runtime generates direct setter words for host-driven edits.
Examples:
PLC.SET.0PLC.FORCE.SET.0PLC.FORCE.CLEAR.0PLC.SET.TIMER.PRE.0PLC.SET.TIMER.ACC.0PLC.SET.COUNTER.ACC.0
These words are what the Python transport calls through TAQOZ.
This is one of the key design choices in the current system:
- the board owns the runtime state and scan logic
- the host does not poke raw memory addresses
- the host invokes stable words with values
Hello, Snapshot, And Upload
The runtime includes three important host-facing words:
PLC.HELLOPLC.SNAPSHOTPLC.UPLOAD
PLC.HELLO
PLC.HELLO returns a simple version marker:
PLC HELLO 2
The transport uses this to detect whether the Propeller 2 currently has the PLC runtime loaded.
PLC.SNAPSHOT
PLC.SNAPSHOT prints a machine-readable dump surrounded by:
PLC SNAPSHOT BEGINPLC SNAPSHOT END
Inside that block it prints:
- mode
- scalar variables
- active forces
- timer state
- counter state
The Python transport parses those lines back into a structured snapshot payload.
PLC.UPLOAD
PLC.UPLOAD prints the currently loaded ladder program as hex-encoded JSON chunks:
PLC CHUNK <index> <hex>
This allows the host to reconstruct the last loaded logical program, not just the live variable state.
Why The Program Is Embedded As Hex JSON
The generated runtime stores the host-side Program model as serialized JSON turned into hex chunks.
That makes upload simple and robust:
- the board does not need a general JSON encoder
- TAQOZ only has to print known chunk literals
- the host can rebuild the exact ladder program from
PLC.UPLOAD
Initialization
PLC.INIT sets all runtime state to a defined starting condition.
It initializes:
- scan period
- mode to
stop - scalar variables to their configured initial values
- force enable flags to
0 - force value cells to the current initial value
- timer presets and zeroed timer runtime fields
- counter presets and zeroed counter runtime fields
- counter edge-detect scratch cells
The runtime starts in stop, not run.
That means downloading a program does not immediately begin scanning. The host must explicitly issue a run command.
Background Scan Execution
The actual background execution lives in the generated runtime words:
PLC.RUNNERPLC.START.COGPLC.RESTORE.RUNTIMEPLC.START.RUNTIME
PLC.RUNNER
PLC.RUNNER loops forever:
- if mode is
run, executePLC.SCAN - wait
scan_ms - repeat
PLC.START.COG
PLC.START.COG:
- stops cog 1
- starts a fresh cog 1
- waits
5 ms - installs
PLC.RUNNERinto that cog's task slot
PLC.RESTORE.RUNTIME
PLC.RESTORE.RUNTIME:
- sets mode to
stop - starts the background cog
PLC.START.RUNTIME
PLC.START.RUNTIME:
- runs
PLC.INIT - runs
PLC.RESTORE.RUNTIME
Why These Words Are Emitted At The End
This was an important bug fix.
PLC.RUNNER must be compiled after PLC.SCAN exists. Earlier versions emitted the runtime task words too early, and on real hardware the background scan task failed even though manual PLC.SCAN worked.
Placing @@RUNTIME_WORDS@@ after PLC.INIT in the template ensures the runner is compiled against the final PLC.SCAN.
The Generated PLC.HOST Protocol
The runtime still generates a custom foreground line protocol:
PLC.HOSTPLC.HOST.CMD.HELLOPLC.HOST.CMD.SNAPSHOTPLC.HOST.CMD.UPLOADPLC.HOST.CMD.RUNPLC.HOST.CMD.SETPLC.HOST.CMD.FORCEPLC.HOST.CMD.TIMERPLC.HOST.CMD.COUNTERPLC.HOST.CMD.QUIT
This protocol uses !HELLO, !VAR, !TIMER, and similar response lines.
Important Current Status
The Python transport does not currently use PLC.HOST.
It is left in the generated runtime, but the working host path uses ordinary TAQOZ command execution instead. This is because the foreground protocol handoff proved unreliable during real hardware testing.
The core issue was that entering the foreground host loop from the prompt was more fragile than expected, while prompt-driven command execution was stable and fast enough once the serial timeout was reduced.
So today:
PLC.HOSTexists in the runtime- the IDE and transport do not rely on it
- prompt-driven TAQOZ control is the supported path
Host Transport Architecture
The host-side runtime transport is transport.py.
Its job is to translate generic remote PLC messages into Propeller 2 specific TAQOZ commands.
Attach Strategy
Propeller2Transport.__post_init__() tries several attach styles:
- no reset, short attach timeout
- reset, short serial timeout
- reset, larger serial timeout
This is a practical robustness measure. Real hardware sometimes responds better to different attach timing depending on the current board state.
Host Message Handling
The transport implements:
hellodownload_program_begindownload_program_chunkdownload_program_commitupload_program_beginupload_program_chunkupload_program_endsnapshot_requestrunset_tagforce
Download Path
Download works like this:
- accumulate JSON chunks from the host
- reconstruct a
Program - re-enter TAQOZ with reset
- send the generated runtime source
- cache the program structure locally
- mark mode as
stop
Snapshot Path
For snapshots, the transport runs:
PLC.SNAPSHOT
and parses lines such as:
PLC MODE 1PLC VAR 0 1PLC TIMER 0 1000 37 0 1 1
into the IDE-facing structured snapshot.
Run/Stop Path
The transport maps:
runtoPLC.RUNstoptoPLC.STOP
Scalar Set Path
A scalar write becomes:
<value> PLC.SET.<index>
Force Path
A force request becomes either:
<value> PLC.FORCE.SET.<index>PLC.FORCE.CLEAR.<index>
Timer And Counter Editing
Timer and counter members are edited through generated words such as:
PLC.SET.TIMER.PRE.<index>PLC.SET.COUNTER.ACC.<index>
The transport resolves tags like T1.pre or C1.acc to the correct generated word.
Why Prompt-Driven TAQOZ Was Chosen
The current transport architecture is the result of hardware debugging.
The repo previously moved toward a more dedicated foreground protocol, but real testing exposed several issues:
- protocol entry was fragile
- serial handoff behavior was noisy
- some approaches hid timing and parser problems rather than simplifying them
The current design works better because:
- TAQOZ prompt handling is already stable
- generated runtime words are simple and direct
- failures are easier to inspect manually from the serial console
- host round-trips are fast enough at
921600baud with a short serial timeout
In real testing, prompt-driven operations such as set, run, snapshot, and force were landing around a few tens of milliseconds per round-trip, which is sufficient for the current IDE use case.
Known Constraints
The current runtime is intentionally narrow.
Important constraints:
- RAM-only, no persistence across reset/power cycle
- only
bool,int,timer, andcounterare supported - no full REAL/float execution path
- no generic arbitrary memory editing from the host
- no host dependence on
PLC.HOST - upload returns the last loaded logical program, not a decompiled runtime
Important Implementation Lessons
These are the main lessons encoded in the current code:
1. Use Simple TAQOZ Storage Primitives
Stable runtime storage came from:
bytesfor the host input bufferVARfor runtime cells
Other allocation strategies caused corruption on real hardware.
2. Compile The Background Runner After PLC.SCAN
Manual scan execution working does not guarantee the background scan task is valid. The generation order matters.
3. Use TAQOZ As The Shell, Not As Something To Replace
Using ordinary prompt-driven command execution turned out to be more reliable than trying to force a full transport takeover.
4. Keep The Runtime In stop After Download
This makes downloads safer and easier to debug. The host can inspect state before starting scan execution.
5. Keep Host Parsing Textual And Explicit
Snapshot and upload output are intentionally text-based and rigidly formatted so the host parser stays simple.
Typical Runtime Lifecycle
A normal session looks like this:
- Create
Propeller2Transport - Attach to TAQOZ at
921600or fallback115200 - Send
hello - Download a program
- Optionally set or force tags while in
stop - Send
run - Periodically request snapshots
- Stop, edit, or redownload as needed
File Responsibilities
src/plc_runtime/propeller2/runtime.py
Responsible for:
- validating compile support
- allocating generated symbols
- generating TAQOZ source
- opening TAQOZ serial sessions
- installing the runtime
src/plc_runtime/propeller2/runtime.fth
Responsible for:
- defining the skeleton layout of the generated runtime
- controlling generation order
- ensuring startup executes
PLC.START.RUNTIME
src/plc_runtime/propeller2/transport.py
Responsible for:
- mapping generic remote messages to Propeller 2 behavior
- attaching to the serial console robustly
- parsing snapshot/upload output
- issuing TAQOZ commands for online control
Summary
The Propeller 2 TAQOZ runtime in this repository is a generated, RAM-only PLC runtime that:
- compiles ladder logic into TAQOZ Forth words
- stores PLC state in TAQOZ
VARcells - runs the scan loop in a background cog
- exposes snapshots, upload, and online edit helpers as TAQOZ words
- is controlled from Python through ordinary TAQOZ command/response traffic

