Files
plast-track/README.md
2026-06-16 22:53:38 +01:00

456 lines
11 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
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
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.
### 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