English | العربية
A free, open 3D factory simulator for learning PLC programming — a modern, customizable open replacement for Factory I/O.
Developed by Mahamed Algaroshy (محمد الجروشي)
Repository: github.com/malgaroshy-maker/factoryforge
Windows (zip) · Linux (zip) · all releases
- Extract the zip, for example to
C:\FactoryForge. Nothing to install. - Run
windows\FactoryForge.exe(Linux:linux/FactoryForge.x86_64). If Windows says "Windows protected your PC", click More info, then Run anyway: the build is not code-signed, and that is all the warning means. - Click Watch it run to see the factory move with no PLC at all, then pick a scene and connect your own controller with the Getting Started guide.
Questions, ideas or feedback: Discussions. Found a bug: open an issue.
FactoryForge allows students, automation engineers, and software developers to write PLC logic (Ladder Diagram, SCL, Function Block Diagram) in TIA Portal, OpenPLC, Node-RED, or Ignition SCADA, and watch it drive a real-time 3D physics-based factory in Godot 4.7.
No accounts, no per-seat subscription fees, and 100% open for custom part & driver creation.
┌────────────────────────────┐ ┌──────────────────────────┐
│ SIM ENGINE (Godot 4.7/C#) │ │ DRIVER SIDECAR (Python) │
│ │ │ │
│ 3D render + Jolt physics │ tag bus │ asyncua (OPC UA) │
│ scene editor / voxel grid │ ◄──────► │ pythonnet (PLCSIM) │
│ 44-part library │ WS │ python-snap7 (S7) │
│ tag registry (authority) │ JSON │ built-in (Modbus) │
└────────────────────────────┘ └──────────────────────────┘
- A first hour with free tools. The first-hour path takes you from the download to OpenPLC running a Structured Text program you complete, driving the sorting line over Modbus TCP. On Windows OpenPLC runs inside WSL; on Linux everything is one machine. Nothing in it needs a Siemens licence. If you have TIA Portal and PLCSIM Advanced, the same guide's second path covers them.
- A grader that marks your program.
factoryforge-sidecar graderuns an exercise headless against one of 18 graded scenes and returns a verdict, an exit code and the evidence. It presses the panel's own Start, Stop and Reset, cuts the E-stop, and injects faults, so a program that ignores them fails for the reason a real one would. See GRADING.md. - A starter for every graded scene, in OpenPLC Structured Text and in TIA Portal SCL, with the tag addresses already laid out: see examples/README.md.
- Connect from the F5 dialog. Pick a driver, fill in the address, and it starts the sidecar and copies the command; for Modbus it takes the bind host and port, which is what OpenPLC in WSL needs.
- Two levels. A mezzanine deck and a vertical lift (
PgUp/PgDnpick the level) make height a routing dimension. - Raw analog. The pressure transmitter publishes 4-20 mA raw counts and a broken wire reads 32767: your program does the scaling, and has to tell a fault from a reading.
- The first-hour program graded PASS 15/15 at three seeds on OpenPLC running in WSL2 and polling the sidecar across the Windows boundary, and 15/15 at three seeds on an S7-1500 in PLCSIM Advanced through the native driver. The reports are in examples/graded/, along with the run that failed: the original
Sorting.scl, which sorts perfectly and fails the exam because it predates the operator panel. - The unchanged
Sorting.sclv0.4 sorted 48 tall / 48 short in a 300-second run over the PLCSIM native driver and again over OPC UA, with no misroutes (IMPROVEMENT_PLAN.md, IP-13). - No real student's program has been graded yet. What has been graded is programs this project wrote. Both release archives passed the release gate, which includes the frozen grader marking a built-in controller PASS and a wrong one FAIL; no real controller has been graded from the Linux archive.
- 🧲 Material-aware sensing: items carry a material, so an inductive sensor sorts metal from cardboard instead of being a second presence sensor.
⚠️ Break the machine on purpose (⚠ Fault): arm the fault tool and click a conveyor or a pusher. The drive stops while its command is still on — the belt disobeys, its fault beacon lights, and<part>.faultgoes true for the PLC to read. A jammed cylinder freezes mid-stroke rather than returning home, so the limit switches are the only honest thing to read, and a seized tank valve holds its opening — the nastier failure, because the process keeps moving and the controller's own output cannot tell you. Until this existed every actuator did exactly what it was told, which made half of real PLC work unteachable: an interlock exists precisely because the plant does not always obey.- 🎚️ Analog I/O: Float tags end to end — a modulating valve, a level transmitter, a heater against a first-order thermal plant, a variable-speed drive whose actual speed lags the reference you gave it, and a needle gauge to read any of them in the scene. Enough to write a real PID against a nonlinear process, and to measure why the integral term exists rather than being told.
- ⏯️ Run / Pause / Reset & time scale (0.25×–4×): freeze the line mid-cycle to read every sensor and actuator at that instant, or slow a fast sequence down to watch an interlock. The PLC stays connected while paused.
- 🎮 Godot 4.7 C# 3D Engine & Jolt Physics: 60 FPS 3D rendering with 4× MSAA, soft shadows, SSAO, glow on the parts that are meant to be lights, and continuous collision detection. Conveyor drums turn at true surface speed, cylinders cushion into their end stops, stack-light lamps cast real light onto what is beside them, and the camera frames the scene you just opened instead of leaving you looking at a control panel.
- 📦 Real rigid-body cartons: mass from carton density, friction tuned per material pair (rubber belt, cardboard, steel chute), boxes that accumulate behind a blocked diverter instead of passing through it.
- 🏠 Start screen with 19 templates: open on a chooser rather than cold into one demo. Each template teaches a different thing — among them momentary buttons and a latching E-stop, analog level control with a nonlinear process, sorting on a measurement instead of two bits, a checkweigher with metal detection, a pick-and-place gantry you sequence on feedback rather than timers, a thermal loop where proportional control alone visibly parks short of setpoint, and a buffer where product accumulates behind a blade stop and is released by belt travel rather than by a timer — plus recent scenes and the full key list.
F12brings that key list back once a scene is open. - 🕹️ Operate any component by hand (
F1): switch the toolbar from✎ Buildto👆 Operateand click a conveyor, a pusher, a stack light lamp or a tank valve directly — not just the operator panel's Start/Stop/Reset/E-stop. Every shipped scene answers to its panel: Start runs the line, Stop stops it, the mushroom latches a trip that only Reset clears, and the panel's setpoint pot is the one number that scene is about — the level to hold, the height that counts as tall, the weight that counts as a reject, the batch to make. Drag the knob mid-run and the line changes what it does, with no code edited. A banner names what's clickable, hovering outlines it, and every part's own property panel carries a live toggle or slider for its I/O too, so you can see what a part does before writing a line of PLC code against it. - 🏭 A shop to build it in: the line stands on poured concrete with a control joint around every two-metre bay, inside clad walls five metres to the eaves. Both are generated in code — this project ships no image assets — and both are there for a reason beyond looking better: with nothing around it, a conveyor could be two metres long or twenty, and a floor bay is a ruler lying under the machine.
- 📋 Every template tells you what to build: press
Tand the scene says what it is asking of you — the task, the tags your program drives and reads, and how you know it works. The lesson used to live only in the Python test harness, which is the last place somebody learning PLC programming will look. - 🔎 A tag list you can find things in: search it, collapse it by machine, or show just the half the PLC writes. A tag you have forced by hand marks its own name and raises a count with a one-click release — because a forgotten force is a value that disagrees with the simulation on purpose, and it explains more "why is my program not working" than anything else.
- 🛠️ 3D Scene Editor Suite: a searchable palette that tells you what each part does and which tags it will register. The part stays in your hand after you place it, so a line of six conveyors is six clicks rather than six trips back to the palette — the lit palette button says what you are holding, and
Escputs it down.Ctrl+Dlands its copy clear of the original and selects it, so pressing it again walks a line across the grid; the arrow keys nudge the selection one cell at a time. Click a placed part and drag it to a new cell — one gesture, oneCtrl+Z— with grid snapping, rotation (R), a selection wireframe gizmo, and undo/redo throughout. A selection can be several parts:Shift-click to add,Ctrl-drag a box over the floor to take everything inside it,Ctrl+Afor all of them, and then move, nudge, rotate, duplicate or delete the whole group as one undo step.Ctrl+C/Ctrl+Vcarries a section into another scene.Nfloats every part's name over it — and a part's name is its tag prefix, so that is the list your PLC program is written against.PgUp/PgDnchange the level you are building on — a line can run on a mezzanine above the floor, and a lift carries product between the two; with a selection, they lift it a level as one undo step.1-4snap the camera to iso, top, front and side without losing what you were looking at. The property panel names what the selected part responds to, so none of it has to be guessed. - 🔌 Visual I/O Driver Wiring Panel (
F4): Centered split-screen modal — click a PLC address (%I0.0,%Q0.0), then click the component tag to map it to. Auto-map suggests an address for every tag in the loaded scene, and Export writesio_mapping.jsonandio_tags.csvfor the sidecar and for whoever is building the PLC side. - 🏷️ Live tag inspection and forcing: the Tag Inspector lists every tag the loaded scene owns with its live value, and forces any of them — bit, int and float alike — with a typed value. A
🔓 N forcedchip in the toolbar shows what is being held by hand and releases it all in one click. The parts that measure something — the light curtain, the level tank, the digital display — also read out in 3D on the part itself. - 🧪 A built-in exercise per scene: the toolbar's 🧪 Try button (or, from a source checkout,
python tools/try_scene.py --scene <id>) spawns or attaches to the engine and drives the scene with the grader's own reference controller — the same program the grader is checked against, pressing the panel's own buttons and turning the setpoint pot the way the grader's exam does — then reports whether the scene completed, measured from what the engine itself reports. The thing to run before writing a real program against it. - 🏭 Native Siemens Integration: all three Siemens paths verified driving the 3D scene from a virtual S7-1500 — PLCSIM Advanced Simulation Runtime API (shared memory, no network, no OPC UA licence), OPC UA client, and Snap7 ISO-on-TCP. Belt, emitter, sensors, diverter and counters all run off the CPU's own program.
- 📊 Multi-Protocol SCADA Support: Built-in OPC UA client/server, Modbus TCP server, and Node-RED integration.
The tag ids below are the built-in scene's names. A part's Name is its tag
prefix — rename a pusher to reject in the property panel and its tags become
reject.extend, reject.extended, reject.retracted. That is the whole naming
rule, and it is what makes a scene you build addressable from a PLC.
| Component | Description | Tag Bus Interface |
|---|---|---|
| Conveyor Belt | Surface-velocity belt with side rails and legs, plus a drive-fault beacon | conveyor.rotate (Bit, Output) · conveyor.fault (Bit, Input) |
| Photoelectric Sensor | Diffuse beam sensor, reflects off the item itself | sensor.detect (Bit, Input) |
| Retroreflective Sensor | Beams to a reflector post across the lane; sees matt and dark items a diffuse sensor misses | sensor.detect (Bit, Input) |
| Inductive Sensor | Responds to metal only — cardboard passes it as if the lane were empty | sensor.detect (Bit, Input) |
| Light Curtain | Light curtain of 12 beams; reports the height of the tallest blocked beam, so one part replaces a low/high sensor pair | lightarray.height (Float, Input), .blocked (Bit, Input) |
| Pneumatic Pusher | Cylinder housing, chrome shaft & orange face plate; a jam freezes it mid-stroke | pusher.extend, pusher.extended, pusher.retracted, pusher.fault |
| Ramp (Chute) | 30° gravity chute with guide rails; incline and friction are a matched pair so cartons actually slide | Physical static body |
| Stack Light | 3-stage industrial tower light (Green, Yellow, Red) | stacklight.green, yellow, red |
| Digital Display | 3D 7-segment LED panel displaying live integer counts | display.value (Int, Output) |
| Roller Conveyor | Driven roller deck for pallets and totes that would scuff a belt; rollers spin at the true surface speed | rollerconveyor.rotate (Bit, Output) · .fault (Bit, Input) |
| Weigh Conveyor | Integrated load cell scale reading the carton's mass in grams — 720 g for a short carton, 2160 g for a tall one, 12960 g for a metal one — and showing it on the scale | weighconveyor.weight (Int, Input) |
| Box Emitter | Spawner emitting tall & short rigid cartons, optionally every Nth in metal | emitter.emit (Bit, Output) |
| Box Remover | Area3D zone despawning items & incrementing a counter; the counted tag is pickable, so two removers can feed one total | remover.count (Int, Input) |
| Control Panel | Operator station you can actually press. Start/Stop/Reset are momentary — one clean scan per click, however long you hold the mouse — and the mushroom is a maintained E-stop wired normally closed, so its tag is true while the circuit is healthy. The setpoint pot is dragged, reads out in the scene's own units on its scale plate, and turns itself to match a tag driven from a PLC | panel.start, .stop, .reset, .estop (Bit, Input) · panel.setpoint (Float, Input) · panel.green, .red (Bit, Output) |
| Level Tank | Analog process tank; outflow follows Torricelli, so process gain varies with level and a PID tuned full overshoots when empty. A seized valve holds its opening — the process keeps moving while the command reads zero | tank.fill, tank.drain (Float, Output), tank.level, tank.fault (Input) |
| VFD Conveyor | A belt behind a variable-frequency drive. The reference ramps, so commanded and actual speed genuinely disagree while the drive is moving between them — a controller that treats the reference as instantly true is wrong here in a way you can measure | vfd.run, vfd.speed (Float, Output) · vfd.actual (Float, Input), .fault |
| Pivot Diverter | A blade on a rotary actuator that deflects a moving carton across the lane without stopping the line. Two limit switches and a swing time are the whole exercise; a seizure freezes it mid-sweep, across a running lane | div.divert (Output) · div.diverted, div.home, div.fault (Input) |
| Pick & Place Gantry | A portal with a travelling carriage, a telescoping Z column and a vacuum cup that really picks a carton up and drops it with the carriage's own velocity. Three motions to sequence, and a grip that reports honestly when it caught nothing | arm.target (Float, Output), .lower, .grip (Output) · .position (Float, Input), .inposition, .lowered, .raised, .holding, .fault |
| Barcode Scanner | Overhead reader that says what an item is, not just that it is there — 101 short carton, 102 tall, 201 metal — as a code plus a one-scan read pulse a program has to latch | scan.enable (Output) · scan.code (Int), scan.read, scan.present (Input) |
| Heating Station | First-order thermal plant with ambient loss: heat fast, cool only as fast as the room allows. Pure proportional control leaves a standing offset you can measure. A failed element still reports full power while the plate cools | oven.heater (Float, Output) · oven.temperature (Float), oven.attemp, oven.fault (Input) |
| Analog Gauge | Needle instrument with a graduated plate, a red band and a digital sub-readout — somewhere for a float to be read in the scene rather than only in the tag list | gauge.value (Float, Output) |
| Alarm Beacon | Rotating beacon that sweeps a real light across the machines near it, plus a horn with a visible diaphragm. What you notice from the other end of the building | beacon.beacon, beacon.horn (Bit, Output) |
| Selector Switch | The third kind of operator input: not a pulse and not a latch, but a knob that stays where it is put. The controller reads a position, not an edge — which is what an Auto/Manual program is built around | selector.position (Int, Input) |
| Guard Door | An interlocked guard whose switch is closed while the door is shut (normally closed, like the mushroom). The solenoid lock makes it a two-way contract: the controller decides whether the door may be opened at all, and until it releases the lock the handle does nothing | guard.lock (Bit, Output) · guard.closed, guard.locked (Bit, Input) |
| Blade Stop | A blade that rises through the lane to hold cartons on a belt that keeps running. The only way to build an accumulation buffer here: the queue behind it builds because the solver says so, and releasing one lets the rest close up on their own. A seizure is the nasty one — frozen halfway, it stops short cartons and lets tall ones ride over | stop.raise (Bit, Output) · stop.up, stop.down, stop.fault (Bit, Input) |
| Transfer Turntable | A rotary index that turns a carton into a new lane. The load is held on by friction, not by being parented to the deck, so a deck told to index too fast genuinely throws it — which is the real constraint on how fast a transfer can run | xfer.index (Bit, Output) · xfer.athome, xfer.atindex, xfer.fault (Bit, Input) |
| Measuring Encoder | A wheel riding the belt it is placed over, counting pulses per metre of travel. Product tracked by distance instead of by a timer, so the logic survives anybody turning the drive up. Place it away from a conveyor and it counts nothing and does not turn — the honest failure, and a visible one | enc.reset (Bit, Output) · enc.count (Int), enc.rate (Float, Input) |
| Cooling Fan | A ducted fan that adds to the loss term of any heating station within reach, giving the thermal plant a second actuator pulling the other way. One plant, two actuators — which is split-range control, and the first place a deadband exists for a reason | fan.run, fan.speed (Float, Output) · fan.airflow (Float), fan.fault (Bit, Input) |
| Two-Hand Control | Two palm buttons whose permissive is not left AND right: the relay also requires that the two presses arrived within half a second of each other, so taping one button down defeats nothing. A program that ANDs the two bits itself passes its own test and fails the real device |
hands.left, hands.right, hands.valid (Bit, Input) |
| Motor Starter | The device between the PLC and the motor, which the library did not have. Your program energises a coil; the contactor runs the machine, and the auxiliary contact is what tells you it actually did — a scan later than you commanded it. A sustained overload trips and stays tripped while the coil is still called | starter.coil (Bit, Output) · starter.aux, starter.overload (Bit, NC) · starter.current (Float, Input) |
| Safety Relay | Dual-channel cross-monitoring with a real discrepancy window: two contacts that stop agreeing latch a fault, which is what a welded contact looks like from the relay's side. Its reset is edge-triggered, so a taped-down button restarts nothing, and it permits rather than commands — closing it must start no machine | relay.reset (Bit, Output) · relay.cha, relay.chb, relay.fault (Bit, Input) |
| Area Scanner | Warning and protective fields as separate outputs, and muting that expires. A mute is a real hole in a guard; the timeout is what stops a bridge request becoming a permanent defeat, and the scanner refuses to honour one past its limit | scanner.mute (Bit, Output) · scanner.stop, scanner.warn, scanner.muted (Bit, Input) |
| Double-Acting Cylinder | Two coils on a 5/2 valve with no spring return, and two reeds with a genuine gap between them — so mid-stroke neither is made. Dropping both coils leaves the rod coasting where it was going, because a spool without a spring stays where it was put | cyl.extend, cyl.retract (Bit, Output) · cyl.extended, cyl.retracted, cyl.fault (Bit, Input) |
| Dosing Pump | Gives the tank an inflow it does not own, which is what makes cascade control possible: a fast flow loop inside a slow level loop. The speed reference ramps, so commanded and delivered flow genuinely disagree while it gets there | pump.run, pump.speed (Float, Output) · pump.flow (Float), pump.fault (Bit, Input) |
| Flow Meter | A rate and a resettable totaliser — a process variable and a batch counter in one part. The reset is a level, not an edge, so a program that pulses it clears nothing. Ending a batch on the total rather than on a timer is the lesson: halve the flow and the litres stay the same | meter.reset (Bit, Output) · meter.rate (Float), meter.total (Int, Input) |
| Articulated Arm | Joint angles, not a tip target — the gantry already takes one of those, and an arm that did too would teach pick-and-place twice and hide the nonlinearity. A command past a stop is refused rather than clamped silently; the tool travels arcs, not chords (176 mm off the chord on a two-joint move); and radial authority collapses at full stretch — 7.0 mm of reach per degree of elbow at 90°, 0.061 mm at full extension, a factor of 115 | arm.waist, arm.shoulder, arm.elbow (Float, Output) · arm.grip (Bit, Output) · arm.atwaist, arm.atshoulder, arm.atelbow, arm.reach, arm.height, arm.stretch (Float) · arm.inposition, arm.limit, arm.holding, arm.fault (Bit, Input) |
| Pallet Station | A pattern generator rather than a robot, so it composes with the gantry, the arm or a plain pusher: it publishes where the next carton goes and counts what has landed. Indexing through a layer, a quarter-turn interlock between layers, and a pallet that fills and then refuses while still holding its published position | pallet.index, pallet.change (Bit, Output) · pallet.nextx, pallet.nexty, pallet.nextz (Float) · pallet.slot, pallet.layer, pallet.count (Int) · pallet.layerdone, pallet.full (Bit, Input) |
| Vertical Lift | Height as a routing dimension — everything else in the library lives on one plane. Mutual exclusion is enforced by the machine, not by the program: a blade across the infeed mouth is down only while the carriage is at that level, empty and healthy, so a second carton is physically held on a belt that keeps running | lift.target (Int, Output) · lift.transfer (Bit, Output) · lift.level (Int) · lift.height (Float) · lift.atlevel, lift.occupied, lift.ready, lift.fault (Bit, Input) |
| Mezzanine Deck | A raised floor, so a line can have a second storey. Placed on the level it holds up, its deck is exactly that level's floor: a belt or a sensor placed on the same level stands on it without knowing, and its legs reach the ground from wherever it is. The deck is solid, so a carton that leaves a raised belt anywhere but where it should lands on it, as it would in a building. It pairs with the lift | — (static) |
| Limit Switch | A roller lever that something has to physically push: a carton on the belt, a cylinder's face plate, a carriage. The contacts change over part-way round the lever's travel and change back a few degrees earlier, and NO and NC are two contacts rather than one bit and its inverse. Contact bounce is a setting (off by default): turn it up and a program counting raw edges over-counts | ls.no, ls.nc (Bit, Input) |
| Solenoid Valve | A spring-return process valve with opened and closed feedback, for discrepancy monitoring. It takes its travel time to move and reports neither end while it does, and a stuck valve's feedback never follows the command, whether it seized shut or half way | valve.open (Bit, Output) · valve.opened, valve.closed, valve.fault (Bit, Input) |
| Star-Delta Starter | Main, star and delta contactors on a motor, with the changeover timing left to your program. Star and delta closed together is a short circuit: the breaker trips and stays tripped until reset by hand. A contactor opens slower than one closes, so swapping the two in one scan trips it too. A motor that cannot run up in star trips its overload | sd.main, sd.star, sd.delta (Bit, Output) · sd.mainaux, sd.staraux, sd.deltaaux, sd.breaker, sd.overload (Bit, Input) · sd.speed, sd.current (Float, Input) |
| Pressure Transmitter | An air receiver with a supply valve and a steady draw, measured by a 4-20 mA transmitter that publishes raw counts only: your program scales them to bar. A broken wire reads 32767, which the program has to tell apart from a real reading | air.supply (Bit, Output) · air.pressure (Int, Input) · air.wirebreak (Bit, Input) |
| Servo Axis | A linear axis on a servo drive with a PLCopen-shaped interface: enable, ready, target and velocity, position and in-position. A drive fault, or a command it cannot carry out, stops the axis and latches error, and nothing moves until a rising edge of ack arrives with the cause gone |
axis.enable, axis.ack (Bit, Output) · axis.target, axis.velocity (Float, Output) · axis.position (Float, Input) · axis.ready, axis.error, axis.inposition, axis.fault (Bit, Input) |
Grab the archive for your platform from
Releases, extract
it, and run FactoryForge. No Godot, no .NET SDK and no Python needed — the
sidecar that speaks every PLC protocol, and grades your program, ships frozen
alongside the engine.
Then follow Your first hour
in the Getting Started guide, which is also in the download as
docs/GETTING_STARTED.md. It takes you from the download to
OpenPLC, a free IEC 61131-3
runtime, running a Structured Text program you complete. Tall cartons go down
the chute, and factoryforge-sidecar grade marks the program PASS. No licence
is needed, and no Siemens software. If you have TIA Portal and PLCSIM
Advanced, the same guide's second path covers them.
Every release is gated on the headless self-tests in
tools/packaging/check_release.py run against the exported binary, not
against a source checkout — a checkout can pass everything while the release
fails, because two of those tests read fixtures that System.IO cannot reach
once packed into a .pck.
Windows will warn you on first run. These builds are not code-signed, so SmartScreen shows "Windows protected your PC" — click More info → Run anyway. That warning means the binary has no purchased certificate attached, not that anything is wrong with it. See PACKAGING.md for why this project does not buy one.
Everything below builds it yourself, the same path GETTING_STARTED.md walks in detail. You need Godot 4.7 mono, the .NET 8 SDK and Python 3.11+.
git clone https://github.com/malgaroshy-maker/factoryforge.git
cd factoryforge
pip install -e "sidecar[dev,opcua]"python -m pytest -qHow many pass is whatever that command prints. No count is written here on
purpose — four files used to quote three different numbers between them, and
A6b in tools/test_plan.py now fails if one comes back.
Or the full plan — build, the Python suite, the engine's own self-tests, determinism, the engine↔sidecar seam and robustness. No PLC needed:
python tools/test_plan.py # --gui adds the two display-dependent checkspython run.pyrun.py finds Godot (or tells you exactly what to install and where), builds
the C# engine, and launches it — no separate dotnet build step, and it works
the same on Windows and Linux. (Windows users can also double-click
run_factoryforge.bat, which just calls run.py.)
This runs the physics scene: Jolt rigid-body cartons, real collisions, and components whose properties genuinely change how the line behaves — speed up the belt and boxes outrun the diverter; hold the pusher out and the line backs up behind it.
# Fixed-timestep scene instead: reproducible, and the regression contract.
python run.py -- --deterministicBoth scenes expose the same tags and report the same scene name, so a PLC
program, Node-RED flow or SCADA client drives either one unchanged. Use
--deterministic whenever you need repeatable counts — CI and
tools/drive_engine.py rely on it.
The engine speaks only its own tag bus; every PLC protocol lives in the Python
sidecar. connect attaches to a running engine — that is the one to use with
the 3D view. (demo starts its own headless Python scene instead, which is
for checking a driver with no Godot in the picture.) The F5 Driver dialog
runs these for you and copies the command.
From the download, run each one in the folder FactoryForge was extracted to,
starting it with .\factoryforge-sidecar (./factoryforge-sidecar on Linux)
in place of the Python command, and without the cd.
From a source checkout:
cd sidecar
# Siemens PLCSIM Advanced (Shared Memory API — Zero Licence Cost)
python -m factoryforge_sidecar connect --driver plcsim-advanced -o instance Sorting_PLC
# Siemens S7 ISO-on-TCP (Snap7)
python -m factoryforge_sidecar connect --driver s7-snap7 -o host 192.168.1.20 -o db 1
# OPC UA Client (connecting to an S7-1500 @ 192.168.1.20)
python -m factoryforge_sidecar connect --driver opcua-client \
-o url opc.tcp://192.168.1.20:4840 --mapping io_mapping.json
# OPC UA Server (exposing the scene to Node-RED / SCADA)
python -m factoryforge_sidecar connect --driver opcua-serverBuilding your own scene? Name your parts in the inspector, then F4 → Export
writes io_mapping.json and io_tags.csv for the tags that scene actually has.
See Getting Started.
| Document | Description |
|---|---|
| 🚀 GETTING_STARTED.md | Your first hour with OpenPLC, then PLCSIM Advanced, TIA Portal & Node-RED |
| 🎯 GRADING.md | Marking a PLC program against a scene, headless |
| 🔓 OPENPLC.md | Driving FactoryForge from OpenPLC over Modbus TCP: the reference behind the first hour |
| 📝 examples/README.md | Starter programs for every graded scene |
| 🛠️ PART_AUTHORING.md | Guide & template for building custom 3D factory components |
| 🔌 DRIVER_AUTHORING.md | Guide for adding custom Python protocol drivers |
| ✅ TEST_PLAN.md | What is tested, what is not, and the last run's results |
| 📦 PACKAGING.md | Building and gating a distributable release |
| 🧭 IMPROVEMENT_PLAN_v1.2.md | The v1.2 work list: polish and reach (the shipped v1.1 plan is in docs/history/) |
| 🔨 HARDENING_PLAN.md | The closed hardening plan and the release gate inside it |
| 🗺️ ROADMAP.md | Milestone completion tracking |
| 📑 PRD.md | Problem statement, target audience, and success criteria |
| ⚡ tag-bus.md | WebSocket tag bus protocol specification |
| 🤖 AGENTS.md | Developer cheat sheet, paths, and hardware gotchas |
| 📚 docs/history/ | Thirteen completed plan documents — why each piece was built the way it was. History, not instructions |
Nobody outside the project has contributed yet, so CONTRIBUTING.md is written to tell you what it actually costs rather than to sell you on it. The short version: a driver is cheap — one Python module, no Godot and no C# — and a part is not, because it still touches eight shared files besides its own class. That is a design problem, it is written down, and HP-34 in the hardening plan is the work to reduce it to one file plus a catalog entry.
Issue templates for a bug, a part and a driver. Security issues go through SECURITY.md, not the issue tracker — it also lists what is already known, including a Modbus bind default that is wrong today and being fixed under HP-22. Behaviour here: CODE_OF_CONDUCT.md.
Distributed under the MIT License. See LICENSE for more information.
FactoryForge is developed by Mahamed Algaroshy (محمد الجروشي).

