COMING SOON
ESP32
IP PORT
Hardware integration — Coming in the next version
CONTACTS
─┤ ├─ NO
─┤/├─ NC
─┤↑├─ POS EDGE
─┤↓├─ NEG EDGE
COILS
─( )─ OUTPUT
─(/) NOT COIL
─(S)─ SET
─(R)─ RESET
TIMERS
TON ON DELAY
TOF OFF DELAY
┤T├ TMR DONE
RUNGS
CURSOR
SELECT
// LADDER PROGRAM — CLICK SLOT ( + ) TO INSERT · CLICK ELEMENT TO SELECT
// SIMULATION
CHOOSE ▾
OUTPUT Q0.0 STOPPED
SENSOR I0.2 · NOT TRIGGERED
// I/O MEMORY TABLE

// PICKETTPLC HELP

OVERVIEW
INSTRUCTIONS
WORKFLOW
SIMULATIONS
I/O TABLE
TIPS
What is PickettPLC?
PickettPLC is a free browser-based Ladder Logic simulator. Build, edit, and run PLC programs visually — no hardware, no install, no account required. Ladder Logic is the standard programming language used in industrial automation to control motors, conveyors, pumps, and machinery worldwide.
Key Concepts
⚡ Rung
A horizontal circuit in the ladder. One logical rule. Power flows left-to-right when all conditions are satisfied. Lights up blue when energized.
─┤ ├─ Contact
A condition check on the left side of the rung. Passes power if the linked I/O bit matches the expected state (ON or OFF).
─( )─ Coil
An output action on the right column. When the rung energizes, the coil writes a bit in memory or drives a physical output.
🔄 Scan Cycle
Every 100 ms the PLC reads all inputs, evaluates every rung top-to-bottom, then writes all outputs. The SCAN dot in the header flashes each cycle.
⬇ Parallel Branches
Multiple branch rows inside one rung are OR'd together — if any branch passes, the rung energizes. Used for seal-in and alternative conditions.
⏱ Timer
TON counts up while the rung is energized. TOF counts after de-energize. Use a TDN contact on a later rung to act on the Done bit.
Quick Start
STEP 1
Select tool from toolbar
STEP 2
Click + slot in rung
STEP 3
Pick I/O address
STEP 4
Press ▶ RUN
STEP 5
Press inputs in I/O table
💡
The default program is a Seal-In Motor Start/Stop circuit — the most common PLC pattern. Press ▶ RUN, then hold PRESS on I0.0 (Start) to energize the rung. Release it — the motor stays on via the seal bit. Press I0.1 (Stop) to shut it down.
🖱
Drag & Drop — grab any contact or coil and drag it to a + slot to reorder elements. Works within a rung or across rungs. The dragged element fades while moving.
Contact Instructions — Conditions (Left Side)
─┤  ├─
NO — Normally Open
CONTACT
Passes power when bit = 1 (ON). Most common instruction. Used for start buttons and sensor triggers.
─┤/├─
NC — Normally Closed
CONTACT
Passes power when bit = 0 (OFF). Used for stop buttons, E-stops, and safety interlocks.
─┤↑├─
POS — Positive Edge
CONTACT
Passes power for exactly one scan on the 0→1 transition. Good for one-shot triggers on button press.
─┤↓├─
NEG — Negative Edge
CONTACT
Passes power for exactly one scan on the 1→0 transition. Triggers on button release.
─┤T├─
TDN — Timer Done Contact
CONTACT
Passes power when a timer's .DN bit is set. Place after a TON or TOF to act on completion. Address must match the timer block.
Coil Instructions — Actions (Right Column)
Coils always live in the right column of a rung, separated by a blue border. They are written when the rung is energized. Multiple coils can stack vertically in one rung — all fire together.
─( )─
OUT — Output Coil
OUTPUT
Sets bit to 1 while rung is energized. Cleared to 0 automatically when rung de-energizes. Standard output for motors, lights, solenoids.
─(/)─
NOT — Negated Coil
OUTPUT
Inverse of OUT. Bit is 0 while rung energized, 1 when de-energized. Useful for fail-safe outputs.
─(S)─
SET — Latch Coil
LATCH
Sets bit to 1 on rung energize. Bit stays ON even after rung de-energizes — requires a RST coil to clear it.
─(R)─
RST — Reset (Unlatch)
UNLATCH
Clears bit to 0 on rung energize. Use with a SET coil on a separate rung to build latching (retentive) circuits.
Timer Instructions
TypeNameBehaviour
TONTimer On-DelayCounts up while rung is energized. DN bit goes ON when accumulator reaches preset. Resets to 0 when rung de-energizes.
TOFTimer Off-DelayDN bit stays ON while rung is energized. Starts counting when rung de-energizes; DN bit clears after preset elapses.
TDNTimer Done ContactA contact that reads the .DN bit of a named timer. Place on a separate rung to trigger actions after the timer completes.
Add a timer with TON or TOF — it goes into the coil column. Then use a TDN contact on a separate rung to read its Done bit. The timer address (e.g. T0) must match in both instructions.
Adding Instructions to a Rung
Select an instruction type from the toolbar. Click any + slot between elements in a rung to insert it at that position. A dialog lets you choose the I/O address and an optional label. Contacts go in the left/middle area; coils always route to the right output column automatically.
Editing & Deleting Elements
ActionHow
Delete elementHover over it — a red appears in the top corner. Click it.
Reorder elementsDrag any contact or coil and drop it onto a + slot. Works within a branch or across rungs.
Select a rungClick the number badge on the left (e.g. 001) or click anywhere in the rung body.
Managing Rungs
ButtonAction
+ ADD RUNGAppends a new empty rung at the bottom of the program.
✕ DEL RUNGDeletes the currently selected rung.
↑ / ↓ MOVEReorders the selected rung up or down one position.
Rung Layout — Traditional PLC Format
Each rung has three zones: the left power rail, the contact/logic area (where conditions are placed), and the right coil column (where outputs live). This matches the layout of real Allen-Bradley and Siemens PLC programming software. Multiple coils stack vertically in the right column and all fire simultaneously when the rung is energized.
Parallel Branches (OR Logic)
If a rung has multiple branch rows stacked vertically, they are evaluated as OR logic — the rung energizes if any branch passes. The default Seal-In circuit uses this: Branch A is the Start path, Branch B is the Seal path. Both share the same Stop NC contact.
Seal-In (Motor Latch) Circuit
The default program is the most common PLC pattern. Branch A: Start (NO) in series with Stop (NC). Branch B: Seal bit M0.0 (NO) in series with Stop (NC). Both branches drive Q0.0 Motor and M0.0 Seal coils. Pressing Start sets M0.0, which keeps Branch B alive even after Start is released. Pressing Stop breaks both branches.
Memory Bits (M addresses)
PatternHow it works
Seal-inM0.0 NO contact in parallel branch drives M0.0 OUT coil — holds itself ON after trigger is released.
Step sequencerM0.0 → M0.1 → M0.2: each rung fires when the previous M bit is ON, sets the next bit, resets itself.
Interlock / FaultSet M bit on fault condition. Use NC contact of that M bit on any rung that must stop when fault is active.
One-shotPOS edge contact sets an M bit for exactly one scan — use to trigger a timer without re-triggering every cycle.
OUT coils are cleared each scan then re-written by the coil instruction. SET coils are retentive — they persist until a RST coil explicitly clears them. Use SET/RST when the bit must survive after the trigger condition goes away.
Running & Stopping
Press ▶ RUN to start the scan cycle. The SCAN dot flashes every 100 ms. Toggle inputs in the I/O table using the green PRESS buttons. Press ⏹ STOP to halt — all OUT and NOT-COIL outputs are safely cleared on stop.
Choose a Simulation
🏭
Conveyor Belt
Belt moves when Q0.0 is ON. Box sensor I0.2 triggers automatically when a package passes the detection point.
🛢️
Tank Fill / Drain
Pump Q0.0 fills the tank. HIGH level sensor I0.2 fires at 82% fill. Tank drains slowly at all times.
🚦
Traffic Light Sequencer
Drive Q0.0 RED, Q0.1 AMBER, Q0.2 GREEN with chained TON timers. A classic timing sequence exercise.
⚗️
Batch Mixer Process
3-step sequence: Q0.0 FILL until FULL I0.2, Q0.1 MIX for a timed period, then Q0.2 DRAIN until EMPTY I0.3.
💡
I/O LED Panel
All I/O addresses shown as LEDs. Click blue input LEDs to toggle. Green = outputs. Amber = memory bits. Good for testing logic without a visual process.
I/O Mapping
AddressConveyorTank FillLED Panel
Q0.0Motor — runs beltPump — fills tankOutput LED
I0.0Start button (manual)Start button (manual)Input LED (click)
I0.1Stop button (manual)Stop button (manual)Input LED (click)
I0.2Box position sensor (auto)HIGH level sensor (auto)Input LED (click)
Timer Simulations — I/O Mapping
Address🚦 Traffic Light⚗️ Batch Mixer
Q0.0RED lightFILL valve
Q0.1AMBER lightMIXER motor
Q0.2GREEN lightDRAIN valve
I0.2FULL sensor (auto, fires at 88%)
I0.3EMPTY sensor (auto, fires at 4%)
💡
Switching to the Traffic Light or Batch Mixer sim auto-adds the extra Q and I addresses to your I/O table, so they're ready to use in the insert dialog right away.
Practice Exercises
ExerciseGoalBest Animation
Motor Start/StopHold Start (I0.0), release — motor stays on via seal. Press Stop (I0.1) to kill it.Conveyor
Level ControlPump ON below 20%, pump OFF above 82%. Hysteresis using two rungs and M bits.Tank
Traffic SequenceRED 5s → GREEN 5s → AMBER 2s → repeat. Chain three TON timers, each TDN resetting the cycle.Traffic Light
Batch SequenceFILL until I0.2 → MIX (TON 5s) → DRAIN until I0.3 → idle. Use SET/RST M bits for each step.Batch Mixer
Timed OutputTrigger a TON timer from I0.0. Use TDN contact to turn ON Q0.1 after 3 seconds.LED Panel
Seal with InterlockMotor runs via seal-in. Add an M bit fault flag — NC contact of it on the motor rung locks it out.Conveyor
SET / RST LatchUse SET coil on one rung (trigger: I0.0) and RST coil on another (trigger: I0.1). Bit stays on between presses.LED Panel
🚦 How to Build the Traffic Light
A simple non-repeating version to start: hold I0.0 to run the sequence.
RungLogic
001I0.0 (NO) → TON T0 (5000ms) and OUT Q0.0 RED. Red is on while timing.
002TDN T0 (NO) → TON T1 (5000ms) and OUT Q0.2 GREEN. Green after red completes.
003TDN T1 (NO) → TON T2 (2000ms) and OUT Q0.1 AMBER. Amber after green completes.
For a repeating cycle, add a rung where TDN T2 resets the whole sequence using a SET/RST M bit that gates rung 001.
⚗️ How to Build the Batch Mixer
RungLogic
001I0.0 (NO Start) → SET M0.0 (Fill step active)
002M0.0 (NO) + I0.2 (NC, not full) → OUT Q0.0 FILL
003I0.2 (NO, full) → RST M0.0, SET M0.1 (Mix step)
004M0.1 (NO) → TON T0 (5000ms) and OUT Q0.1 MIX
005TDN T0 (NO) → RST M0.1, SET M0.2 (Drain step)
006M0.2 (NO) + I0.3 (NC, not empty) → OUT Q0.2 DRAIN
007I0.3 (NO, empty) → RST M0.2 (cycle complete)
Switch animations any time using the dropdown above the simulation panel — your ladder program keeps running unchanged.
Address Types
PrefixTypeDescription
IInputPhysical field devices — push buttons, sensors, limit switches. Toggle manually with PRESS or driven automatically by a simulation.
QOutputPhysical actuators — motors, solenoids, indicator lights. Written by coil instructions in your ladder program.
MMemoryInternal bits with no physical connection. Used for seal bits, step flags, interlocks, and intermediate logic.
Pressing Inputs
Each Input (I) row has a green PRESS button. Hold it down to activate the input — release to deactivate. This simulates a momentary push button. The PLC must be running for the ladder logic to respond.
Adding Custom Addresses
Use the bar at the bottom of the I/O table. Select the type (I / Q / M), type an address like I0.3, add an optional name, then click + ADD. New addresses appear immediately in the instruction insert dialog when you add instructions to rungs.
Address Naming Convention
RangeTypeExample
I0.0 – I0.7Input bitsI0.0 = Start, I0.1 = Stop, I0.2 = Sensor
Q0.0 – Q0.7Output bitsQ0.0 = Motor, Q0.1 = Lamp
M0.0 – M9.7Memory bitsM0.0 = Seal, M0.1 = Fault
T0, T1, T2…Timer addressesT0 = 3 s delay, T1 = 10 s hold
💡
Output (Q) and Memory (M) bits are read-only in the I/O table — they are written entirely by your ladder program coils. Watch their values and status dots update in real time as the scan cycle runs.
Keyboard Shortcuts
KeyAction
EnterConfirm the insert dialog (same as clicking OK).
EscapeCancel / close any open dialog or the help modal.
Drag & Drop Tips
Any contact or coil can be dragged and dropped onto a + slot. The + slot highlights blue when a valid drop is available. Dragging a coil to a contact-area slot still places it in the coil column — coils always route to the output side automatically.
💡
You can drag an element from one rung into a different rung entirely — just hover over the destination rung's + slot and release.
Common Mistakes
ProblemCauseFix
Rung never energizesAn NC contact's bit is ON, blocking power flowCheck the I/O table — the bit driving that NC contact may already be 1
Motor won't stay on after releasing StartNo seal-in branch — the rung needs a parallel M bitAdd Branch B: M0.0 (NO) + I0.1 (NC), with M0.0 OUT coil in the coil column
Timer never firesTDN contact address doesn't match the TON/TOF addressBoth must use the same timer address, e.g. T0
Output fires before RUN pressedPLC must be running for logic to executePress ▶ RUN — PRESS buttons only update bits; logic only runs during the scan cycle
Inserted coil appears in contacts areaOld browser cacheCoils auto-route to the right column — refresh if you see stale layout
Learning Resources
🏭 Start Here
Run the default program. Press RUN → hold I0.0 Start → release → press I0.1 Stop. Understand the seal-in before building anything new.
⏱ Timer Exercise
Add a TON coil to rung 001 (T0, 3000 ms). Add rung 002 with a TDN contact (T0) and an OUT coil for Q0.1. Press RUN and hold I0.0 — Q0.1 fires after 3 s.
🔒 SET/RST Pattern
Rung 001: I0.0 NO → SET M0.0. Rung 002: I0.1 NO → RST M0.0. Rung 003: M0.0 NO → OUT Q0.0. The output latches on I0.0 and only clears on I0.1.
🛢️ Tank Hysteresis
Rung 001: I0.2 NC (HIGH sensor) → OUT Q0.0 Pump. This turns the pump off when the tank is full. Add a second rung for low-level restart using M bits.
PickettPLC is part of the free PICKETTECH platform — visit pickettech.com for electronics calculators, lean manufacturing tools, OEE trackers, and more.

// ESP32 FIRMWARE & SETUP GUIDE

WIRING
LIBRARIES
FIRMWARE
CONNECT
Required Components

You need an ESP32 DevKit board (any variant), plus the field devices below. All connect directly to the ESP32 GPIOs — no extra hardware required for the basic kit.

ESP32 GPIOSimulator AddressConnect ToNotes
GPIO 18I0.0 — StartPush button → GNDINPUT_PULLUP — button pulls low when pressed
GPIO 19I0.1 — StopPush button → GNDINPUT_PULLUP — same as above
GPIO 20I0.2 — SensorPush button / proximity sensorINPUT_PULLUP
GPIO 21I0.3Optional 4th inputINPUT_PULLUP
GPIO 22Q0.0 — MotorLED (+ 330Ω to 3.3V) or relay INOUTPUT — HIGH when coil energized
GPIO 23Q0.1LED or relay INOUTPUT
GPIO 25Q0.2LED or relay INOUTPUT
GPIO 26Q0.3LED or relay INOUTPUT
GNDCommon groundShare with all components
3V3LED anodes via 330Ω3.3 V logic rail
Input pins use INPUT_PULLUP — the button logic is inverted (LOW = pressed). The firmware handles this automatically so the simulator sees 1 = pressed.
Never connect a relay coil directly to a GPIO — always use a relay module with an optocoupler. The GPIO can only source ~12 mA.
Arduino IDE Setup

Install the Arduino IDE 2.x from arduino.cc/en/software then add ESP32 board support:

  1. Open File → Preferences
  2. Paste into Additional boards manager URLs:
    https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
  3. Open Tools → Board → Boards Manager
  4. Search esp32 by Espressif Systems → Install
  5. Select your board: Tools → Board → ESP32 Arduino → ESP32 Dev Module
Required Libraries

Install these via Sketch → Include Library → Manage Libraries:

LibraryAuthorPurpose
WebSocketsMarkus SattlerWebSocket server on ESP32
ArduinoJsonBenoit BlanchonJSON encode/decode for I/O data
💡
The WiFi.h library is built into the ESP32 Arduino core — no separate install needed.
ESP32 Arduino Firmware

Copy the full firmware below into a new Arduino sketch. Update the WiFi credentials at the top, then upload to your ESP32.

/*
 * PickettPLC — ESP32 Hardware Bridge Firmware
 * Compatible with PickettPLC simulator at pickettech.com
 *
 * WIRING:
 *   GPIO 18 = I0.0 (button to GND)    GPIO 22 = Q0.0 (LED/relay)
 *   GPIO 19 = I0.1 (button to GND)    GPIO 23 = Q0.1 (LED/relay)
 *   GPIO 20 = I0.2 (sensor / button)  GPIO 25 = Q0.2 (LED/relay)
 *   GPIO 21 = I0.3 (optional)         GPIO 26 = Q0.3 (LED/relay)
 *
 * HOW IT WORKS:
 *   - ESP32 creates a WebSocket server on port 81
 *   - Simulator sends output states (Q bits) as JSON every scan
 *   - ESP32 drives GPIO outputs accordingly
 *   - ESP32 reads GPIO inputs every 50ms and sends to simulator
 */

#include <WiFi.h>
#include <WebSocketsServer.h>
#include <ArduinoJson.h>

// ── WiFi credentials ──────────────────────────────────────────────
const char* WIFI_SSID     = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";

// ── Input pins (INPUT_PULLUP — LOW when pressed) ──────────────────
const int INPUT_PINS[]  = { 18, 19, 20, 21 };
const char* INPUT_ADDR[] = { "I0.0", "I0.1", "I0.2", "I0.3" };
const int NUM_INPUTS = 4;

// ── Output pins ───────────────────────────────────────────────────
const int OUTPUT_PINS[]  = { 22, 23, 25, 26 };
const char* OUTPUT_ADDR[] = { "Q0.0", "Q0.1", "Q0.2", "Q0.3" };
const int NUM_OUTPUTS = 4;

// ── WebSocket server ──────────────────────────────────────────────
WebSocketsServer webSocket(81);
int connectedClient = -1;

// ── Previous input state for change detection ─────────────────────
bool prevInputState[4] = { false, false, false, false };

unsigned long lastSendTime = 0;
const unsigned long SEND_INTERVAL = 50; // ms

void setup() {
  Serial.begin(115200);
  Serial.println("\n\n=== PickettPLC ESP32 Bridge ===");

  // Setup GPIO
  for (int i = 0; i < NUM_INPUTS; i++) {
    pinMode(INPUT_PINS[i], INPUT_PULLUP);
  }
  for (int i = 0; i < NUM_OUTPUTS; i++) {
    pinMode(OUTPUT_PINS[i], OUTPUT);
    digitalWrite(OUTPUT_PINS[i], LOW);
  }

  // Connect to WiFi
  Serial.printf("Connecting to %s", WIFI_SSID);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi connected!");
  Serial.printf("IP Address: %s\n", WiFi.localIP().toString().c_str());
  Serial.println("Enter this IP in the PickettPLC simulator");

  // Start WebSocket server
  webSocket.begin();
  webSocket.onEvent(wsEvent);
  Serial.println("WebSocket server started on port 81");
}

void loop() {
  webSocket.loop();

  unsigned long now = millis();
  if (now - lastSendTime >= SEND_INTERVAL) {
    lastSendTime = now;
    sendInputs();
  }
}

// ── Read physical inputs and send to simulator ────────────────────
void sendInputs() {
  if (connectedClient == -1) return;

  StaticJsonDocument<256> doc;
  JsonObject inputs = doc.createNestedObject("inputs");

  for (int i = 0; i < NUM_INPUTS; i++) {
    // INPUT_PULLUP: LOW = pressed = 1 in simulator
    bool state = !digitalRead(INPUT_PINS[i]);
    inputs[INPUT_ADDR[i]] = state ? 1 : 0;
  }

  String json;
  serializeJson(doc, json);
  webSocket.sendTXT(connectedClient, json);
}

// ── WebSocket event handler ───────────────────────────────────────
void wsEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) {
  switch (type) {

    case WStype_CONNECTED:
      connectedClient = num;
      Serial.printf("Simulator connected from %s\n",
        webSocket.remoteIP(num).toString().c_str());
      // Send welcome message
      webSocket.sendTXT(num, "{\"status\":\"connected\",\"device\":\"PickettPLC-ESP32\"}");
      break;

    case WStype_DISCONNECTED:
      Serial.printf("Simulator disconnected (client %d)\n", num);
      if (connectedClient == num) {
        connectedClient = -1;
        // Safe state: turn off all outputs on disconnect
        for (int i = 0; i < NUM_OUTPUTS; i++) {
          digitalWrite(OUTPUT_PINS[i], LOW);
        }
      }
      break;

    case WStype_TEXT:
      // Receive output states from simulator and drive GPIO
      {
        StaticJsonDocument<256> doc;
        DeserializationError err = deserializeJson(doc, payload, length);
        if (err) { Serial.println("JSON parse error"); return; }

        if (doc.containsKey("outputs")) {
          JsonObject outputs = doc["outputs"];
          for (int i = 0; i < NUM_OUTPUTS; i++) {
            if (outputs.containsKey(OUTPUT_ADDR[i])) {
              int val = outputs[OUTPUT_ADDR[i]];
              digitalWrite(OUTPUT_PINS[i], val ? HIGH : LOW);
            }
          }
        }
      }
      break;

    default:
      break;
  }
}
Uploading
  1. Update WIFI_SSID and WIFI_PASSWORD
  2. Select your ESP32 board and COM port in Arduino IDE
  3. Click Upload (→)
  4. Open Serial Monitor at 115200 baud
  5. Note the IP Address printed — you'll enter it in the simulator
Connecting the Simulator
  1. Make sure your PC/phone is on the same WiFi network as the ESP32
  2. Check the Serial Monitor for the ESP32's IP address (e.g. 192.168.1.105)
  3. Enter the IP in the IP field in the ESP32 bar above
  4. Leave Port as 81 (default)
  5. Click ⚡ CONNECT
  6. The dot turns green and status shows HARDWARE LIVE
  7. Press ▶ RUN — your ladder logic now drives real GPIO pins!
What Happens in Hardware Mode
DirectionWhatHow Often
Simulator → ESP32Q output states (JSON)Every 100ms scan cycle
ESP32 → SimulatorI input states (JSON)Every 50ms
In hardware mode, the simulator's PRESS buttons in the I/O table are disabled for input addresses — physical buttons on the ESP32 take over. Output LEDs still show simulated state.
Same network required. The WebSocket connection is direct browser → ESP32 on your local network. No internet routing needed — but both must be on the same WiFi or LAN.
Troubleshooting
ProblemFix
Can't connectCheck IP address in Serial Monitor. Ensure same WiFi. Try disabling browser HTTPS enforcement for local IPs.
Outputs don't fireCheck Q address names match exactly (Q0.0, Q0.1…). Verify GPIO wiring.
Inputs not readingCheck INPUT_PULLUP wiring — buttons should connect GPIO to GND. Verify address names match simulator.
Connection dropsESP32 watchdog may restart — add esp_task_wdt_reset() in loop if needed.
// SIMULATION INFO

🚧 Coming Soon!

ESP32 hardware integration will be available in the next version of PickettPLC. Stay tuned!

▶ Press RUN first to activate the PLC