496 lines
12 KiB
Markdown
496 lines
12 KiB
Markdown
# 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
|
|
```
|
|
|
|
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
|
|
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
|
|
```
|
|
|
|
The reverse proxy must route:
|
|
|
|
```text
|
|
/api/* -> backend:8000
|
|
/ws/dashboard -> backend:8000
|
|
/* -> frontend:5173
|
|
```
|
|
|
|
### 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
|