Running FactorySemantics MES beside your existing MES¶
A guide for plant engineers. One afternoon, in order, with a controls person and an IT person. No Python required, and nothing here changes anything on your plant.
You have an MES already. This one goes beside it: it watches the same machines through the same OPC UA server, computes states, downtime, OEE and production from what it sees, and writes nothing back. After a week you can compare the two and decide whether this one is worth more of your time.
If it turns out not to be, you stop the process and untrust one certificate, and nothing on your plant has changed.
What you will need before you start¶
| From whom | What |
|---|---|
| IT / whoever administers the OPC UA server | the endpoint URL, confirmation the OPC UA interface is on, the security policy, a read-only account, and one certificate trusted. There is a page to hand them. |
| Engineering / whoever knows the machines | one CSV row per machine: four tag addresses, what the state values mean, and the rated cycle time. There is a page to hand them too, with a template. |
| You | a PC that can reach the OPC UA server, with Python 3.12 or 3.13 on it. A plant PC or a small VM is plenty. |
| Your ERP team, only if you want orders | a folder both systems can reach, and a file per schedule. Step 5. |
Send the two guides out first; they are the long pole. Everything else is an afternoon.
If you would rather be walked through it. This page, and the install and
plant-shape pages either side of it, also ship as a folder you hand to whatever
AI assistant you already use — fsmes-assistant.zip on the release page, or
fsmes skills unpack assistant ./assistant out of an installation. You run the
commands and paste back what they print; it needs no access to this machine and
it asks you for no plant data. Stage 3 of it is this page, as a script.
The afternoon, in order¶
| # | Step | Budget |
|---|---|---|
| 1 | What this guarantees | 10 min, reading |
| 2 | Install it and see it run | 15 min |
| 3 | Get read-only access to the OPC UA server | 20 min, plus their change window |
| 4 | Browse, map, and prove the map | 1–2 hours, and it is the part that matters |
| 5 | Orders in, confirmations out, by file | 30 min, optional |
| 6 | What people type into other systems | 5 min, reading |
| 7 | First-day checks, and week one | 20 min, then a week |
| 8 | Where the numbers live, and backing them up | 20 min |
Those are budgets to plan an afternoon around, not measurements. Step 4 is the one that decides whether any number this MES ever reports is worth reading; the rest is plumbing.
1. What this guarantees, and what it does not¶
It does not write to your PLCs. A tag map generated from the worksheet marks every machine read-only, and with no write target configured there is no code path that writes a tag. Ask for a read-only OPC account as well — then it is your server enforcing it, not this software's good intentions.
It does not change your OPC server's project. No configuration API, no tags added, no users of ours. The only thing that persists on their side is one trusted certificate, and untrusting it cuts the access cleanly.
It does not talk to your ERP unless you tell it to. MES_ERP_MODE=off
reaches nothing; MES_ERP_MODE=file reads and writes folders you name and
opens no connection to any live system. Step 5 sets that up deliberately.
It does not publish to a broker unless you tell it to. MES_UNS_MODE is
off by default.
As of 2026-09-10 those are four separate settings, each checked separately.
Check all four before you point this at a real line, and check them again
after anyone edits .env.
What it does not guarantee: it still needs to read your OPC server, so that server must permit a read-only client and will carry one more session. And it is pre-alpha software that has never run a real plant in production — which is why it runs beside yours and not instead of it.
2. Install it and see it run¶
Nothing plant-specific yet. Prove the software works on the machine before you point it at anything real. No checkout, no clone, no Docker: the package carries two complete example plants inside it.
pipx install factorysemantics-mes
fsmes info
fsmes demo bottling
pipx install factorysemantics-mes # or: uv tool install factorysemantics-mes
fsmes info
fsmes demo bottling
fsmes demo bottling brings up a six-station beverage line the way a plant
runs: an eight-hour shift replayed over a real OPC UA server, a simulated
floor booking lots and naming its stops, and the operator screens reading it.
It prints the address — http://127.0.0.1:8010/dashboard — and an account to
sign in with, and it keeps running until fsmes demo bottling stop.
fsmes demo bottling status says whether it is up.
Run it on your own box is one page of what trips
people in this first hour on a machine of their own: fsmes not on PATH after
a py -m pip install, and a plant left alive but silent.
Done looks like: that dashboard on screen, with the six machines on the
floor plan and a shift's worth of readings behind them. First start takes a
little longer than later ones: it copies the pack out of the package and
generates the shift from the seed in its line.json, which is seconds.
Or, before step 3: a simulated plant shaped like yours¶
The bottling line is our plant, not yours, and some people would rather see their own machine count, their own stop mix and their own tags before they ask IT for anything. There is a path for that, and it needs nothing to leave the building: the same install writes an assistant folder out of itself, three commands run on your MES box with a plain python, and one command here turns the one file they write into a running plant.
fsmes skills unpack plant-from-your-mes ./assistant
# ... three commands on the MES box, which needs no fsmes ...
fsmes demo from-shape ./plant-shape.toml --name acme
The whole path, with PowerShell and bash side by side, is in your plant without sending anyone your data. It is a simulation shaped like your plant, not your plant — steps 3 and 4 below are still how real readings get in.
Everything it writes — the pack copy, the database, the logs, the generated
line data — goes to one per-user directory: %LOCALAPPDATA%\fsmes on
Windows, $XDG_DATA_HOME/fsmes or ~/.local/share/fsmes on Linux,
~/Library/Application Support/fsmes on macOS, or wherever FSMES_HOME says.
Nothing under the installed package is written to, so a locked-down plant PC
is fine. fsmes demo machining is a second example plant, on 8020.
That plant is for evaluation: it listens on loopback and its accounts are the example pack's, with their passwords in the open. Step 4 is where you draft a pack for your own plant; deploy is where one that is meant to stay up gets a service and a real secret key.
fsmes demo with no plant name is the shorter, older check: a whole
simulated line in one process for about ninety seconds — two stations over a
real OPC UA server, one order released, booked and completed — printing
Demo result: full loop closed - order booked, ERP confirmed, OEE reported.
at the end. If that line does not appear, stop here; nothing later will work
either.
Database: SQLite is the default and needs no setup — one file, and it is
fine for one line. Use PostgreSQL when you have more than one plant on the
box, or when your IT department wants the backups in their existing regime:
set MES_DATABASE_URL=postgresql+psycopg://user:password@host:5432/dbname
and install with the postgres extra. Both are tested; see
compatibility.
Your plant's own configuration is one directory, a
plant pack: what this plant is called, what clock it keeps, which
modules it serves, what it calls things, and the files that describe its
machines and its master data. fsmes pack check reads it offline and refuses
a typo instead of defaulting past it, and it holds no password, so it is safe
to send to whoever you are asking for help. Step 4 is where you draft one.
Running more than one plant from one installation is
the fleet, which builds each one from its pack and will only ever
touch the plants it built.
Everything the MES is configured with is an environment variable starting
MES_, and it reads a .env file in the directory you run it from. There
is no other config format to learn.
Two settings every plant sets. Put them in .env before anything else:
MES_PLANT_PROFILE=plant
MES_PLANT_NAME=northgate
MES_PLANT_TIMEZONE=America/Chicago
MES_PLANT_NAME is what this plant is called, and it appears on every screen,
in /health, on every metric, in every backup and on every namespace event —
so somebody looking at two plants can tell which one they are looking at. It
may hold letters, digits and _ . : @ = -, because it is published as one
segment of the namespace topic. Off the laptop profile the MES refuses to
start without it.
MES_PLANT_TIMEZONE is the zone this plant works in, as an
IANA name. It
decides where a shift starts, when a gauge falls due, and what time every
screen shows. Leave it out and the MES uses the server's own zone and says
"defaulted" everywhere it reports it — which is correct if the server sits in
the plant, and wrong the day somebody moves it to a data centre. On Windows
the zone database comes from the tzdata package (pip install tzdata).
MES_PLANT_PROFILE is laptop, plant or fleet. laptop is the
evaluation profile — fsmes demo and the labs — and needs nothing set.
3. Get read-only access to the OPC UA server¶
Send the access request page to whoever administers the server. It is written to be forwarded as-is and asks for five things.
Put their answers in .env:
MES_OPC_ENDPOINT=opc.tcp://their-server:49320
MES_OPC_SECURITY=Basic256Sha256,SignAndEncrypt
MES_OPC_USER=the-read-only-account
MES_OPC_PASSWORD=...
MES_TAG_MAP_FILE=config/tag_map_plant.json
Expect the first connection to be rejected. That is the certificate handshake working: their server has never seen your client's certificate, so it refuses it and files it under rejected certificates — which is how the certificate reaches their side. They trust it once; the second attempt succeeds.
Done looks like: fsmes opc-browse --depth 1 prints the server's
namespace table instead of a certificate error.
The details, the failure symptoms and what the subscription costs their server are on Connecting read-only to an OPC UA server you do not own.
4. Browse, map, and prove the map¶
Look at what the server actually exposes:
fsmes opc-browse --contains Filler
Turn Engineering's worksheet into the tag map:
fsmes make-tag-map worksheet.csv --out config/tag_map_plant.json
It refuses anything it could not honestly interpret, and prints a note for every blank that has a consequence — no scrap counter means quality reads unknown, no rated cycle time means performance reads unknown. It never fills a blank with a default that looks like a measurement.
Prove it against the live server, while the line is running:
fsmes opc-verify --seconds 15
Done looks like: PASS: every mapped tag is readable and every observed
State value maps.
Then give the MES its equipment and start it:
fsmes init-db
fsmes seed-line LINE1 --line-name "Filling Line 1"
fsmes run-opc-agent # subscribes and records
fsmes run-api # dashboards at http://127.0.0.1:8000/dashboard
The decision that matters is in the worksheet, not in any command: which
state values count as down. A changeover mapped to down wrecks every
availability figure this system will ever report, and nobody will be able to
say why afterwards. Sit with the person who runs the line for that column.
Full detail: Connecting read-only to an OPC UA server you do not own.
5. Orders in, confirmations out, by file¶
Optional, and worth doing: without orders the MES reports states, downtime and trends but books no production against anything.
The simplest interface, and the one that needs no access to your ERP, is a folder. Set:
MES_ERP_MODE=file
MES_ERP_INBOX=erp_exchange/inbox
MES_ERP_OUTBOX=erp_exchange/outbox
MES_ERP_ARCHIVE=erp_exchange/archive
fsmes erp requirements # what this connector needs — the list to send your ERP team
fsmes erp setup # makes the folders, idempotent
fsmes erp check # says whether it would work, in plain language
fsmes run-erp-sync # reads the inbox, writes the outbox, forever
Orders in. Drop a file in the inbox — either B2MML-lite
ProductionSchedule XML, or plain JSON, one order or a list:
[{"code": "WO-10041", "material": "FG-LINE1", "quantity": 1200,
"due_date": "2026-09-12T06:00:00", "priority": 20}]
Read files move to the archive. A file nobody could parse keeps its name with
.rejected on the end and stays where you can see it, rather than
disappearing.
Confirmations out. One ProductionPerformance XML per confirmation lands
in the outbox, named for the order:
erp_exchange/outbox/confirmation_WO-10041_op10_20260910-141233.xml
erp_exchange/outbox/confirmation_WO-10041_completion_20260910-152551.xml
One per operation as it completes, and one for the order. Your ERP team collects them; nothing here deletes them.
Done looks like: fsmes erp check says Ready., an order file you drop
appears on the dashboard, and a confirmation file appears in the outbox when
the line makes something.
There is also a live ERPNext connector if that is your ERP — but for a first shadow run beside a system already in charge, files are the right choice: two systems confirming into one ERP is a problem you do not need this week.
6. What people type into other systems¶
Downtime reasons, scrap tickets, manual counts, changeover notes — the things people type into a terminal, a spreadsheet or the incumbent MES.
There is a second front door for those, and it is its own page: three typed shapes — a downtime label, a quality result, a count — each carrying the name of the system that supplied it, its key and its own recording time. They arrive as files dropped in a folder, or on an MQTT broker where the plant already has one.
Until something feeds it, the downtime pareto reads almost entirely unlabelled in week one: nothing on an OPC-fed line labels a stop, and there is nowhere else to learn it from. The pareto names that bar unlabelled rather than filing it under "other", because a chart that hides its own ignorance convinces a plant it has data it does not have. Once the labels are arriving, the same pareto splits every bucket by who named it — this MES or the system that told it.
There is no SQL poller and no inbound REST endpoint yet. If you need one, say so in Discussions. There is a second front door for those, and in a shadow run you want it open. Nothing on an OPC-fed line labels a stop, so without it the downtime pareto reads almost entirely unlabelled in week one — while the incumbent beside it shows reason codes, and the comparison is unfair by construction. Two ways in, both configuration rather than code: a CSV or JSON export dropped in a folder, or a read-only query against the database the other system already keeps. Either way the fact is recorded as told, with the supplying system named on it, and never as something this MES watched.
The pareto still names its unlabelled bar unlabelled rather than filing it under "other", because a chart that hides its own ignorance convinces a plant it has data it does not have — and it now breaks every bucket down by who labelled it, so what came from the incumbent can be told from what came from here.
Feeding the MES what people typed elsewhere is the page. Start with the folder driver — a person can produce one export by hand on day one — and move to the query once somebody will give you read-only credentials. A question asked twice becomes a page, and a need stated by a real plant moves up the list.
7. First-day checks, and week one¶
On the first day, in this order:
fsmes info # the version you think you are running
fsmes opc-verify # every mapped tag still readable
fsmes erp check # only if you did step 5
fsmes doctor # one file to paste when something is wrong
Run fsmes doctor once on the first day even if nothing is wrong, and read
it: it is how you will ask for help later, and reading it now is how you
satisfy yourself that nothing of your plant's is in it —
one file to paste when something is wrong.
Then open /dashboard and look at the line, /dashboard/analysis for the
shift, and /dashboard/line for one line's machines, its work in progress
and its state timeline, with the 3D view as a tab.
Three things will look wrong on day one and are correct: OEE says unknown rather than a number, the downtime pareto is nearly all unlabelled, and a machine with no scrap counter shows no quality figure. What to expect in the first week explains each, lists the four mistakes that really are yours to fix, and — the useful part — says what is worth comparing against the system already in charge and what is not.
Beside every figure is a coverage number: how much of that window this
MES actually watched. On day one it is small and climbing, which is the
truth. fsmes oee explain <machine> 8h prints every second of a window and
where the unwatched ones went — how much of the window did the MES
see.
Run the agent under a supervisor from the first day. The window it was not watching is reported as unknown and is never reconstructed, by design; deploy has the systemd units and the Compose file.
At the end of the week, if you did step 5, put the two records side by side. Export the same period's bookings from the MES in charge and run the shadow scorecard: it reports, per order and per operation, where the two agree and where they do not, and — the part that makes it worth trusting — what it could not compare.
8. Where the numbers live, and backing them up¶
Everything the MES recorded is in one database — fsmes.db beside where you
run it, unless you pointed MES_DATABASE_URL somewhere else. Everything it
was told is in .env, the tag map, and the certificate directory.
fsmes backup --out /mnt/somewhere-else
That copies the database, the tag map and the OPC client certificate, states its totals, and names what it did not copy and why. It is safe to run while the plant is running. The certificate is the one worth caring about: losing it means going back to the server's owner for another trust step and another change window.
It deliberately does not copy .env, which holds your OPC password — keep
that wherever this plant already keeps secrets.
Read backup and restore for the schedule, and for how to prove a backup actually restores, which is the only test that counts.
Two things about SQLite worth knowing, and neither needs you to do
anything. SQLite has one writer. A screen reading a KPI no longer stands in
that queue and no longer makes anybody else stand in it, so a busy dashboard
cannot stop the plant recording what it made. If a write does have to wait,
it waits MES_SQLITE_BUSY_TIMEOUT_MS — fifteen seconds by default, which
nothing on a healthy plant reaches. If one ever gives up, the log says so in
one line naming the machine and how many units are still owed; those units
are not lost, and they arrive with the next reading from that machine,
because a machine's counter is absolute. You only need that setting if you
see the line — and then the number is a symptom rather than the cure, so
bring the line to a discussion rather than raising it and moving on.
The plant's own log, logs/<plant>/plant.log, is capped: 50 MB a file, four
files kept, 200 MB and no more. A plant left running for a month cannot fill
the disk with what it said about itself. Set MES_LOG_LEVEL=DEBUG when you
want the whole stack behind a failure; leave it alone the rest of the time.
If you put this on the plant network rather than one PC, put TLS in front of it: the MES does not terminate TLS itself, and TLS in front of the API has a Caddy config and an nginx config, one each.
Upgrading later is its own page, including the one case — a PyPI install upgrading across a schema change — that this project cannot migrate for you yet, written down rather than papered over.
Who to ask¶
GitHub Discussions for questions and ideas, issues for bugs. One maintainer, answering in days rather than hours; that is stated up front rather than discovered.
Never put real plant data in either. No tag names from your site, no screenshots of your dashboards, no order numbers, no company or site names. Say what happened and what you expected; that is enough to help you, and it keeps the project usable by people whose employers would not let them otherwise.
Every page this guide leans on¶
- Connecting read-only to an OPC UA server you do not own
- IT access request · Engineering worksheet
- What to expect in the first week
- The shadow scorecard
- Backup and restore · TLS in front of the API · Upgrading
- Deploy · Security · Compatibility
- Reading OEE · Never invent production
- How much of the window did the MES see
- Your plant without sending anyone your data · Plant packs