Skip to content

ssweber/pyrung

Repository files navigation

pyrung

Write ladder logic in Python. Simulate it. Test it. Deploy it.

pyrung turns Python's with block into a ladder rung — condition on the rail, instructions in the body.

from pyrung import Bool, PLCRunner, Program, Rung, out

Button = Bool("Button")
Light = Bool("Light")

with Program() as logic:
    with Rung(Button):
        out(Light)

runner = PLCRunner(logic)
with runner.active():
    Button.value = True
    runner.step()
    assert Light.value is True

Status: Core engine, Click PLC dialect, CircuitPython dialect, and VS Code debugger are implemented and tested (~26k lines, 1,600+ tests). Not yet on PyPI. API may still change.

Why?

AutomationDirect CLICK PLCs have no built-in simulator. You write logic, download it to hardware, and hope. pyrung lets you test first — same tag names, deterministic scans, real assertions. When it works, transpose it to Click.

Or don't transpose at all. Run your program as a soft PLC to test Modbus send/receive — it runs behind a Click-compatible Modbus interface, no hardware required. You can even spin up two pyrung programs and test them talking to each other. Or generate a CircuitPython scan loop for a ProductivityOpen P1AM-200 and run it on actual I/O.

Quick start

# Requires Python 3.11+
uv add pyrung

A motor with start/stop logic

from pyrung import Bool, Program, Rung, latch, reset

Start = Bool("Start")
Stop = Bool("Stop")
Running = Bool("Running")

with Program() as logic:
    with Rung(Start):
        latch(Running)
    with Rung(Stop):
        reset(Running)

Test it

from pyrung import PLCRunner

runner = PLCRunner(logic)
with runner.active():
    Start.value = True
    runner.step()
    assert Running.value is True

    # Release start — motor stays latched
    Start.value = False
    runner.step()
    assert Running.value is True

    Stop.value = True
    runner.step()
    assert Running.value is False

Map to Click hardware when you're ready

from pyrung.click import TagMap

tags = TagMap()
tags.map(Start, "X001")    # Physical input
tags.map(Stop, "X002")     # Physical input
tags.map(Running, "Y001")  # Physical output

tags.validate(logic)  # Checks against Click constraints
tags.export_nicknames("motor.csv")  # For Click programming software

What's included

Core engine — Pure f(state) → new_state scan cycle with immutable snapshots. Coils, latches, timers, counters, branching, subroutines, structured tags, edge detection, and more. Built to match real Click behavior — no surprises when you move to hardware.

Click PLC dialect — Hardware address mapping, memory bank validation, Modbus instructions, and nickname file export. Run any program as a soft PLC behind a Click-compatible Modbus interface for integration testing. Docs →

CircuitPython dialect — Generates a self-contained scan loop for P1AM-200 hardware from any pyrung program. Docs →

VS Code debugger — Step through scans, set breakpoints on rungs, force tags, diff states, and time-travel through scan history. Docs →

Learn more

Core Concepts Scan cycle, SystemState, tags, blocks
Ladder Logic Guide Full DSL reference
Tag Structures UDTs, named arrays, cloning, block config
Runner Guide Execution, time modes, history, fork
Testing Guide Unit testing with deterministic time
Forces & Debug Force values, breakpoints, time travel

About

Write ladder logic in Python. Simulate it. Test it. Deploy it --- Currently targets AutomationDirect CLICK PLC and CircuitPython ProductivityOpen P1AM-200

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors