# 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 ## Repository Layout ```text /backend /frontend /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` - 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/` - `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://localhost:5173,http://127.0.0.1:5173 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 ``` 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 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 ``` 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 ``` 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 ``` ### 3. Deploy Click `Deploy the stack`. Expected exposed ports: - frontend: `5173` - 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 ``` 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` 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 The 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. ## 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 ``` ## 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 simulator models 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