[#248] setup makefile for dev #1

Merged
matthieu merged 2 commits from chore/makefile-env into develop 2026-01-13 17:57:38 +00:00
6 changed files with 261 additions and 87 deletions

8
.env.example Normal file
View File

@@ -0,0 +1,8 @@
APP_MODE=mock
APP_HOST=0.0.0.0
APP_PORT=8000
SERIAL_PORT=/dev/ttyUSB0
SERIAL_BAUDRATE=9600
SERIAL_TIMEOUT_S=1.0
SERIAL_OPEN_DELAY_S=2.0
SERIAL_POST_WRITE_DELAY_S=0.5

31
.githooks/commit-msg Executable file
View File

@@ -0,0 +1,31 @@
#!/usr/bin/env bash
set -euo pipefail
MSG_FILE="${1}"
FIRST_LINE="$(head -n 1 "$MSG_FILE" | tr -d '\r')"
# Autoriser commits auto-generees par git
if [[ "$FIRST_LINE" =~ ^Merge\ ]]; then
exit 0
fi
# Types autorises (minuscules uniquement)
# Optionnel: scope => feat(auth) : ...
REGEX='^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9._-]+\))?\ :\ .+'
if [[ ! "$FIRST_LINE" =~ $REGEX ]]; then
echo "❌ Message de commit invalide."
echo ""
echo "➡️ Format attendu : <type>(<scope optionnel>) : <message>"
echo "➡️ Types autorises (minuscules uniquement) :"
echo " build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test"
echo ""
echo "✅ Exemples :"
echo " feat : add login page"
echo " fix(auth) : prevent null token crash"
echo " docs : update README"
echo ""
echo "❌ Exemple refuse :"
echo " Feat : add login page"
exit 1
fi

93
Makefile Normal file
View File

@@ -0,0 +1,93 @@
SHELL := /usr/bin/env bash
PYTHON ?= python3
VENV_DIR ?= .venv
PIP := $(VENV_DIR)/bin/pip
UVICORN := $(VENV_DIR)/bin/uvicorn
.PHONY: help venv install env run start stop restart run-mock run-serial reset clean
help:
@echo "Targets:"
@echo " venv - create virtual environment in $(VENV_DIR)"
@echo " install - install Python dependencies"
@echo " env - create .env from .env.example if missing"
@echo " run - run API (uses .env)"
@echo " start - start API in background (PID file: .uvicorn.pid)"
@echo " stop - stop API using PID file"
@echo " restart - stop then start API"
@echo " run-mock - run API with APP_MODE=mock"
@echo " run-serial - run API with APP_MODE=serial"
@echo " reset - remove virtual environment and .env"
venv:
$(PYTHON) -m venv $(VENV_DIR)
install: venv env
$(PIP) install --upgrade pip
$(PIP) install -r requirements.txt
env:
@bash -c 'set -euo pipefail; \
if [ ! -f .env ]; then \
cp .env.example .env; \
echo ".env created from .env.example"; \
else \
changed=0; \
while IFS= read -r line; do \
case "$$line" in \
""|\#*) continue ;; \
esac; \
key="$${line%%=*}"; \
if ! grep -qE "^$${key}=" .env; then \
echo "$$line" >> .env; \
changed=1; \
fi; \
done < .env.example; \
if [ "$$changed" -eq 1 ]; then \
echo ".env updated with missing variables"; \
else \
echo ".env up to date"; \
fi; \
fi'
run:
bash -c 'set -a; [ -f .env ] && . ./.env; set +a; $(UVICORN) app.main:app --host "$${APP_HOST:-0.0.0.0}" --port "$${APP_PORT:-8000}"'
start:
@bash -c 'set -a; [ -f .env ] && . ./.env; set +a; \
if [ -f .uvicorn.pid ] && kill -0 "$$(cat .uvicorn.pid)" 2>/dev/null; then \
echo "API already running (PID $$(cat .uvicorn.pid))"; \
exit 0; \
fi; \
nohup $(UVICORN) app.main:app --host "$${APP_HOST:-0.0.0.0}" --port "$${APP_PORT:-8000}" >/dev/null 2>&1 & \
echo $$! > .uvicorn.pid; \
echo "API started (PID $$(cat .uvicorn.pid))"'
stop:
@bash -c 'set -euo pipefail; \
if [ ! -f .uvicorn.pid ]; then \
echo "No PID file (.uvicorn.pid)."; \
exit 0; \
fi; \
pid="$$(cat .uvicorn.pid)"; \
if kill -0 "$$pid" 2>/dev/null; then \
kill "$$pid"; \
echo "API stopped (PID $$pid)"; \
else \
echo "Process not running (PID $$pid)"; \
fi; \
rm -f .uvicorn.pid'
restart: stop start
run-mock:
bash -c 'set -a; [ -f .env ] && . ./.env; set +a; APP_MODE=mock $(UVICORN) app.main:app --host "$${APP_HOST:-0.0.0.0}" --port "$${APP_PORT:-8000}"'
run-serial:
bash -c 'set -a; [ -f .env ] && . ./.env; set +a; APP_MODE=serial $(UVICORN) app.main:app --host "$${APP_HOST:-0.0.0.0}" --port "$${APP_PORT:-8000}"'
reset:
rm -rf $(VENV_DIR)
rm -f .env .uvicorn.pid
clean: reset

160
README.md
View File

@@ -1,15 +1,15 @@
# Pont Bascule Connector (Raspberry Pi) — FastAPI + Serial + Tailscale
# Pont Bascule Connector — FastAPI + Serial + (optionnel) Tailscale
API HTTP (FastAPI) qui pilote un pont bascule connecté en USB (port série) sur Raspberry Pi.
API HTTP (FastAPI) qui pilote un pont bascule connecté en USB (port série) sur Raspberry Pi ou Linux.
**Objectif :** permettre à une application/serveur distant d'interroger le pont bascule via réseau (Tailscale), avec une contrainte stricte : **1 requête série à la fois**.
**Objectif :** permettre à une application/serveur distant d'interroger le pont bascule via réseau, avec une contrainte stricte : **1 requête série à la fois**.
---
## Fonctionnement global
```
Client (PC / serveur / app) --HTTP--> Raspberry Pi (FastAPI)
Client (PC / serveur / app) --HTTP--> Machine (FastAPI)
|
| 1 appel à la fois (lock)
v
@@ -19,58 +19,78 @@ Client (PC / serveur / app) --HTTP--> Raspberry Pi (FastAPI)
Pont bascule
```
**Accès distant :**
**Accès distant (optionnel) :**
- via IP Tailscale `100.x.x.x` (VPN mesh)
- optionnellement via `tailscale serve` pour exposer l'API sur le port 80 sans `:8000`
- ou autre VPN / reverse-proxy selon votre infra
---
## Prérequis
### Raspberry Pi
- Raspberry Pi OS (Lite recommandé)
- Python 3
- Accès SSH
- Tailscale installé et connecté
### Matériel
- Pont bascule branché en USB (ou via adaptateur USB↔RS232/RS485 selon le matériel)
### Système
- Raspberry Pi OS / Debian / Ubuntu (autres Linux OK)
- Python 3.9+ recommandé
- Accès SSH
- (optionnel) Tailscale installé et connecté
---
## Installation (Raspberry Pi)
## Installation
### 1) Récupérer le projet
```bash
cd ~
git clone <URL_DE_TON_REPO> pont-bascule-connector
git clone gitea@gitea.malio.fr:MALIO-DEV/pont-bascule-connector.git
cd pont-bascule-connector
```
### 2) Environnement Python
Deux options :
#### Option A : venv global (recommandé si déjà en place)
### 2) Installer les dépendances système (Debian/Ubuntu/Raspberry Pi OS)
```bash
python3 -m venv /home/malio/venv
source /home/malio/venv/bin/activate
sudo apt update
sudo apt install -y python3 python3-venv python3-pip git
```
Si vous êtes sur un autre OS, installez Python 3 + pip + venv via votre gestionnaire de paquets.
### 3) Installer les dépendances Python
```bash
python3 -m venv ./.venv
source ./.venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt
```
#### Option B : venv dans le projet
---
## Hook git (commit-msg)
Le repo contient un hook pour valider le format des messages de commit.
Activation :
```bash
python3 -m venv ./venv
source ./venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt
git config core.hooksPath .githooks
```
### 3) Configuration série (.env)
Format attendu :
```text
<type>(<scope optionnel>) : <message>
```
Types autorisés : `build`, `chore`, `ci`, `docs`, `feat`, `fix`, `perf`, `refactor`, `revert`, `style`, `test`.
---
## Configuration série (.env)
Le fichier `.env` est chargé automatiquement au démarrage (via `python-dotenv`).
Créer un fichier `.env` à la racine du projet :
@@ -79,9 +99,12 @@ cd ~/pont-bascule-connector
nano .env
```
Exemple :
Exemple (mode reel / port serie) :
```env
APP_MODE=serial
APP_HOST=0.0.0.0
APP_PORT=8000
SERIAL_PORT=/dev/ttyUSB0
SERIAL_BAUDRATE=9600
SERIAL_TIMEOUT_S=1.0
@@ -89,16 +112,28 @@ SERIAL_OPEN_DELAY_S=2.0
SERIAL_POST_WRITE_DELAY_S=0.5
```
Exemple (mode mock pour dev, sans port serie) :
```env
APP_MODE=mock
APP_HOST=0.0.0.0
APP_PORT=8000
```
**Notes importantes :**
- `APP_MODE=serial` (defaut) utilise le port serie ; `APP_MODE=mock` simule les reponses pour le dev.
- `APP_HOST` et `APP_PORT` permettent de changer l'interface d'ecoute et le port HTTP.
- `SERIAL_OPEN_DELAY_S=2.0` et `SERIAL_POST_WRITE_DELAY_S=0.5` reproduisent le comportement du script Tkinter historique :
- attente 2s après ouverture du port
- envoi trame
- attente 0.5s
- lecture une seule fois de `in_waiting`
- Si ton port est `/dev/ttyACM0`, adapte `SERIAL_PORT`
- Si votre port est `/dev/ttyACM0`, adaptez `SERIAL_PORT`
### 4) Droits port série (dialout)
---
## Droits port série (dialout)
Vérifier les devices :
@@ -108,19 +143,21 @@ ls /dev/ttyACM* 2>/dev/null || true
dmesg | tail -n 30
```
Ajouter l'utilisateur au groupe `dialout` :
Ajouter l'utilisateur courant au groupe `dialout` :
```bash
sudo usermod -aG dialout malio
sudo reboot
sudo usermod -aG dialout "$USER"
newgrp dialout
```
Si besoin, reconnectez-vous (ou redémarrez) pour que les droits prennent effet.
---
## Lancer l'API (mode manuel)
```bash
source /home/malio/venv/bin/activate # ou ./venv/bin/activate
source ./.venv/bin/activate
uvicorn app.main:app --host 0.0.0.0 --port 8000
```
@@ -140,19 +177,19 @@ curl http://127.0.0.1:8000/health
sudo nano /etc/systemd/system/pont-bascule-api.service
```
Contenu (adapter les chemins si nécessaire) :
Contenu (adapter les chemins et l'utilisateur) :
```ini
[Unit]
Description=Pont bascule API (FastAPI)
After=network-online.target tailscaled.service
After=network-online.target
Wants=network-online.target
[Service]
User=malio
WorkingDirectory=/home/malio/pont-bascule-connector
EnvironmentFile=/home/malio/pont-bascule-connector/.env
ExecStart=/home/malio/venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000
User=<votre_user>
WorkingDirectory=/home/<votre_user>/pont-bascule-connector
EnvironmentFile=/home/<votre_user>/pont-bascule-connector/.env
ExecStart=/home/<votre_user>/pont-bascule-connector/.venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000
Restart=always
RestartSec=2
@@ -255,18 +292,18 @@ Si une requête est déjà en cours, l'API renvoie :
---
## Accès à distance via Tailscale
## Accès à distance via Tailscale (optionnel)
### 1) Vérifier Tailscale
Sur le Raspberry :
Sur la machine :
```bash
tailscale status
tailscale ip -4
```
Exemple : IP Tailscale du Pi `100.122.43.54`.
Exemple : IP Tailscale `100.122.43.54`.
### 2) Appeler l'API via Tailscale (simple)
@@ -277,7 +314,7 @@ curl -X POST http://100.122.43.54:8000/send/esclave
### 3) Option recommandée : exposer sans port avec `tailscale serve`
Sur le Raspberry :
Sur la machine :
```bash
sudo tailscale serve --http=80 localhost:8000
@@ -291,12 +328,6 @@ curl http://100.122.43.54/health
curl -X POST http://100.122.43.54/send/esclave
```
### 4) SSH via Tailscale
```bash
tailscale ssh malio@raspberrypi
```
---
## Dépannage rapide
@@ -330,7 +361,7 @@ groups
---
## Sécurité recommandée (à faire)
## Sécurité recommandée (optionnel)
1. **Exposer l'API uniquement via Tailscale :**
- faire écouter uvicorn en local seulement (`--host 127.0.0.1`)
@@ -342,29 +373,8 @@ groups
---
## Autres recommandations (vraiment utiles)
## Recommandations utiles
### 1) Sécuriser l'API
### Port série stable (udev)
Aujourd'hui tu exposes `0.0.0.0:8000` → accessible depuis le LAN.
Si tu veux "Tailscale only" (recommandé) :
- Dans systemd : `--host 127.0.0.1`
- Et tu actives `tailscale serve --http=80 localhost:8000`
### 2) Ajouter une route `/weight`
Tu m'envoies 1 exemple de `response_ascii` (exact) et je te code un parseur robuste qui sort :
```json
{
"weight": 12.34,
"unit": "kg",
"ticket": "000123",
"raw": "..."
}
```
### 3) Port série stable (udev)
Si ton port passe parfois de `/dev/ttyUSB0` à `/dev/ttyUSB1`, on peut créer une règle udev pour avoir un nom fixe, genre `/dev/pontbascule`.
Si votre port passe parfois de `/dev/ttyUSB0` à `/dev/ttyUSB1`, on peut créer une règle udev pour avoir un nom fixe (ex: `/dev/pontbascule`).

View File

@@ -3,8 +3,10 @@ import time
import socket
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from dotenv import load_dotenv
from .serial_bridge import SerialBridge, SerialConfig
from .mock_bridge import MockBridge
app = FastAPI(title="Pont bascule API", version="1.3")
@@ -16,20 +18,38 @@ def env(name: str, default: str) -> str:
return os.getenv(name, default)
load_dotenv()
def hex2b(s: str) -> bytes:
s = s.strip().replace("0x", "").replace(",", " ")
parts = [p for p in s.split() if p]
return bytes(int(p, 16) for p in parts)
def serial_port_connected(port: str):
try:
from serial.tools import list_ports
ports = {p.device for p in list_ports.comports()}
return port in ports, None
except Exception as e:
return None, str(e)
cfg = SerialConfig(
port=env("SERIAL_PORT", "/dev/ttyUSB0"),
baudrate=int(env("SERIAL_BAUDRATE", "9600")),
timeout_s=float(env("SERIAL_TIMEOUT_S", "1.0")),
open_delay_s=float(env("SERIAL_OPEN_DELAY_S", "2.0")), # ✅ comme Tkinter
post_write_delay_s=float(env("SERIAL_POST_WRITE_DELAY_S", "0.5")), # ✅
)
bridge = SerialBridge(cfg)
APP_MODE = env("APP_MODE", "serial").strip().lower()
if APP_MODE not in ("serial", "mock"):
APP_MODE = "serial"
if APP_MODE == "mock":
cfg = None
bridge = MockBridge()
else:
cfg = SerialConfig(
port=env("SERIAL_PORT", "/dev/ttyUSB0"),
baudrate=int(env("SERIAL_BAUDRATE", "9600")),
timeout_s=float(env("SERIAL_TIMEOUT_S", "1.0")),
open_delay_s=float(env("SERIAL_OPEN_DELAY_S", "2.0")), # ✅ comme Tkinter
post_write_delay_s=float(env("SERIAL_POST_WRITE_DELAY_S", "0.5")), # ✅
)
bridge = SerialBridge(cfg)
class CustomReq(BaseModel):
@@ -38,14 +58,25 @@ class CustomReq(BaseModel):
@app.get("/health")
def health():
port_connected = None
port_error = None
port_status = None
if cfg and cfg.port:
port_connected, port_error = serial_port_connected(cfg.port)
if port_connected is False:
port_status = "Port COM pas connecte"
if cfg is None or not cfg.port:
port_status = "Port COM pas configure"
return {
"ok": True,
"mode": "serial",
"ok": False if port_connected is False or (cfg is None or not cfg.port) else True,
"mode": APP_MODE,
"busy": bridge.busy(),
"hostname": socket.gethostname(),
"timestamp": time.time(),
"port": cfg.port,
"baudrate": cfg.baudrate,
"port": port_status if port_status else (cfg.port if cfg else None),
"baudrate": cfg.baudrate if cfg else None,
"port_connected": port_connected,
"port_error": port_error,
}

View File

@@ -2,3 +2,4 @@ fastapi
uvicorn
pydantic
pyserial
python-dotenv