Progetto Container Docker ble-ai-localizer
Ambiente: MN reslevis 192.168.1.3
	  m1.MajorNet-x64.6.6.0-60.bin:03 October 2025
	  server linux gentoo kernl: 6.6.74-gentoo-x86_64 Python default version 3.10.16

cd /data/service/

###########################################################################################
Passo 1  Creazione struttura progetto (sul server)
	mkdir -p ble-ai-localizer/{app,config,data,models}
	cd /data/service/ble-ai-localizer

		touch Dockerfile requirements.txt entrypoint.sh docker-compose.yml
		touch app/{__init__.py,main.py,settings.py,auth.py,mqtt_client.py,train.py,infer.py}
		touch config/config.yaml config/secrets.yaml

	Cosa hai ottenuto
		app/ codice python (training + inference)
		config/ configurazione (config.yaml versionabile + secrets.yaml non versionabile)
		data/ dataset fingerprint (volume)
		models/ modello salvato (volume)

###########################################################################################
Passo 2 .gitignore (consigliato subito)
	Se usi git:
cat > .gitignore << 'EOF'
# secrets e artefatti
config/secrets.yaml
data/
models/

# python
__pycache__/
*.pyc
.venv/

# editor
.vscode/
.idea/
EOF

###########################################################################################
Passo 3  Verifica prerequisiti Docker (Gentoo)

Esegui:

docker --version
docker compose version


Se docker compose non esiste, di solito su Gentoo:

o hai il plugin docker-cli-compose

o usi docker-compose (legacy). In tal caso dimmelo e ti adatto i comandi.

Quando hai eseguito i comandi del Passo 1–3, nel prossimo step ti faccio creare:

requirements.txt minimale + pinned

Dockerfile compatibile (python:3.10-slim)

docker-compose.yml con volumi (config/, data/, models/) e variabile MODE=train|infer

Se vuoi andare avanti subito senza feedback, incolla qui l’output di:

docker --version
docker compose version

docker compose version
Docker version 27.4.1, build b9d17eaebb55b7652ce37ae5c7c52fcb34194956
Docker Compose version v2.28.1

Perfetto, sei a posto (Docker 27.4.1 + Compose v2.28.1).

###########################################################################################
Passo 4 — Popoliamo i file base (requirements, Dockerfile, entrypoint, compose)
4.1 requirements.txt

Metti dentro (pinned e minimale per il tuo caso):

cat > requirements.txt << 'EOF'
pandas==2.2.2
numpy==1.26.4
scikit-learn==1.5.1
joblib==1.4.2

paho-mqtt==2.1.0
requests==2.32.3
PyYAML==6.0.2

fastapi==0.115.0
uvicorn[standard]==0.30.6

matplotlib==3.9.2
seaborn==0.13.2
EOF


(Se poi decidiamo che plot non servono in produzione, li togliamo.)

4.2 Dockerfile (Python “fissato”, non-root, robusto)
cat > Dockerfile << 'EOF'
FROM python:3.10-slim

# dipendenze native utili a numpy/scikit (safe choice)
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential gcc g++ \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r /app/requirements.txt

COPY app/ /app/app/
COPY entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh

# utente non-root
RUN useradd -m appuser
USER appuser

# cartelle dati/modelli (volumi)
RUN mkdir -p /home/appuser/data /home/appuser/models
ENV DATA_DIR=/home/appuser/data
ENV MODELS_DIR=/home/appuser/models

ENTRYPOINT ["/app/entrypoint.sh"]
EOF

4.3 entrypoint.sh (MODE=train|infer|api)
cat > entrypoint.sh << 'EOF'
#!/usr/bin/env bash
set -euo pipefail

MODE="${MODE:-infer}"

case "$MODE" in
  train)
    exec python -m app.main train
    ;;
  infer)
    exec python -m app.main infer
    ;;
  api)
    exec uvicorn app.main:api --host 0.0.0.0 --port "${PORT:-8080}"
    ;;
  *)
    echo "MODE non valido: $MODE (usa train|infer|api)"
    exit 1
    ;;
esac
EOF


Poi rendilo eseguibile:

chmod +x entrypoint.sh

4.4 docker-compose.yml (volumi + config esterna)
cat > docker-compose.yml << 'EOF'
services:
  ble-ai-localizer:
    build: .
    image: ble-ai-localizer:0.1.0
    environment:
      MODE: "infer"
      CONFIG_FILE: "/app/config/config.yaml"
      SECRETS_FILE: "/app/config/secrets.yaml"
    volumes:
      - ./config:/app/config:ro
      - ./data:/data
      - ./models:/models
    restart: unless-stopped
EOF

4.5 config/config.yaml (scheletro)
cat > config/config.yaml << 'EOF'
mqtt:
  host: "mosquitto"
  port: 1883
  topic: "ble/raw"

api:
  get_gateways_url: "https://APIHOST:5050/reslevis/getGateways"
  verify_tls: false
  refresh_seconds: 300

oidc:
  token_url: "https://KEYCLOAK/realms/REALM/protocol/openid-connect/token"
  client_id: "Fastapi"
  audience: "Fastapi"

paths:
  dataset: "/data/fingerprint.parquet"
  model: "/models/model.joblib"

ml:
  # parametri generali; l'algoritmo è un dettaglio interno
  method: "knn"
  k: 7
  weights: "distance"
  metric: "euclidean"
EOF

4.6 config/secrets.yaml (placeholder, non versionare)
cat > config/secrets.yaml << 'EOF'
oidc:
  client_secret: "CHANGE_ME"
  username: "CHANGE_ME"
  password: "CHANGE_ME"
EOF

###########################################################################################
Passo 5 Metti un main minimale per verificare che il container parte
app/main.py
cat > app/main.py << 'EOF'
from fastapi import FastAPI
from .settings import load_settings

api = FastAPI()

@api.get("/health")
def health():
    return {"status": "ok"}

def main():
    import sys
    settings = load_settings()
    print("Settings loaded. Keys:", list(settings.keys()))

    if len(sys.argv) < 2:
        raise SystemExit("Usage: python -m app.main [train|infer]")

    cmd = sys.argv[1].lower()
    if cmd == "train":
        print("TRAIN mode (placeholder)")
    elif cmd == "infer":
        print("INFER mode (placeholder)")
    else:
        raise SystemExit("Unknown command")

if __name__ == "__main__":
    main()
EOF

app/settings.py
cat > app/settings.py << 'EOF'
import os
from pathlib import Path
import yaml

def _read_yaml(path: str) -> dict:
    with open(path, "r", encoding="utf-8") as f:
        return yaml.safe_load(f) or {}

def deep_merge(a: dict, b: dict) -> dict:
    out = dict(a or {})
    for k, v in (b or {}).items():
        if isinstance(v, dict) and isinstance(out.get(k), dict):
            out[k] = deep_merge(out[k], v)
        else:
            out[k] = v
    return out

def load_settings() -> dict:
    cfg_path = os.getenv("CONFIG_FILE", "/app/config/config.yaml")
    settings = _read_yaml(cfg_path)

    secrets_path = os.getenv("SECRETS_FILE", "")
    if secrets_path and Path(secrets_path).exists():
        secrets = _read_yaml(secrets_path)
        settings = deep_merge(settings, secrets)

    # fallback paths (coerenti con compose)
    settings.setdefault("paths", {})
    settings["paths"].setdefault("dataset", os.getenv("DATASET_PATH", "/data/fingerprint.parquet"))
    settings["paths"].setdefault("model", os.getenv("MODEL_PATH", "/models/model.joblib"))

    return settings
EOF

(Gli altri file per ora possono restare vuoti.)

###########################################################################################
Passo 6 Build & test smoke test
	docker compose build


###########################################################################################
###  END prima creazione container                                        #################
###########################################################################################




###########################################################################################
1) Come avviare SOLO ble-ai-localizer (senza impattare gli altri)

	Vai nella directory del progetto:
		cd /data/service/ble-ai-localizer

	Avvio in background:
		docker compose -p ble-ai-localizer up -d
			Perché -p ble-ai-localizer?
			Imposta esplicitamente il project name, così sei sicuro al 100% di nonagganciare per errore un altro compose.

	Verifica stato (solo di questo progetto):
		docker compose -p ble-ai-localizer ps

	Log (solo di questo progetto):
		docker compose -p ble-ai-localizer logs -f


2) Come stoppare/riavviare SOLO ble-ai-localizer

	Stop (non rimuove container):
		docker compose -p ble-ai-localizer stop

	Restart:
		docker compose -p ble-ai-localizer restart

	Stop + rimozione container/network del progetto (NON tocca volumi bind ./data, ./models):
		docker compose -p ble-ai-localizer down

3) Aggiornare solo il tuo container (codice o Dockerfile cambiato)

	Ricostruisci l'immagine:
		docker compose -p ble-ai-localizer build

	Riavvia applicando l'immagine nuova:
		docker compose -p ble-ai-localizer up -d

	Se vuoi forzare rebuild+restart in un colpo:
		docker compose -p ble-ai-localizer up -d --build


4) Esportare l'immagine (backup o deploy su altro server)
	Esempio export in tar (meglio gzippato):
		docker save ble-ai-localizer:0.1.0 | gzip > ble-ai-localizer_0.1.0.tar.gz
	Su altro server:
		gzip -dc ble-ai-localizer_0.1.0.tar.gz | docker load

#Debug
	mosquitto_sub -v -h 192.168.1.101 -p 1883 -t '#' -V mqttv311  | grep publish_out

	docker compose -p ble-ai-localizer exec -T ble-ai-localizer ls -l /data/config/

	#Caso di gateway che non rileva nessun beacon
	publish_out/ac233fc1dcd3 [{"timestamp":"2026-01-30T13:40:08.885Z","type":"Gateway","mac":"AC233FC1DCD3","nums":0}]


#Verifica del modello in uso:
#Ver 1
docker compose -p ble-ai-localizer exec -T ble-ai-localizer python - <<'PY'
import hashlib, joblib, os
p="/data/model/model.joblib"
b=open(p,"rb").read()
print(f"FILE: {p}")
print(f"sha256={hashlib.sha256(b).hexdigest()[:12]} size={len(b)} bytes")
m=joblib.load(p)
print("TYPE:", type(m))
for k in ["version","nan_fill","k_floor","k_xy","weights","metric","floors"]:
    print(f"{k}:", getattr(m,k,None))
gws=getattr(m,"feature_gateways",[])
print("gateways:", len(gws), "first:", gws[:5])
regs=getattr(m,"xy_regs",{})
print("xy_regs floors:", sorted(list(regs.keys())))
PY
service "ble-ai-localizer" is not running

#Ver 2
docker compose -p ble-ai-localizer exec -T ble-ai-localizer python - <<'PY'
import joblib, pprint
m = joblib.load("/data/model/model.joblib")
keys = [
  "created_at_utc","sklearn_version","numpy_version",
  "gateways_order","nan_fill","k_floor","k_xy","weights","metric","floors"
]
pprint.pprint({k: m.get(k) for k in keys})
PY

#Esempio utilizzo server API:
Ottenimento del token:
TOKEN=$(
  curl -k -s -X POST "https://10.251.0.30:10002/realms/API.Server.local/protocol/openid-connect/token" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "grant_type=password" \
    -d "client_id=Fastapi" \
    -d "client_secret=wojuoB7Z5xhlPFrF2lIxJSSdVHCApEgC" \
    -d "username=core" \
    -d "password=C0r3_us3r_Cr3d3nt14ls" \
    -d "audience=Fastapi" \
  | jq -r '.access_token'
)

Utilizzare il token in un API:

curl -k -X 'GET' \
  'https://10.251.0.30:5050/reslevis/getTrackers' \
  -H 'accept: application/json' \
  -H 'Authorization: Bearer $TOKEN'




#Aggiornamento del software
	cd /data/service/ble-ai-localizer
	docker compose up -d --build
	docker compose -p ble-ai-localizer build
	docker compose -p ble-ai-localizer up -d --build
	docker system prune
    docker rmi ble-ai-localizer:0.1.0

Gestione Sart/Stop Container
	cd /data/service/ble-ai-localizer
	docker compose -p ble-ai-localizer up -d
	docker compose -p ble-ai-localizer logs -f --tail=200 --timestamps
	docker compose -p ble-ai-localizer stop
	docker compose -p ble-ai-localizer restart
	docker compose -p ble-ai-localizer down


Accesso Web MajorNET ResLevis:
	https://10.251.0.30/frontend/app_reslevis/app.html#home

Accesso Web a Container ble-ai-localizer
	URL: http://0.0.0.0:8501
	http://192.168.1.3:8501/
		username e password da file composer: docker-compose.yml
      			UI_USER: "Admin"
      			UI_PASSWORD: "pwdadmin1"  <-- facilitate per accesso
			da mobile




NOTE:
- Il valore di k utilizzato rappresenta la retta (k=2) o il piano (k>2) tra i
  beacon di trainig, in addestramento deve coincidere come minimo con il
numero di beacon per stanza, ma è un parametro globale per cui va fatto
rispettare per tutte le stanze,  per cui ad esempio se si decide di catturare 5
misure per stanza k conviene metterlo a 3

- almeno a 1m dalle pareti della stanza (evitare attaccato al muro su pareti
  adiacenti)

- per piani simemtrici le misure di ogni piano conviene farle conincidere

- aggingere timestamp in fingerprint e registarre traffico mqtt raw (a
  parita' di finestra di registrazione potranno essere rivalutati i valori
  letti)

- Tempo di tx nel beacon 200 ms
- potenze 0, -4 ,-8, -12
- slot di raccolta 30s

- time 1400
- Inferenza a 7 sec

- durante la raccolta occorre almeno un beacon di test per potenza che veiene escluso
  nell'addestarmento ma usato per l'inferenza con il traffico registrato nella
  fase di raccolta.

- testare prima gw e mgtt se regge 200 ms






ORGANIZZAZIONE CAMPAGNA:


N.4 piedistalli con a bordo n.4 becon configurati ognuno:
 - Tempo di tx nel beacon 200 ms
 - potenze 0, -4 ,-8, -12


Organizzazione dei piedistalli:
GBC-01: BC-00-21, BC-04-22, BC-08-23, BC-12-24

GBC-02: BC-00-25, BC-04-26, BC-08-27, BC-12-28

GBC-03: BC-00-36, BC-04-37, BC-08-38, BC-12-39

GBC-14: BC-00-29, BC-04-30, BC-08-41, BC-12-42 







