# Plast Track MVP Lightweight MES/IoT MVP for supervising multi-brand injection molding machines using passive signal acquisition, MQTT event ingestion, operator input, and real-time web dashboards. ## Objective This MVP supervises production without Euromap dependency and without sending any command back to the press. It focuses on: - machine status visualization - automatic cycle counting - real cycle time capture - current production order tracking - produced quantity and scrap quantity - automatic downtime detection - operator downtime qualification - scrap declaration - daily OEE/TRS - workshop dashboard in real time ## Architecture ```text Passive machine signal / external sensor -> isolated relay / optocoupler / industrial DI module -> IoT gateway or edge service -> MQTT topic -> FastAPI backend -> PostgreSQL -> WebSocket / REST API -> React frontend dashboard ``` Services: - `database`: PostgreSQL with schema and seed data - `mqtt`: Mosquitto broker - `backend`: FastAPI API, MQTT consumer, downtime logic, OEE computation, WebSocket notifications - `edge-simulator`: publishes simulated machine events on MQTT - `frontend`: React + TypeScript operator/dashboard interface - `simulator-ui`: separate React + TypeScript test console for sending simulated events ## Repository Layout ```text /backend /frontend /simulator-ui /edge-simulator /database/init.sql /database/seed.sql /mqtt/mosquitto.conf /docker-compose.yml ``` ## Run Locally Prerequisites: - Docker Desktop or Docker Engine with Compose Start the full MVP: ```bash docker compose up --build ``` Exposed services: - frontend: `http://localhost:5173` - simulator UI: `http://localhost:5174` - backend API: `http://localhost:8000` - backend health: `http://localhost:8000/health` - PostgreSQL: `localhost:5432` - MQTT: `localhost:1883` ## Deploy With Portainer Use a Portainer `Stack` from the Git repository. Do not paste only the Compose file into the web editor, because the stack also needs these repository files: - `database/init.sql` - `database/seed.sql` - `mqtt/mosquitto.conf` - `backend/` - `frontend/` - `simulator-ui/` - `edge-simulator/` ### 1. Create The Stack In Portainer: 1. Go to `Stacks`. 2. Click `Add stack`. 3. Choose `Repository`. 4. Set repository URL: ```text https://gitea.b2i.business/masterdev/plast-track.git ``` 5. Set branch: ```text main ``` 6. Set Compose path: ```text docker-compose.yml ``` ### 2. Configure Stack Variables For a local server accessed directly by IP, replace `SERVER_IP` with the host IP: ```env POSTGRES_DB=plasttrack POSTGRES_USER=plasttrack POSTGRES_PASSWORD=change-this-password FRONTEND_ORIGIN=http://SERVER_IP:5173 FRONTEND_ORIGINS=http://SERVER_IP:5173,http://SERVER_IP:5174,http://localhost:5173,http://127.0.0.1:5173,http://localhost:5174,http://127.0.0.1:5174 BACKEND_PORT=8000 VITE_API_BASE_URL=http://SERVER_IP:8000 VITE_WS_URL=ws://SERVER_IP:8000/ws/dashboard VITE_ALLOWED_HOSTS=SERVER_IP,localhost,127.0.0.1 SIMULATOR_PORT=5174 SIMULATOR_API_BASE_URL=http://SERVER_IP:8000 SIMULATOR_ALLOWED_HOSTS=SERVER_IP,localhost,127.0.0.1 ``` For a domain with HTTPS and a reverse proxy, use: ```env POSTGRES_DB=plasttrack POSTGRES_USER=plasttrack POSTGRES_PASSWORD=change-this-password FRONTEND_ORIGIN=https://plasttrack.example.com FRONTEND_ORIGINS=https://plasttrack.example.com,https://simulator.plasttrack.example.com BACKEND_PORT=8000 VITE_API_BASE_URL=https://api.plasttrack.example.com VITE_WS_URL=wss://api.plasttrack.example.com/ws/dashboard VITE_ALLOWED_HOSTS=plasttrack.example.com SIMULATOR_PORT=5174 SIMULATOR_API_BASE_URL=https://api.plasttrack.example.com SIMULATOR_ALLOWED_HOSTS=simulator.plasttrack.example.com ``` Important: do not keep `localhost` in `VITE_API_BASE_URL` or `VITE_WS_URL` for a remote deployment. `localhost` would point to the operator's browser machine, not the Docker host. If Vite returns `Blocked request. This host is not allowed`, add the public frontend host to `VITE_ALLOWED_HOSTS` as a comma-separated value. If the backend host port conflicts with another service such as Portainer, change `BACKEND_PORT` instead of editing `docker-compose.yml`. For example: ```env BACKEND_PORT=8001 VITE_API_BASE_URL=http://SERVER_IP:8001 VITE_WS_URL=ws://SERVER_IP:8001/ws/dashboard SIMULATOR_API_BASE_URL=http://SERVER_IP:8001 ``` If the browser shows a CORS error, make sure the exact frontend origin is in `FRONTEND_ORIGINS`. Example: ```env FRONTEND_ORIGINS=https://plasttrack.sable.ynsdev.site,https://simulator.plasttrack.sable.ynsdev.site ``` If the frontend is served over HTTPS, the WebSocket URL must use `wss://`, not `ws://`. Browsers block insecure WebSockets from secure pages. Recommended same-domain reverse proxy variables: ```env FRONTEND_ORIGIN=https://plasttrack.sable.ynsdev.site FRONTEND_ORIGINS=https://plasttrack.sable.ynsdev.site,https://simulator.plasttrack.sable.ynsdev.site VITE_API_BASE_URL=https://plasttrack.sable.ynsdev.site VITE_WS_URL=wss://plasttrack.sable.ynsdev.site/ws/dashboard VITE_ALLOWED_HOSTS=plasttrack.sable.ynsdev.site,localhost,127.0.0.1 SIMULATOR_API_BASE_URL=https://plasttrack.sable.ynsdev.site SIMULATOR_ALLOWED_HOSTS=simulator.plasttrack.sable.ynsdev.site,localhost,127.0.0.1 ``` The main dashboard reverse proxy must route: ```text /api/* -> backend:8000 /ws/dashboard -> backend:8000 /* -> frontend:5173 ``` The simulator must use a separate link, not a tab inside the operator frontend: ```text https://simulator.plasttrack.example.com -> simulator-ui:5174 ``` If `SIMULATOR_API_BASE_URL` points to the main dashboard domain, that dashboard reverse proxy must also route `/api/*` to the backend. ### 3. Deploy Click `Deploy the stack`. Expected exposed ports: - frontend: `5173` - simulator UI: `5174` - backend API: `8000` - MQTT: `1883` - MQTT WebSocket listener: `9001` PostgreSQL is intentionally not exposed by default in the Compose file. It is only reachable by the backend on the internal Docker network. The stack builds small local images for `database` and `mqtt` so Portainer does not need to bind-mount individual files like `database/init.sql` or `mqtt/mosquitto.conf`. This avoids file-versus-directory mount errors in Portainer Git stacks. The backend receives PostgreSQL connection settings as separate variables (`POSTGRES_HOST`, `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD`) and builds the SQLAlchemy URL internally. This avoids broken database URLs when the password contains special characters. The backend also retries the database connection during startup to tolerate Portainer service/DNS startup delays. ### 4. Verify Open: ```text http://SERVER_IP:5173 ``` Open the simulator test console: ```text http://SERVER_IP:5174 ``` Check the backend: ```text http://SERVER_IP:8000/health ``` Expected response: ```json {"status":"ok"} ``` ### 5. Updating From Gitea After pushing new commits to Gitea: 1. Open the stack in Portainer. 2. Click `Pull and redeploy` or `Update the stack`. 3. Enable rebuild if Portainer asks, because `backend`, `frontend`, and `edge-simulator` are built from the repository. ## Demo Data Seeded machines: - `INJ-01` - Haitian Mars II - `INJ-02` - Engel e-victory - `INJ-03` - Arburg Allrounder Seeded production orders: - `OF-1001` running on `INJ-01` - `OF-1002` planned on `INJ-02` The edge simulator continuously publishes cycles and stop events for the demo machines. ## MQTT Topics And Payloads Topic pattern: ```text plasttrack/machines/{machine_code}/events ``` Raw signal event example: ```json { "machine_id": "INJ-01", "event_type": "digital_input_changed", "timestamp": "2026-06-14T10:32:15Z", "input_name": "cycle_signal", "input_value": true, "cycle_time_sec": 18.7, "source": "digital_input" } ``` The backend derives valid cycles from the configured signal edge and timing rules. It still stores normalized `cycle_completed` events if upstream integrations publish them directly. Automatic stop event example: ```json { "machine_id": "INJ-01", "event_type": "machine_stopped", "timestamp": "2026-06-14T10:35:20Z", "auto_detected": true } ``` ## REST API Machines: - `GET /api/dashboard` - `GET /api/machines` - `GET /api/machines/{machine_id}` - `GET /api/machines/{machine_id}/config` - `PATCH /api/machines/{machine_id}/config` - `GET /api/machines/{machine_id}/status` - `GET /api/machines/{machine_id}/events` - `GET /api/machines/{machine_id}/cycles` - `GET /api/machines/{machine_id}/downtimes` Production orders: - `GET /api/production-orders` - `POST /api/production-orders` - `POST /api/production-orders/{id}/start` - `POST /api/production-orders/{id}/pause` - `POST /api/production-orders/{id}/close` Downtimes: - `GET /api/downtimes/open` - `POST /api/downtimes/{id}/qualify` Scrap: - `GET /api/scraps` - `POST /api/scraps` OEE: - `GET /api/oee/daily?date=2026-06-14` - `GET /api/oee/machines/{machine_id}?from=2026-06-14T00:00:00Z&to=2026-06-14T23:59:59Z` Reference data: - `GET /api/reference-data` Simulator: - `POST /api/simulator/events` - `POST /api/simulator/scenarios` WebSocket: - `ws://localhost:8000/ws/dashboard` ## Manual API Checks List machines: ```bash curl http://localhost:8000/api/machines ``` Create a production order: ```bash curl -X POST http://localhost:8000/api/production-orders \ -H "Content-Type: application/json" \ -d '{ "order_number": "OF-2001", "machine_id": 2, "article_ref": "ART-2001", "article_name": "Bac Technique", "mold_ref": "M-2001", "material_ref": "PP-BLACK", "planned_qty": 1200, "cavities": 2, "theoretical_cycle_time_sec": 21.5 }' ``` Qualify a downtime: ```bash curl -X POST http://localhost:8000/api/downtimes/1/qualify \ -H "Content-Type: application/json" \ -d '{ "reason_code": "attente_matiere", "comment": "Material not available at the machine", "qualified_by": "operateur_1" }' ``` Declare scrap: ```bash curl -X POST http://localhost:8000/api/scraps \ -H "Content-Type: application/json" \ -d '{ "machine_id": "INJ-01", "production_order_id": 1, "quantity": 12, "reason_code": "bavure", "comment": "Flash on parting line", "operator_name": "operateur_1" }' ``` ## OEE Logic Formula: ```text OEE = Availability x Performance x Quality ``` Implemented rules: - planned time: overlap of started production orders with the query window - downtime: overlap of machine downtimes with the query window - operating time: `planned time - downtime` - performance: `theoretical cycle contribution / operating time`, capped at `100%` - quality: `good quantity / total produced quantity` ## How The Simulator Works There are two simulator components: - `edge-simulator`: automatic MQTT publisher for continuous demo data - `simulator-ui`: separate browser console for manually triggering test cases The edge simulator publishes: - `digital_input_changed` for `machine_power_on` - `digital_input_changed` for `cycle_signal` - `digital_input_changed` for `general_alarm` Each machine gets a nominal cycle time, a pulse width, and a random stop probability. The backend consumes these MQTT events, detects the configured cycle edge, and updates production data in PostgreSQL. The separate simulator UI calls backend simulator endpoints and can trigger: - power on/off - single normalized cycle - cycle burst - passive `cycle_signal` edge using each machine's configured edge - debounce noise that should be ignored - forced stop / open downtime - recovery with a new cycle - alarm on/off - raw passive digital input by name and value Use the simulator UI through its own URL, for example: ```text http://localhost:5174 https://simulator.plasttrack.example.com ``` ## Connecting A Real Passive Gateway Later To replace the simulator with a real gateway: 1. Read passive, electrically isolated machine signals only. 2. Publish normalized JSON events to the same MQTT topic pattern. 3. Keep event timestamps in UTC ISO-8601 format. 4. Map per-machine debounce, min/max cycle time, and stop detection delay in the database or future admin UI. Expected minimum signals: - `machine_power_on` - `cycle_signal` Optional signals: - `auto_mode` - `mold_open` - `ejector_forward` - `general_alarm` - `pump_running` ## Industrial Safety - Never wire the IoT gateway directly to PLC outputs or machine circuits. - Use interface relays, optocouplers, or isolated industrial input modules. - Validate every wiring decision with a qualified automation or industrial electrical technician. - Keep acquisition passive and non-intrusive. - Do not modify the machine PLC logic for this MVP. - This MVP must never command or pilot the press. ## Tests Backend tests included: - OEE calculation unit test - MQTT event parsing test Run locally: ```bash cd backend pytest ``` Frontend build check: ```bash cd frontend npm install npm run build ``` Simulator UI build check: ```bash cd simulator-ui npm install npm run build ``` ## Current Limits - No authentication or role management - No production scheduling calendar - No historian-grade buffering on the edge side - No historical reporting endpoints beyond daily OEE/TRS - No real passive gateway adapter package yet; the simulator publishes normalized MQTT payloads - WebSocket currently pushes refresh notifications, then the UI refetches data - The simulators model passive-signal-derived events, not actual electrical IO reads ## Current UI Modules The frontend is organized into module tabs rather than one long dashboard page: - `Atelier`: machine cards, status filtering, sorting, quick actions, fullscreen workshop mode - `Machine`: selected machine detail, cycle trend, recent events, downtime history - `TRS`: daily OEE/TRS summary and per-machine OEE - `OF`: production order creation and status management - `Arrets`: open downtime qualification - `Rebuts`: scrap declaration and scrap log - `Config`: persistent passive signal and timing configuration per machine Operator forms include inline validation, inline API error feedback, and success toasts. ## Industrial Readiness Backlog Next hardening steps before a real shop-floor pilot: - define a real passive gateway adapter spec with exact input mapping and MQTT payload examples - add edge-side buffering for MQTT/backend outage periods - expand tests for downtime transitions, order status transitions, and signal debounce edge cases - add historical reporting endpoints for OEE, downtime, and scrap trends