| @@ -1,33 +1,28 @@ | |||||
| FROM python:3.10-slim | FROM python:3.10-slim | ||||
| ENV PYTHONUNBUFFERED=1 | ENV PYTHONUNBUFFERED=1 | ||||
| ENV PYTHONDONTWRITEBYTECODE=1 | |||||
| # dipendenze native utili a numpy/scikit (safe choice) | |||||
| # Installiamo solo libgomp1 (necessaria per scikit-learn) | |||||
| RUN apt-get update && apt-get install -y --no-install-recommends \ | RUN apt-get update && apt-get install -y --no-install-recommends \ | ||||
| build-essential gcc g++ \ | |||||
| libgomp1 \ | |||||
| && rm -rf /var/lib/apt/lists/* | && rm -rf /var/lib/apt/lists/* | ||||
| WORKDIR /app | WORKDIR /app | ||||
| COPY requirements.txt /app/requirements.txt | |||||
| RUN pip install --no-cache-dir -r /app/requirements.txt | |||||
| # Correzione nome pacchetto e upgrade pip | |||||
| RUN pip install --no-cache-dir --upgrade pip | |||||
| RUN pip install streamlit==1.29.0 streamlit-drawable-canvas==0.9.3 PyYAML Pillow pandas | |||||
| RUN pip install --no-cache-dir folium streamlit-folium | |||||
| RUN pip install --no-cache-dir psutil | |||||
| # Copia e installazione in un unico step per ridurre i layer | |||||
| COPY requirements.txt . | |||||
| RUN pip install --no-cache-dir --upgrade pip && \ | |||||
| pip install --no-cache-dir -r requirements.txt | |||||
| # Copia del resto dell'applicazione | |||||
| COPY app/ /app/app/ | COPY app/ /app/app/ | ||||
| COPY entrypoint.sh /app/entrypoint.sh | COPY entrypoint.sh /app/entrypoint.sh | ||||
| RUN chmod +x /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 | |||||
| # Setup utente e permessi | |||||
| RUN useradd -m appuser && \ | |||||
| mkdir -p /home/appuser/data /home/appuser/models && \ | |||||
| chown -R appuser:appuser /home/appuser /app | |||||
| USER appuser | |||||
| ENTRYPOINT ["/app/entrypoint.sh"] | ENTRYPOINT ["/app/entrypoint.sh"] | ||||
| @@ -411,7 +411,7 @@ curl -k -X 'GET' \ | |||||
| docker compose -p ble-ai-localizer build | docker compose -p ble-ai-localizer build | ||||
| docker compose -p ble-ai-localizer up -d --build | docker compose -p ble-ai-localizer up -d --build | ||||
| docker system prune | docker system prune | ||||
| docker rmi ble-ai-localizer:0.1.0 | |||||
| Gestione Sart/Stop Container | Gestione Sart/Stop Container | ||||
| cd /data/service/ble-ai-localizer | cd /data/service/ble-ai-localizer | ||||
| @@ -1,58 +0,0 @@ | |||||
| // leaflet_bridge.js | |||||
| function initMap(config) { | |||||
| const { imgUrl, width, height, meta, grid_size, dots } = config; | |||||
| // Setup Coordinate XY (0,0 in alto a sinistra) | |||||
| const map = L.map('map', { | |||||
| crs: L.CRS.Simple, | |||||
| minZoom: -2, | |||||
| maxZoom: 4, | |||||
| attributionControl: false | |||||
| }); | |||||
| const bounds = [[-height, 0], [0, width]]; | |||||
| L.imageOverlay(imgUrl, bounds).addTo(map); | |||||
| map.fitBounds(bounds); | |||||
| // Gestione Griglia | |||||
| let gridLayer = L.layerGroup(); | |||||
| if (meta.show_grid) { | |||||
| const step = meta.grid_size || 100; | |||||
| const S = meta.pixel_ratio || 1; | |||||
| for (let x = 0; x <= width; x += (step * S)) { | |||||
| L.polyline([[0, x], [-height, x]], {color: '#ccc', weight: 1, opacity: 0.5}).addTo(gridLayer); | |||||
| } | |||||
| for (let y = 0; y >= -height; y -= (step * S)) { | |||||
| L.polyline([[y, 0], [y, width]], {color: '#ccc', weight: 1, opacity: 0.5}).addTo(gridLayer); | |||||
| } | |||||
| gridLayer.addTo(map); | |||||
| } | |||||
| // Visualizzazione punti esistenti (Verdi e Blu) | |||||
| dots.forEach(dot => { | |||||
| const color = dot.status === 'completed' ? '#228B22' : '#0000FF'; | |||||
| L.circleMarker([dot.y, dot.x], { | |||||
| radius: config.dot_size / 5, | |||||
| fillColor: color, | |||||
| color: 'white', | |||||
| weight: 2, | |||||
| fillOpacity: 0.8 | |||||
| }).addTo(map).bindPopup(`X: ${dot.relX}, Y: ${dot.relY}`); | |||||
| }); | |||||
| // Evento Click per rilievo | |||||
| map.on('click', function(e) { | |||||
| const raw_x = e.latlng.lng; | |||||
| const raw_y = e.latlng.lat; // In Leaflet Simple, Y è negativa sotto l'origine | |||||
| // Invio dati a Streamlit | |||||
| window.parent.postMessage({ | |||||
| type: 'streamlit:setComponentValue', | |||||
| value: { | |||||
| x: raw_x, | |||||
| y: raw_y, | |||||
| timestamp: new Date().getTime() | |||||
| } | |||||
| }, '*'); | |||||
| }); | |||||
| } | |||||
| @@ -1,49 +1,217 @@ | |||||
| import os | |||||
| import json | |||||
| import streamlit as st | |||||
| import pandas as pd | |||||
| from PIL import Image | |||||
| from pathlib import Path | |||||
| import time | |||||
| import folium | |||||
| from streamlit_folium import st_folium | |||||
| import math | |||||
| import base64 | import base64 | ||||
| from io import BytesIO | from io import BytesIO | ||||
| def get_image_base64(img): | |||||
| # --- CACHE IMMAGINE --- | |||||
| @st.cache_data | |||||
| def get_image_base64(img_path): | |||||
| img = Image.open(img_path).convert("RGBA") | |||||
| w, h = img.size | |||||
| buffered = BytesIO() | buffered = BytesIO() | ||||
| img.save(buffered, format="PNG") | img.save(buffered, format="PNG") | ||||
| img_str = base64.b64encode(buffered.getvalue()).decode() | |||||
| return f"data:image/png;base64,{img_str}" | |||||
| img_str = base64.b64encode(buffered.getvalue()).decode("ascii") | |||||
| return f"data:image/png;base64,{img_str}", w, h | |||||
| def show_mapper_v2(cfg): | |||||
| # ... (caricamento meta e percorsi come nel tuo file originale) ... | |||||
| img_path = MAPS_DIR / f"{cfg['maps']['floor_prefix']}{floor_id}.png" | |||||
| img = Image.open(img_path).convert("RGBA") | |||||
| img_width, img_height = img.size | |||||
| img_b64 = get_image_base64(img) | |||||
| # --- FUNZIONE SALVATAGGIO FISICO --- | |||||
| def force_save_json(path, data): | |||||
| try: | |||||
| with open(path, "w", encoding='utf-8') as f: | |||||
| json.dump(data, f, indent=4) | |||||
| return True | |||||
| except Exception as e: | |||||
| st.error(f"Errore scrittura disco: {e}") | |||||
| return False | |||||
| # Prepariamo la lista dei punti esistenti (Punto 6 delle specifiche) | |||||
| dots_data = [] | |||||
| # Qui cicla sui tuoi file CSV e popola dots_data con {x, y, relX, relY, status} | |||||
| # Integrazione del componente HTML | |||||
| # Carichiamo il JS dal file esterno | |||||
| with open("leaflet_bridge.js", "r") as f: | |||||
| js_code = f.read() | |||||
| html_content = f""" | |||||
| <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" /> | |||||
| <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> | |||||
| <div id="map" style="height: 600px; width: 100%;"></div> | |||||
| <script> | |||||
| {js_code} | |||||
| initMap({{ | |||||
| imgUrl: "{img_b64}", | |||||
| width: {img_width}, | |||||
| height: {img_height}, | |||||
| meta: {json.dumps(meta)}, | |||||
| dots: {json.dumps(dots_data)}, | |||||
| dot_size: {dot_size} | |||||
| }}); | |||||
| </script> | |||||
| """ | |||||
| # Il componente restituisce il valore di window.parent.postMessage | |||||
| result = components.html(html_content, height=650) | |||||
| def show_mapper(cfg): | |||||
| MAPS_DIR = Path(cfg['maps']['map_dir']) | |||||
| SAMPLES_DIR = Path("/data/train/samples") | |||||
| JOBS_BASE = Path(cfg['collect_train']['jobs_dir']) | |||||
| PENDING_DIR = JOBS_BASE / "pending" | |||||
| ERROR_DIR = JOBS_BASE / "error" | |||||
| BEACONS_FILE = "/data/config/beacons.csv" | |||||
| [p.mkdir(parents=True, exist_ok=True) for p in [MAPS_DIR, SAMPLES_DIR, PENDING_DIR, ERROR_DIR]] | |||||
| # --- 1. GESTIONE UPLOAD --- | |||||
| # La logica ora gestisce stringhe per floor_id permettendo il carattere "-" | |||||
| maps = sorted([f.replace(cfg['maps']['floor_prefix'], "").split('.')[0] | |||||
| for f in os.listdir(MAPS_DIR) if f.startswith(cfg['maps']['floor_prefix'])]) | |||||
| if result: | |||||
| st.write(f"Posizione catturata: {result}") | |||||
| # Qui inserisci la tua logica di salvataggio CSV che avevi nel punto 7 | |||||
| with st.expander("📂 Carica Nuova Planimetria", expanded=not maps): | |||||
| c_up1, c_up2 = st.columns([1, 2]) | |||||
| new_f_id = c_up1.text_input("ID Piano (es. 0, -1):", key="new_f_id_v21") | |||||
| up_file = c_up2.file_uploader("Immagine (PNG/JPG):", type=['png', 'jpg', 'jpeg'], key="up_v21") | |||||
| if st.button("🚀 SALVA PIANO", use_container_width=True): | |||||
| if new_f_id and up_file: | |||||
| ext = Path(up_file.name).suffix | |||||
| # Il nome file includerà il "-" se presente in new_f_id | |||||
| img_save_path = MAPS_DIR / f"{cfg['maps']['floor_prefix']}{new_f_id}{ext}" | |||||
| with open(img_save_path, "wb") as f: | |||||
| f.write(up_file.getbuffer()) | |||||
| m_path = MAPS_DIR / f"{cfg['maps']['meta_prefix']}{new_f_id}.json" | |||||
| force_save_json(m_path, {"pixel_ratio": 1.0, "calibrated": False, "origin": [0, 0], "grid_size": 50}) | |||||
| st.session_state.current_floor = new_f_id | |||||
| st.rerun() | |||||
| if not maps: return | |||||
| # --- 2. RIGA STATO --- | |||||
| c_p, c_s, c_o = st.columns([2.5, 2, 2]) | |||||
| with c_p: | |||||
| s1, s2 = st.columns([1, 1.2]) | |||||
| s1.markdown("<p style='padding-top:35px; font-weight:bold;'>Piano Attivo:</p>", unsafe_allow_html=True) | |||||
| def_idx = maps.index(st.session_state.current_floor) if st.session_state.get("current_floor") in maps else 0 | |||||
| floor_id = s2.selectbox("", maps, index=def_idx, key="f_v21", label_visibility="collapsed") | |||||
| if st.session_state.get("prev_floor") != floor_id: | |||||
| st.session_state.cal_points = [] | |||||
| st.session_state.temp_lat, st.session_state.temp_lng = None, None | |||||
| st.session_state.prev_floor = floor_id | |||||
| meta_path = MAPS_DIR / f"{cfg['maps']['meta_prefix']}{floor_id}.json" | |||||
| if meta_path.exists(): | |||||
| with open(meta_path, "r") as f: meta = json.load(f) | |||||
| else: | |||||
| meta = {"pixel_ratio": 1.0, "calibrated": False, "origin": [0, 0]} | |||||
| with c_s: st.info(f"📏 Scala: {'✅' if meta['calibrated'] else '❌'}\n({meta['pixel_ratio']:.4f} px/cm)") | |||||
| with c_o: st.info(f"🎯 Origine: {'✅' if meta['origin'] != [0,0] else '❌'}\n(X:{meta['origin'][0]}, Y:{meta['origin'][1]})") | |||||
| if not meta["calibrated"] or meta["origin"] == [0, 0]: | |||||
| st.warning(f"💡 **Piano {floor_id} da configurare**: Esegui **Calibra** e imposta l'**Origine**.") | |||||
| # --- 3. IMPOSTAZIONI GLOBALI (Default disabilitati) --- | |||||
| st.markdown("---") | |||||
| g1, g2, g3, g4 = st.columns([1.2, 1.5, 1.2, 2]) | |||||
| with g1: | |||||
| # Griglia disattivata di default per evitare interferenze iniziali | |||||
| show_grid = st.toggle("Griglia", value=False, key="grid_v21") | |||||
| with g2: | |||||
| grid_cm = st.select_slider("Passo (cm):", options=[25, 50, 100, 200], value=50, key="step_v21") | |||||
| with g3: | |||||
| # Stay Grid disattivato di default | |||||
| snap_on = st.toggle("Stay Grid", value=False, key="snap_v21") | |||||
| with g4: | |||||
| m_size = st.slider("Dimensione Marker:", 5, 20, 8, key="msize_v21") | |||||
| st.markdown("---") | |||||
| # --- 4. TOOLSET --- | |||||
| t1, t2, t3 = st.columns(3) | |||||
| if t1.button("📏 CALIBRA", use_container_width=True): st.session_state.map_tool = "Calibra"; st.session_state.cal_points = [] | |||||
| if t2.button("🎯 SET ORIGINE", use_container_width=True): st.session_state.map_tool = "Origine" | |||||
| if t3.button("📡 RILEVA", use_container_width=True): st.session_state.map_tool = "Rileva" | |||||
| tool = st.session_state.get('map_tool', 'Rileva') | |||||
| # --- 5. MAPPA --- | |||||
| col_map, col_ui = st.columns([3, 1]) | |||||
| with col_map: | |||||
| img_p = next((MAPS_DIR / f"{cfg['maps']['floor_prefix']}{floor_id}{e}" for e in ['.png','.jpg','.jpeg'] if (MAPS_DIR / f"{cfg['maps']['floor_prefix']}{floor_id}{e}").exists())) | |||||
| img_data, w, h = get_image_base64(img_p) | |||||
| bounds = [[0, 0], [h, w]] | |||||
| m = folium.Map(location=[h/2, w/2], crs="Simple", tiles=None, attribution_control=False) | |||||
| m.fit_bounds(bounds) | |||||
| m.options.update({"minZoom": -6, "maxZoom": 6, "zoomSnap": 0.25, "maxBounds": bounds, "maxBoundsViscosity": 1.0}) | |||||
| folium.raster_layers.ImageOverlay(image=img_data, bounds=bounds, interactive=True).add_to(m) | |||||
| # ORIGINE (Sempre visibile se impostata) | |||||
| if meta["origin"] != [0, 0]: | |||||
| ox, oy = meta["origin"] | |||||
| folium.CircleMarker(location=[oy, ox], radius=6, color="black", fill=True, zindex=1000, tooltip="Origine (0,0)").add_to(m) | |||||
| # GRIGLIA | |||||
| if show_grid and meta["calibrated"] and meta["origin"] != [0, 0]: | |||||
| px_step = grid_cm * meta["pixel_ratio"] | |||||
| ox, oy = meta["origin"] | |||||
| for x in sorted(list(range(int(ox), w, int(px_step))) + list(range(int(ox), 0, -int(px_step)))): | |||||
| folium.PolyLine([[0, x], [h, x]], color="blue", weight=1, opacity=0.1).add_to(m) | |||||
| for y in sorted(list(range(int(oy), h, int(px_step))) + list(range(int(oy), 0, -int(px_step)))): | |||||
| folium.PolyLine([[y, 0], [y, w]], color="blue", weight=1, opacity=0.1).add_to(m) | |||||
| # Disegno Punti Storici | |||||
| def draw_points(directory, color, shape="circle"): | |||||
| if not meta["calibrated"] or meta["origin"] == [0,0]: return | |||||
| for f in Path(directory).glob("*.csv"): | |||||
| try: | |||||
| df = pd.read_csv(f, sep=";") | |||||
| df.columns = [c.lower() for c in df.columns] | |||||
| # Confronto tra stringhe per gestire piani negativi e ID complessi | |||||
| if str(df.iloc[0].get('z', df.iloc[0].get('floor'))) == str(floor_id): | |||||
| px_x = (df.iloc[0]['x'] * meta["pixel_ratio"]) + meta["origin"][0] | |||||
| px_y = meta["origin"][1] - (df.iloc[0]['y'] * meta["pixel_ratio"]) | |||||
| if shape == "circle": folium.CircleMarker(location=[px_y, px_x], radius=m_size, color=color, fill=True, fill_opacity=0.8).add_to(m) | |||||
| elif shape == "square": folium.RegularPolygonMarker(location=[px_y, px_x], number_of_sides=4, radius=m_size, color="black", weight=2, fill=True, fill_color=color).add_to(m) | |||||
| elif shape == "diamond": folium.RegularPolygonMarker(location=[px_y, px_x], number_of_sides=4, rotation=45, radius=m_size, color="black", weight=1, fill=True, fill_color=color).add_to(m) | |||||
| except: continue | |||||
| draw_points(SAMPLES_DIR, "green", "circle") | |||||
| draw_points(PENDING_DIR, "yellow", "diamond") | |||||
| draw_points(ERROR_DIR, "red", "square") | |||||
| # Feedback Visivo Click | |||||
| if tool == "Calibra" and st.session_state.get("cal_points"): | |||||
| for p in st.session_state.cal_points: folium.CircleMarker(location=p, radius=6, color="red", fill=True).add_to(m) | |||||
| if len(st.session_state.cal_points) == 2: folium.PolyLine(st.session_state.cal_points, color="red", weight=3).add_to(m) | |||||
| elif st.session_state.get("temp_lat") is not None: | |||||
| folium.CircleMarker(location=[st.session_state.temp_lat, st.session_state.temp_lng], radius=m_size+2, color="blue", fill=True, zindex=500).add_to(m) | |||||
| out = st_folium(m, height=600, width=None, key=f"map_v21_{floor_id}_{tool}") | |||||
| click = out.get("last_clicked") | |||||
| if click: | |||||
| clat, clng = click["lat"], click["lng"] | |||||
| if tool == "Calibra": | |||||
| if len(st.session_state.get("cal_points", [])) < 2: | |||||
| st.session_state.cal_points.append([clat, clng]); st.rerun() | |||||
| else: | |||||
| if tool == "Rileva" and snap_on and meta["calibrated"]: | |||||
| px_step = grid_cm * meta["pixel_ratio"] | |||||
| st.session_state.temp_lng = meta["origin"][0] + round((clng - meta["origin"][0]) / px_step) * px_step | |||||
| st.session_state.temp_lat = meta["origin"][1] + round((clat - meta["origin"][1]) / px_step) * px_step | |||||
| else: | |||||
| st.session_state.temp_lat, st.session_state.temp_lng = clat, clng | |||||
| st.rerun() | |||||
| # --- 6. LOGICA UI --- | |||||
| with col_ui: | |||||
| st.write(f"### Tool: **{tool}**") | |||||
| err_files = list(ERROR_DIR.glob("*.csv")) | |||||
| if err_files and st.button(f"🗑️ PULISCI ERRORI", use_container_width=True): | |||||
| for f in err_files: os.remove(f); st.rerun() | |||||
| if tool == "Calibra": | |||||
| pts = st.session_state.get("cal_points", []) | |||||
| if len(pts) == 2: | |||||
| dist_cm = st.number_input("Distanza reale (cm):", value=100.0) | |||||
| if st.button("📏 SALVA SCALA", use_container_width=True, type="primary"): | |||||
| px_d = math.sqrt((pts[1][1]-pts[0][1])**2 + (pts[1][0]-pts[0][0])**2) | |||||
| meta.update({"pixel_ratio": px_d / dist_cm, "calibrated": True}) | |||||
| if force_save_json(meta_path, meta): st.session_state.cal_points = []; st.rerun() | |||||
| elif st.session_state.get("temp_lat") is not None: | |||||
| px_x, px_y = st.session_state.temp_lng, st.session_state.temp_lat | |||||
| if tool == "Origine": | |||||
| st.metric("X (px)", int(px_x)); st.metric("Y (px)", int(px_y)) | |||||
| if st.button("💾 SALVA ORIGINE", use_container_width=True, type="primary"): | |||||
| meta["origin"] = [int(px_x), int(px_y)] | |||||
| if force_save_json(meta_path, meta): st.rerun() | |||||
| elif tool == "Rileva" and meta["calibrated"]: | |||||
| rx = (px_x - meta["origin"][0]) / meta["pixel_ratio"] | |||||
| ry = (meta["origin"][1] - px_y) / meta["pixel_ratio"] | |||||
| sx, sy = int(round(rx)), int(round(ry)) | |||||
| st.metric("X (cm)", sx); st.metric("Y (cm)", sy) | |||||
| if os.path.exists(BEACONS_FILE): | |||||
| b_df = pd.read_csv(BEACONS_FILE, sep=";") | |||||
| sel_b = st.selectbox("Beacon:", b_df.apply(lambda x: f"{x['BeaconName']} | {x['MAC']}", axis=1)) | |||||
| if st.button("🚀 REGISTRA", use_container_width=True, type="primary"): | |||||
| b_name, b_mac = sel_b.split(" | ") | |||||
| id_str = f"{b_name}_{floor_id}_{sx}_{sy}" | |||||
| # Salvataggio Z come stringa per mantenere il "-" | |||||
| data = {"Position": id_str, "Floor": floor_id, "RoomName": b_name, "X": sx, "Y": sy, "Z": floor_id, "BeaconName": b_name, "MAC": b_mac} | |||||
| pd.DataFrame([data]).to_csv(PENDING_DIR / f"{id_str}.csv", sep=";", index=False); st.rerun() | |||||
| @@ -1,145 +0,0 @@ | |||||
| import os | |||||
| import json | |||||
| import streamlit as st | |||||
| import pandas as pd | |||||
| from PIL import Image, ImageDraw | |||||
| from pathlib import Path | |||||
| from streamlit_drawable_canvas import st_canvas | |||||
| import time | |||||
| def show_mapper(cfg): | |||||
| # --- 1. CONFIGURAZIONE PERCORSI --- | |||||
| MAPS_DIR = Path(cfg['maps']['map_dir']) | |||||
| SAMPLES_DIR = Path(cfg['train']['samples_dir']) | |||||
| PENDING_DIR = Path(cfg['collect_train']['jobs_dir']) / "pending" | |||||
| BEACONS_FILE = "/data/config/beacons.csv" | |||||
| [p.mkdir(parents=True, exist_ok=True) for p in [MAPS_DIR, SAMPLES_DIR, PENDING_DIR]] | |||||
| def load_map_metadata(f_id): | |||||
| meta_path = MAPS_DIR / f"{cfg['maps']['meta_prefix']}{f_id}.json" | |||||
| defaults = {"pixel_ratio": 1.0, "calibrated": False, "origin": [0, 0], "grid_size": 50} | |||||
| if meta_path.exists(): | |||||
| with open(meta_path, "r") as f: return {**defaults, **json.load(f)} | |||||
| return defaults | |||||
| st.subheader("🗺️ Gestione Mappatura e Rilevamento") | |||||
| # Selezione Piano | |||||
| maps = sorted([f.replace(cfg['maps']['floor_prefix'], "").split('.')[0] for f in os.listdir(MAPS_DIR) if f.startswith(cfg['maps']['floor_prefix'])]) | |||||
| if not maps: st.error("Nessuna mappa trovata."); return | |||||
| floor_id = st.selectbox("Seleziona Piano (Z):", maps) | |||||
| meta = load_map_metadata(floor_id) | |||||
| # --- 2. STATO SISTEMA --- | |||||
| c_s1, c_s2 = st.columns(2) | |||||
| with c_s1: | |||||
| st.info(f"📏 Scala: {'✅' if meta['calibrated'] else '❌'} ({meta['pixel_ratio']:.4f} px/cm)") | |||||
| with c_s2: | |||||
| st.info(f"🎯 Origine: {'✅' if meta['origin'] != [0,0] else '❌'} (X:{meta['origin'][0]}, Y:{meta['origin'][1]})") | |||||
| # --- 3. CONTROLLI VISUALIZZAZIONE --- | |||||
| with st.expander("🎨 Opzioni Visualizzazione", expanded=True): | |||||
| c_v1, c_v2, c_v3 = st.columns(3) | |||||
| zoom = c_v1.slider("🔍 Zoom Mappa", 0.1, 4.0, 1.0, 0.1) | |||||
| dot_size = c_v2.slider("🔵/🟢 Dimensione Punti", 10, 100, 40) | |||||
| mode = c_v3.radio("Modalità Interazione:", ["🖐️ NAVIGA", "🎯 AZIONE"], horizontal=True) | |||||
| # --- 4. SELEZIONE STRUMENTO --- | |||||
| t1, t2, t3 = st.columns(3) | |||||
| if t1.button("📏 IMPOSTA SCALA", use_container_width=True): st.session_state.map_tool = "Calibra" | |||||
| if t2.button("🎯 SET ORIGINE", use_container_width=True): st.session_state.map_tool = "Origine" | |||||
| is_ready = meta.get("calibrated", False) and meta["origin"] != [0, 0] | |||||
| if t3.button("📡 RILEVA", use_container_width=True, disabled=not is_ready): st.session_state.map_tool = "Rileva" | |||||
| tool = st.session_state.get('map_tool', 'Rileva') | |||||
| st.write(f"Strumento attivo: **{tool}**") | |||||
| # --- 5. PREPARAZIONE IMMAGINE E STORICO --- | |||||
| img_path = MAPS_DIR / f"{cfg['maps']['floor_prefix']}{floor_id}.png" | |||||
| if not img_path.exists(): img_path = MAPS_DIR / f"{cfg['maps']['floor_prefix']}{floor_id}.jpg" | |||||
| img = Image.open(img_path).convert("RGBA") | |||||
| draw = ImageDraw.Draw(img) | |||||
| if is_ready: | |||||
| samples_files = list(SAMPLES_DIR.glob(f"{floor_id}_*.csv")) | |||||
| pending_files = list(PENDING_DIR.glob(f"{floor_id}_*.csv")) | |||||
| def draw_points(files, color): | |||||
| for f in files: | |||||
| try: | |||||
| p = f.stem.split("_") | |||||
| px = (int(p[1]) * meta["pixel_ratio"]) + meta["origin"][0] | |||||
| py = (int(p[2]) * meta["pixel_ratio"]) + meta["origin"][1] | |||||
| r = dot_size // 2 | |||||
| draw.ellipse([px-r, py-r, px+r, py+r], fill=color, outline="white", width=2) | |||||
| except: continue | |||||
| draw_points(samples_files, "#228B22") # Verde: Completati | |||||
| draw_points(pending_files, "#0000FF") # Blu: In corso | |||||
| # --- 6. CANVAS --- | |||||
| d_mode = "transform" if mode == "🖐️ NAVIGA" else ("line" if tool == "Calibra" else "point") | |||||
| canvas_result = st_canvas( | |||||
| background_image=img, | |||||
| height=int(img.size[1] * zoom), | |||||
| width=int(img.size[0] * zoom), | |||||
| drawing_mode=d_mode, | |||||
| display_toolbar=True, | |||||
| update_streamlit=True, | |||||
| key=f"canvas_v10_{floor_id}_{zoom}_{mode}_{tool}", | |||||
| ) | |||||
| # --- 7. LOGICA AZIONI E SALVATAGGIO CSV --- | |||||
| if mode == "🎯 AZIONE" and canvas_result.json_data and canvas_result.json_data["objects"]: | |||||
| last = canvas_result.json_data["objects"][-1] | |||||
| raw_x, raw_y = last["left"] / zoom, last["top"] / zoom | |||||
| if tool == "Calibra" and last["type"] == "line": | |||||
| px_dist = ((last["x2"] - last["x1"])**2 + (last["y2"] - last["y1"])**2)**0.5 / zoom | |||||
| dist_cm = st.number_input("Distanza reale (cm):", value=100.0) | |||||
| if st.button("APPLICA SCALA"): | |||||
| meta.update({"pixel_ratio": px_dist / dist_cm, "calibrated": True}) | |||||
| with open(MAPS_DIR / f"{cfg['maps']['meta_prefix']}{floor_id}.json", "w") as f: | |||||
| json.dump(meta, f) | |||||
| st.rerun() | |||||
| elif tool == "Origine": | |||||
| if st.button(f"Fissa Origine qui: {int(raw_x)}, {int(raw_y)}"): | |||||
| meta["origin"] = [int(raw_x), int(raw_y)] | |||||
| with open(MAPS_DIR / f"{cfg['maps']['meta_prefix']}{floor_id}.json", "w") as f: | |||||
| json.dump(meta, f) | |||||
| st.rerun() | |||||
| elif tool == "Rileva" and is_ready: | |||||
| dx = (raw_x - meta["origin"][0]) / meta["pixel_ratio"] | |||||
| dy = (raw_y - meta["origin"][1]) / meta["pixel_ratio"] | |||||
| grid = meta.get("grid_size", 50) | |||||
| sx, sy = int(round(dx/grid)*grid), int(round(dy/grid)*grid) | |||||
| st.write(f"### 📍 Punto: {sx}cm, {sy}cm") | |||||
| if os.path.exists(BEACONS_FILE): | |||||
| b_df = pd.read_csv(BEACONS_FILE, sep=";") | |||||
| sel_b = st.selectbox("Seleziona Beacon:", b_df.apply(lambda x: f"{x['BeaconName']} | {x['MAC']}", axis=1)) | |||||
| if st.button("🚀 REGISTRA LETTURA", type="primary", use_container_width=True): | |||||
| b_name, b_mac = sel_b.split(" | ") | |||||
| fname = f"{floor_id}_{sx}_{sy}.csv" | |||||
| # --- FIX SINTASSI CSV --- | |||||
| # Formato: Position;Floor;RoomName;X;Y;Z;BeaconName;MAC | |||||
| data_out = { | |||||
| "Position": f"P_{sx}_{sy}", | |||||
| "Floor": floor_id, | |||||
| "RoomName": "Area_Rilevazione", | |||||
| "X": sx, | |||||
| "Y": sy, | |||||
| "Z": 0, | |||||
| "BeaconName": b_name, | |||||
| "MAC": b_mac | |||||
| } | |||||
| pd.DataFrame([data_out]).to_csv(PENDING_DIR / fname, index=False, sep=";") | |||||
| st.toast(f"Punto Blu registrato!") | |||||
| time.sleep(1); st.rerun() | |||||
| @@ -1,49 +0,0 @@ | |||||
| import base64 | |||||
| from io import BytesIO | |||||
| def get_image_base64(img): | |||||
| buffered = BytesIO() | |||||
| img.save(buffered, format="PNG") | |||||
| img_str = base64.b64encode(buffered.getvalue()).decode() | |||||
| return f"data:image/png;base64,{img_str}" | |||||
| def show_mapper_v2(cfg): | |||||
| # ... (caricamento meta e percorsi come nel tuo file originale) ... | |||||
| img_path = MAPS_DIR / f"{cfg['maps']['floor_prefix']}{floor_id}.png" | |||||
| img = Image.open(img_path).convert("RGBA") | |||||
| img_width, img_height = img.size | |||||
| img_b64 = get_image_base64(img) | |||||
| # Prepariamo la lista dei punti esistenti (Punto 6 delle specifiche) | |||||
| dots_data = [] | |||||
| # Qui cicla sui tuoi file CSV e popola dots_data con {x, y, relX, relY, status} | |||||
| # Integrazione del componente HTML | |||||
| # Carichiamo il JS dal file esterno | |||||
| with open("leaflet_bridge.js", "r") as f: | |||||
| js_code = f.read() | |||||
| html_content = f""" | |||||
| <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" /> | |||||
| <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> | |||||
| <div id="map" style="height: 600px; width: 100%;"></div> | |||||
| <script> | |||||
| {js_code} | |||||
| initMap({{ | |||||
| imgUrl: "{img_b64}", | |||||
| width: {img_width}, | |||||
| height: {img_height}, | |||||
| meta: {json.dumps(meta)}, | |||||
| dots: {json.dumps(dots_data)}, | |||||
| dot_size: {dot_size} | |||||
| }}); | |||||
| </script> | |||||
| """ | |||||
| # Il componente restituisce il valore di window.parent.postMessage | |||||
| result = components.html(html_content, height=650) | |||||
| if result: | |||||
| st.write(f"Posizione catturata: {result}") | |||||
| # Qui inserisci la tua logica di salvataggio CSV che avevi nel punto 7 | |||||
| @@ -1,100 +1,102 @@ | |||||
| import streamlit as st | import streamlit as st | ||||
| import pandas as pd | import pandas as pd | ||||
| import os | |||||
| import json | import json | ||||
| from PIL import Image, ImageDraw, ImageFont | |||||
| import folium | |||||
| from streamlit_folium import st_folium | |||||
| from pathlib import Path | from pathlib import Path | ||||
| import time | |||||
| import os | |||||
| from PIL import Image | |||||
| import base64 | |||||
| from io import BytesIO | |||||
| # --- UTILS --- | |||||
| @st.cache_data | |||||
| def get_image_base64(img_path): | |||||
| img = Image.open(img_path).convert("RGBA") | |||||
| w, h = img.size | |||||
| buffered = BytesIO() | |||||
| img.save(buffered, format="PNG") | |||||
| img_str = base64.b64encode(buffered.getvalue()).decode("ascii") | |||||
| return f"data:image/png;base64,{img_str}", w, h | |||||
| def show_inference_page(cfg): | def show_inference_page(cfg): | ||||
| # Definizione percorsi | |||||
| # Assumendo che il file sia in /app/app/web_inference.py, | |||||
| # cerchiamo il font nella stessa cartella 'app' | |||||
| BASE_DIR = Path(__file__).parent | |||||
| st.subheader("📡 Monitoraggio Beacon Real-Time") | |||||
| # --- CONFIGURAZIONE PERCORSI --- | |||||
| MAPS_DIR = Path(cfg['maps']['map_dir']) | MAPS_DIR = Path(cfg['maps']['map_dir']) | ||||
| INFER_FILE = Path(cfg['infer']['output_dir']) / cfg['infer']['out_file'] | |||||
| INFER_FILE = Path("/data/infer/infer.csv") | |||||
| # Percorso del font locale | |||||
| FONT_PATH = BASE_DIR / "DejaVuSans-Bold.ttf" | |||||
| st.subheader("🤖 Monitoraggio Inferenza Live") | |||||
| # --- 1. SELEZIONE E STATO (RIGA COMPATTA) --- | |||||
| maps = sorted([f.replace(cfg['maps']['floor_prefix'], "").split('.')[0] | |||||
| for f in os.listdir(MAPS_DIR) if f.startswith(cfg['maps']['floor_prefix'])]) | |||||
| if not maps: | |||||
| st.warning("Nessuna mappa configurata.") | |||||
| return | |||||
| # --- CONTROLLI NEL TAB --- | |||||
| with st.expander("🎨 Opzioni e Controllo", expanded=True): | |||||
| col_ctrl1, col_ctrl2, col_ctrl3, col_ctrl4 = st.columns(4) | |||||
| with col_ctrl1: | |||||
| dot_size = st.slider("Dimensione Marker", 10, 100, 45, key="inf_dot") | |||||
| with col_ctrl2: | |||||
| refresh_rate = st.select_slider("Refresh (s)", options=[2, 5, 10, 30], value=5, key="inf_ref") | |||||
| with col_ctrl3: | |||||
| show_labels = st.checkbox("Mostra MAC", value=True, key="inf_show_mac") | |||||
| with col_ctrl4: | |||||
| # Monitoraggio disattivato di default per evitare login fantasma | |||||
| auto_refresh = st.toggle("🔄 Monitoraggio Attivo", value=False, key="inf_auto") | |||||
| # Lettura dati per conteggio | |||||
| df_infer = pd.DataFrame() | |||||
| if INFER_FILE.exists(): | |||||
| df_infer = pd.read_csv(INFER_FILE, sep=";") | |||||
| floor_id = st.number_input("Piano (Z)", value=0, min_value=0, step=1, key="inf_z") | |||||
| # Layout riga 1 | |||||
| c_piano, c_count, c_size = st.columns([3, 2, 2]) | |||||
| with c_piano: | |||||
| sub1, sub2 = st.columns([1, 1.2]) | |||||
| sub1.markdown("<p style='padding-top:35px; font-weight:bold; font-size:15px;'>Piano Visualizzato:</p>", unsafe_allow_html=True) | |||||
| floor_id = sub2.selectbox("", maps, key="inf_floor_v24", label_visibility="collapsed") | |||||
| # Filtro dati per il piano scelto | |||||
| df_active = df_infer[(df_infer['z'].astype(str) == str(floor_id)) & (df_infer['x'] != -1)] if not df_infer.empty else pd.DataFrame() | |||||
| with c_count: | |||||
| st.info(f"📡 Beacon Attivi: **{len(df_active)}**\n(Totali nel file: {len(df_infer)})") | |||||
| with c_size: | |||||
| # Slider per dimensione pallini (come nel mapper) | |||||
| m_size = st.slider("Dimensione Beacon:", 5, 20, 8, key="inf_msize_v24") | |||||
| # Caricamento Metadati | # Caricamento Metadati | ||||
| meta_path = MAPS_DIR / f"{cfg['maps']['meta_prefix']}{floor_id}.json" | meta_path = MAPS_DIR / f"{cfg['maps']['meta_prefix']}{floor_id}.json" | ||||
| meta = {"pixel_ratio": 1.0, "origin": [0, 0]} | |||||
| if meta_path.exists(): | |||||
| with open(meta_path, "r") as f: | |||||
| meta.update(json.load(f)) | |||||
| if not meta_path.exists(): return | |||||
| with open(meta_path, "r") as f: meta = json.load(f) | |||||
| # --- 2. RENDERING MAPPA --- | |||||
| st.markdown("---") | |||||
| img_p = next((MAPS_DIR / f"{cfg['maps']['floor_prefix']}{floor_id}{e}" for e in ['.png','.jpg'] if (MAPS_DIR / f"{cfg['maps']['floor_prefix']}{floor_id}{e}").exists())) | |||||
| img_data, w, h = get_image_base64(img_p) | |||||
| bounds = [[0, 0], [h, w]] | |||||
| m = folium.Map(location=[h/2, w/2], crs="Simple", tiles=None, attribution_control=False) | |||||
| m.fit_bounds(bounds) | |||||
| m.options.update({"minZoom": -6, "maxZoom": 6, "zoomSnap": 0.25, "maxBounds": bounds, "maxBoundsViscosity": 1.0}) | |||||
| folium.raster_layers.ImageOverlay(image=img_data, bounds=bounds).add_to(m) | |||||
| try: | |||||
| if INFER_FILE.exists(): | |||||
| df = pd.read_csv(INFER_FILE, sep=";") | |||||
| df_active = df[(df['z'] == floor_id) & (df['x'] != -1) & (df['y'] != -1)] | |||||
| img_path = MAPS_DIR / f"{cfg['maps']['floor_prefix']}{floor_id}.png" | |||||
| if img_path.exists(): | |||||
| img = Image.open(img_path).convert("RGBA") | |||||
| draw = ImageDraw.Draw(img) | |||||
| # Disegno Beacon | |||||
| if meta["calibrated"] and meta["origin"] != [0,0]: | |||||
| # Origine | |||||
| folium.CircleMarker(location=[meta["origin"][1], meta["origin"][0]], radius=4, color="black", fill=True).add_to(m) | |||||
| # --- GESTIONE FONT XL --- | |||||
| # Dimensione proporzionale al pallino (1.5x) | |||||
| dynamic_font_size = int(dot_size * 1.5) | |||||
| font = None | |||||
| if FONT_PATH.exists(): | |||||
| try: | |||||
| # Tenta il caricamento del file TTF locale | |||||
| font = ImageFont.truetype(str(FONT_PATH), dynamic_font_size) | |||||
| except Exception as e: | |||||
| # Se il file è corrotto (unknown file format), mostra errore e usa fallback | |||||
| st.error(f"Errore caricamento font: {e}. Controlla il file DejaVuSans-Bold.ttf") | |||||
| if font is None: | |||||
| font = ImageFont.load_default() | |||||
| for _, row in df_active.iterrows(): | |||||
| px_x = (row['x'] * meta["pixel_ratio"]) + meta["origin"][0] | |||||
| px_y = meta["origin"][1] - (row['y'] * meta["pixel_ratio"]) | |||||
| mac_label = str(row['mac'])[-5:] | |||||
| for _, row in df_active.iterrows(): | |||||
| px_x = (row['x'] * meta["pixel_ratio"]) + meta["origin"][0] | |||||
| px_y = (row['y'] * meta["pixel_ratio"]) + meta["origin"][1] | |||||
| folium.CircleMarker( | |||||
| location=[px_y, px_x], radius=m_size, color="blue", | |||||
| fill=True, fill_color="cyan", fill_opacity=0.8 | |||||
| ).add_to(m) | |||||
| r = dot_size | |||||
| # Disegno Marker | |||||
| draw.ellipse([px_x-(r+5), px_y-(r+5), px_x+(r+5), px_y+(r+5)], fill="black") | |||||
| draw.ellipse([px_x-r, px_y-r, px_x+r, px_y+r], fill="#00E5FF") | |||||
| if show_labels: | |||||
| label = f"{str(row['mac'])[-5:]}" | |||||
| # Posizionamento dinamico a destra del pallino | |||||
| text_x = px_x + r + 20 | |||||
| text_y = px_y - (dynamic_font_size / 2) | |||||
| # Outline per leggibilità (bordo nero spesso 3px) | |||||
| for dx, dy in [(-3,-3), (3,-3), (-3,3), (3,3), (0,-3), (0,3), (-3,0), (2,0)]: | |||||
| draw.text((text_x + dx, text_y + dy), label, font=font, fill="black") | |||||
| # Testo Giallo | |||||
| draw.text((text_x, text_y), label, font=font, fill="#FFFF00") | |||||
| folium.Marker( | |||||
| location=[px_y, px_x], | |||||
| icon=folium.DivIcon(html=f"""<div style="font-family: sans-serif; color: black; font-weight: bold; font-size: {int(m_size*1.2)}pt; width: 80px; transform: translate({m_size+2}px, -{m_size+2}px);">{mac_label}</div>""") | |||||
| ).add_to(m) | |||||
| st.image(img, use_column_width=True) | |||||
| st.metric("Dispositivi Online", len(df_active)) | |||||
| except Exception as e: | |||||
| st.error(f"Errore generale: {e}") | |||||
| st_folium(m, height=700, width=None, key=f"inf_map_v24_{floor_id}", use_container_width=True) | |||||
| # --- REFRESH CONDIZIONALE --- | |||||
| if auto_refresh: | |||||
| time.sleep(refresh_rate) | |||||
| st.rerun() | |||||
| # --- 3. TABELLA RIEPILOGO COMPLETA --- | |||||
| if not df_infer.empty: | |||||
| st.subheader("Dettaglio Dispositivi (Tutti i Piani)") | |||||
| # Aggiunta colonna Z per chiarezza | |||||
| st.dataframe(df_infer[['mac', 'z', 'x', 'y']], use_container_width=True) | |||||
| @@ -27,7 +27,7 @@ st.set_page_config( | |||||
| menu_items={ | menu_items={ | ||||
| 'Get Help': None, | 'Get Help': None, | ||||
| 'Report a bug': None, | 'Report a bug': None, | ||||
| 'About': "# ▒~_~[▒▒~O BLE AI Localizer - Suite\nSistema professionale di posizionamento BLE." | |||||
| 'About': "🛰️BLE AI Localizer - Suite\nSistema professionale di posizionamento BLE." | |||||
| } | } | ||||
| ) | ) | ||||
| @@ -44,6 +44,9 @@ st.markdown(""" | |||||
| </style> | </style> | ||||
| """, unsafe_allow_html=True) | """, unsafe_allow_html=True) | ||||
| if "main_login_user" not in st.session_state: | |||||
| st.session_state["main_login_user"] = "" | |||||
| # --- LOGIN (Tua versione originale) --- | # --- LOGIN (Tua versione originale) --- | ||||
| if "password_correct" not in st.session_state: | if "password_correct" not in st.session_state: | ||||
| st.session_state["password_correct"] = False | st.session_state["password_correct"] = False | ||||
| @@ -63,18 +63,18 @@ infer: | |||||
| window_seconds: 5 | window_seconds: 5 | ||||
| xy_round: 0 | xy_round: 0 | ||||
| maps: | maps: | ||||
| default_dot_size: 20 | |||||
| default_grid_size: 100 | |||||
| floor_prefix: floor_ | floor_prefix: floor_ | ||||
| map_dir: /data/maps | map_dir: /data/maps | ||||
| meta_prefix: meta_ | meta_prefix: meta_ | ||||
| default_grid_size: 100 # Dimensione griglia predefinita in cm | |||||
| default_dot_size: 20 # Dimensione predefinita dei marker | |||||
| show_grid_default: true # Stato iniziale della griglia | |||||
| show_grid_default: true | |||||
| ml: | ml: | ||||
| k: 5 | k: 5 | ||||
| method: knn | method: knn | ||||
| metric: euclidean | metric: euclidean | ||||
| weights: distance | weights: distance | ||||
| mode: infer | |||||
| mode: collect_train | |||||
| mqtt: | mqtt: | ||||
| ca_file: '' | ca_file: '' | ||||
| client_id: ble-ai-localizer | client_id: ble-ai-localizer | ||||
| @@ -1,13 +1,13 @@ | |||||
| mac;z;x;y | mac;z;x;y | ||||
| C8:3F:8F:17:DB:35;1;1061;1290 | |||||
| C8:3F:8F:17:DB:35;1;1058;1294 | |||||
| C3:00:00:39:47:DF;-1;-1;-1 | C3:00:00:39:47:DF;-1;-1;-1 | ||||
| C3:00:00:39:47:C4;1;1386;1501 | |||||
| C3:00:00:39:47:C4;1;1304;1478 | |||||
| C3:00:00:39:47:E2;-1;-1;-1 | C3:00:00:39:47:E2;-1;-1;-1 | ||||
| C7:AE:56:1E:38:B7;1;1311;1501 | |||||
| C7:AE:56:1E:38:B7;1;1323;1499 | |||||
| E0:1F:9A:7A:47:D2;-1;-1;-1 | E0:1F:9A:7A:47:D2;-1;-1;-1 | ||||
| C3:00:00:57:B9:D9;1;1060;1380 | |||||
| C3:00:00:57:B9:DB;1;1056;1383 | |||||
| C3:00:00:57:B9:F4;1;1061;1292 | |||||
| C3:00:00:57:B9:DC;1;1060;1294 | |||||
| C3:00:00:57:B9:DD;1;1216;1444 | |||||
| C3:00:00:57:B9:DF;1;1059;1296 | |||||
| C3:00:00:57:B9:D9;1;1058;1382 | |||||
| C3:00:00:57:B9:DB;1;1060;1293 | |||||
| C3:00:00:57:B9:F4;1;1059;1296 | |||||
| C3:00:00:57:B9:DC;1;1060;1291 | |||||
| C3:00:00:57:B9:DD;1;1203;1452 | |||||
| C3:00:00:57:B9:DF;1;1061;1294 | |||||
| @@ -0,0 +1 @@ | |||||
| {"pixel_ratio": 1.5843674344166205, "calibrated": true, "origin": [1296, 3744], "grid_size": 50, "show_grid": true} | |||||
| @@ -0,0 +1 @@ | |||||
| {"pixel_ratio": 1.5919341046277666, "calibrated": true, "origin": [1251, 1143], "grid_size": 50} | |||||
| @@ -0,0 +1,9 @@ | |||||
| { | |||||
| "pixel_ratio": 1.584476207254857, | |||||
| "calibrated": true, | |||||
| "origin": [ | |||||
| 1248, | |||||
| 3790 | |||||
| ], | |||||
| "grid_size": 50 | |||||
| } | |||||
| @@ -0,0 +1 @@ | |||||
| {"pixel_ratio": 1.5924732396126005, "calibrated": true, "origin": [1297, 3586], "grid_size": 50} | |||||
| @@ -0,0 +1,9 @@ | |||||
| { | |||||
| "pixel_ratio": 1.5920551746472242, | |||||
| "calibrated": true, | |||||
| "origin": [ | |||||
| 1297, | |||||
| 3587 | |||||
| ], | |||||
| "grid_size": 50 | |||||
| } | |||||
| @@ -1 +1,9 @@ | |||||
| {"pixel_ratio": 1.574668658056015, "calibrated": true, "origin": [1193, 1031], "grid_size": 50, "show_grid": true} | |||||
| { | |||||
| "pixel_ratio": 1.584773230620443, | |||||
| "calibrated": true, | |||||
| "origin": [ | |||||
| 1247, | |||||
| 3791 | |||||
| ], | |||||
| "grid_size": 50 | |||||
| } | |||||
| @@ -1 +1,9 @@ | |||||
| {"pixel_ratio": 1.5919341046277666, "calibrated": true, "origin": [1251, 1143], "grid_size": 50} | |||||
| { | |||||
| "pixel_ratio": 1.5924129560451799, | |||||
| "calibrated": true, | |||||
| "origin": [ | |||||
| 1297, | |||||
| 3587 | |||||
| ], | |||||
| "grid_size": 50 | |||||
| } | |||||
| @@ -0,0 +1,2 @@ | |||||
| Position;Floor;RoomName;X;Y;Z;BeaconName;MAC | |||||
| BC-21_-1_1050_250;-1;BC-21;1050;250;-1;BC-21;C3:00:00:57:B9:E6 | |||||
| @@ -0,0 +1,2 @@ | |||||
| Position;Floor;RoomName;X;Y;Z;BeaconName;MAC | |||||
| BC-21_0_195_1425;0;BC-21;195;1425;0;BC-21;C3:00:00:57:B9:E6 | |||||
| @@ -0,0 +1,2 @@ | |||||
| Position;Floor;RoomName;X;Y;Z;BeaconName;MAC | |||||
| BC-21_0_2114_796;0;BC-21;2114;796;0;BC-21;C3:00:00:57:B9:E6 | |||||
| @@ -0,0 +1,2 @@ | |||||
| Position;Floor;RoomName;X;Y;Z;BeaconName;MAC | |||||
| P_2115_697;0;BC-21;2115;697;0;BC-21;C3:00:00:57:B9:E6 | |||||
| @@ -0,0 +1,2 @@ | |||||
| Position;Floor;RoomName;X;Y;Z;BeaconName;MAC | |||||
| BC-21_0_2568_946;0;BC-21;2568;946;0;BC-21;C3:00:00:57:B9:E6 | |||||
| @@ -0,0 +1,2 @@ | |||||
| Position;Floor;RoomName;X;Y;Z;BeaconName;MAC | |||||
| BC-21_0_400_100;0;BC-21;400;100;0;BC-21;C3:00:00:57:B9:E6 | |||||
| @@ -0,0 +1,2 @@ | |||||
| Position;Floor;RoomName;X;Y;Z;BeaconName;MAC | |||||
| BC-21_0_619_122;0;BC-21;619;122;0;BC-21;C3:00:00:57:B9:E6 | |||||
| @@ -0,0 +1,2 @@ | |||||
| Position;Floor;RoomName;X;Y;Z;BeaconName;MAC | |||||
| BC-21_1_1050_500;1;BC-21;1050;500;1;BC-21;C3:00:00:57:B9:E6 | |||||
| @@ -0,0 +1,2 @@ | |||||
| Position;Floor;RoomName;X;Y;Z;BeaconName;MAC | |||||
| BC-21_1_950_750;1;BC-21;950;750;1;BC-21;C3:00:00:57:B9:E6 | |||||
| @@ -0,0 +1,2 @@ | |||||
| Position;Floor;RoomName;X;Y;Z;BeaconName;MAC | |||||
| BC-22_-1_1650_1100;-1;BC-22;1650;1100;-1;BC-22;C3:00:00:57:B9:D4 | |||||
| @@ -0,0 +1,2 @@ | |||||
| Position;Floor;RoomName;X;Y;Z;BeaconName;MAC | |||||
| BC-22_-1_170_1418;-1;BC-22;170;1418;-1;BC-22;C3:00:00:57:B9:D4 | |||||
| @@ -0,0 +1,2 @@ | |||||
| Position;Floor;RoomName;X;Y;Z;BeaconName;MAC | |||||
| BC-22_0_534_999;0;BC-22;534;999;0;BC-22;C3:00:00:57:B9:D4 | |||||
| @@ -0,0 +1,2 @@ | |||||
| Position;Floor;RoomName;X;Y;Z;BeaconName;MAC | |||||
| BC-23_0_1580_180;0;BC-23;1580;180;0;BC-23;C3:00:00:57:B9:E8 | |||||
| @@ -0,0 +1,2 @@ | |||||
| Position;Floor;RoomName;X;Y;Z;BeaconName;MAC | |||||
| BC-23_1_130_1386;1;BC-23;130;1386;1;BC-23;C3:00:00:57:B9:E8 | |||||
| @@ -0,0 +1,2 @@ | |||||
| Position;Floor;RoomName;X;Y;Z;BeaconName;MAC | |||||
| BC-23_1_1800_600;1;BC-23;1800;600;1;BC-23;C3:00:00:57:B9:E8 | |||||
| @@ -0,0 +1,2 @@ | |||||
| Position;Floor;RoomName;X;Y;Z;BeaconName;MAC | |||||
| BC-24_1_1900_1400;1;BC-24;1900;1400;1;BC-24;C3:00:00:57:B9:F1 | |||||
| @@ -0,0 +1,2 @@ | |||||
| Position;Floor;RoomName;X;Y;Z;BeaconName;MAC | |||||
| BC-24_1_2200_1000;1;BC-24;2200;1000;1;BC-24;C3:00:00:57:B9:F1 | |||||
| @@ -0,0 +1,2 @@ | |||||
| Position;Floor;RoomName;X;Y;Z;BeaconName;MAC | |||||
| BC-25_0_562_239;0;BC-25;562;239;0;BC-25;C3:00:00:57:B9:E7 | |||||
| @@ -0,0 +1,2 @@ | |||||
| Position;Floor;RoomName;X;Y;Z;BeaconName;MAC | |||||
| BC-25_1_1450_600;1;BC-25;1450;600;1;BC-25;C3:00:00:57:B9:E7 | |||||
| @@ -0,0 +1,2 @@ | |||||
| Position;Floor;RoomName;X;Y;Z;BeaconName;MAC | |||||
| BC-25_1_2400_300;1;BC-25;2400;300;1;BC-25;C3:00:00:57:B9:E7 | |||||
| @@ -0,0 +1,2 @@ | |||||
| Position;Floor;RoomName;X;Y;Z;BeaconName;MAC | |||||
| BC-25_1_2500_1300;1;BC-25;2500;1300;1;BC-25;C3:00:00:57:B9:E7 | |||||
| @@ -0,0 +1,2 @@ | |||||
| Position;Floor;RoomName;X;Y;Z;BeaconName;MAC | |||||
| P_1356_1256;0;BC-21;1356;1256;0;BC-21;C3:00:00:57:B9:E6 | |||||
| @@ -0,0 +1,2 @@ | |||||
| Position;Floor;RoomName;X;Y;Z;BeaconName;MAC | |||||
| P_2142_616;0;BC-21;2142;616;0;BC-21;C3:00:00:57:B9:E6 | |||||
| @@ -0,0 +1,2 @@ | |||||
| Position;Floor;RoomName;X;Y;Z;BeaconName;MAC | |||||
| P_2491_543;0;BC-22;2491;543;0;BC-22;C3:00:00:57:B9:D4 | |||||
| @@ -0,0 +1,2 @@ | |||||
| mac;x;y;z;AC:23:3F:C1:DD:3C;AC:23:3F:C1:DD:49;AC:23:3F:C1:DC:EE;AC:23:3F:C1:DD:40;AC:23:3F:C1:DD:51;AC:23:3F:C1:DD:48;AC:23:3F:C1:DD:50;AC:23:3F:C1:DC:D3;AC:23:3F:C1:DD:55;AC:23:3F:C1:DC:D1;AC:23:3F:C1:DC:CB;AC:23:3F:C1:DC:D2;AC:23:3F:C1:DD:31;AC:23:3F:C1:DD:4B;AC:23:3F:C1:DD:4E;AC:23:3F:C1:DC:CD | |||||
| C3:00:00:57:B9:E6;1050;250;-1;-72.000;-82.000;-75.000;nan;nan;nan;nan;nan;-80.000;-51.500;-74.000;-64.000;-73.000;-77.000;nan;-76.000 | |||||
| @@ -0,0 +1,2 @@ | |||||
| mac;x;y;z;AC:23:3F:C1:DD:3C;AC:23:3F:C1:DD:49;AC:23:3F:C1:DC:EE;AC:23:3F:C1:DD:40;AC:23:3F:C1:DD:51;AC:23:3F:C1:DD:48;AC:23:3F:C1:DD:50;AC:23:3F:C1:DC:D3;AC:23:3F:C1:DD:55;AC:23:3F:C1:DC:D1;AC:23:3F:C1:DC:CB;AC:23:3F:C1:DC:D2;AC:23:3F:C1:DD:31;AC:23:3F:C1:DD:4B;AC:23:3F:C1:DD:4E;AC:23:3F:C1:DC:CD | |||||
| C3:00:00:57:B9:D4;1650;1100;-1;-77.000;nan;-78.000;nan;nan;nan;nan;nan;-76.000;nan;-75.000;-67.000;-76.000;-78.000;-83.000;-73.500 | |||||
| @@ -0,0 +1,2 @@ | |||||
| mac;x;y;z;AC:23:3F:C1:DD:3C;AC:23:3F:C1:DD:49;AC:23:3F:C1:DC:EE;AC:23:3F:C1:DD:40;AC:23:3F:C1:DD:51;AC:23:3F:C1:DD:48;AC:23:3F:C1:DD:50;AC:23:3F:C1:DC:D3;AC:23:3F:C1:DD:55;AC:23:3F:C1:DC:D1;AC:23:3F:C1:DC:CB;AC:23:3F:C1:DC:D2;AC:23:3F:C1:DD:31;AC:23:3F:C1:DD:4B;AC:23:3F:C1:DD:4E;AC:23:3F:C1:DC:CD | |||||
| C3:00:00:57:B9:D4;170;1418;-1;-77.000;nan;-79.000;nan;-85.000;nan;nan;nan;-74.000;nan;-73.000;-67.000;-76.000;-78.000;-83.000;-74.000 | |||||
| @@ -0,0 +1,2 @@ | |||||
| mac;x;y;z;AC:23:3F:C1:DD:3C;AC:23:3F:C1:DD:49;AC:23:3F:C1:DC:EE;AC:23:3F:C1:DD:40;AC:23:3F:C1:DD:51;AC:23:3F:C1:DD:48;AC:23:3F:C1:DD:50;AC:23:3F:C1:DC:D3;AC:23:3F:C1:DD:55;AC:23:3F:C1:DC:D1;AC:23:3F:C1:DC:CB;AC:23:3F:C1:DC:D2;AC:23:3F:C1:DD:31;AC:23:3F:C1:DD:4B;AC:23:3F:C1:DD:4E;AC:23:3F:C1:DC:CD | |||||
| C3:00:00:57:B9:E6;1356;1256;0;-72.000;-82.000;-75.500;nan;nan;nan;nan;nan;-80.000;-51.000;-74.000;-64.500;-70.000;-79.000;nan;-75.500 | |||||
| @@ -0,0 +1,2 @@ | |||||
| mac;x;y;z;AC:23:3F:C1:DD:3C;AC:23:3F:C1:DD:49;AC:23:3F:C1:DC:EE;AC:23:3F:C1:DD:40;AC:23:3F:C1:DD:51;AC:23:3F:C1:DD:48;AC:23:3F:C1:DD:50;AC:23:3F:C1:DC:D3;AC:23:3F:C1:DD:55;AC:23:3F:C1:DC:D1;AC:23:3F:C1:DC:CB;AC:23:3F:C1:DC:D2;AC:23:3F:C1:DD:31;AC:23:3F:C1:DD:4B;AC:23:3F:C1:DD:4E;AC:23:3F:C1:DC:CD | |||||
| C3:00:00:57:B9:E8;1580;180;0;-80.000;-82.000;-83.500;nan;-85.500;nan;nan;nan;-76.000;-59.000;-71.000;-64.000;-77.000;-78.000;-85.000;-74.000 | |||||
| @@ -0,0 +1,2 @@ | |||||
| mac;x;y;z;AC:23:3F:C1:DD:3C;AC:23:3F:C1:DD:49;AC:23:3F:C1:DC:EE;AC:23:3F:C1:DD:40;AC:23:3F:C1:DD:51;AC:23:3F:C1:DD:48;AC:23:3F:C1:DD:50;AC:23:3F:C1:DC:D3;AC:23:3F:C1:DD:55;AC:23:3F:C1:DC:D1;AC:23:3F:C1:DC:CB;AC:23:3F:C1:DC:D2;AC:23:3F:C1:DD:31;AC:23:3F:C1:DD:4B;AC:23:3F:C1:DD:4E;AC:23:3F:C1:DC:CD | |||||
| C3:00:00:57:B9:E6;1600;450;0;-72.000;-82.000;-75.000;nan;nan;nan;nan;nan;-81.000;-51.500;-75.000;-64.000;-73.000;-77.500;nan;-76.000 | |||||
| @@ -0,0 +1,2 @@ | |||||
| mac;x;y;z;AC:23:3F:C1:DD:3C;AC:23:3F:C1:DD:49;AC:23:3F:C1:DC:EE;AC:23:3F:C1:DD:40;AC:23:3F:C1:DD:51;AC:23:3F:C1:DD:48;AC:23:3F:C1:DD:50;AC:23:3F:C1:DC:D3;AC:23:3F:C1:DD:55;AC:23:3F:C1:DC:D1;AC:23:3F:C1:DC:CB;AC:23:3F:C1:DC:D2;AC:23:3F:C1:DD:31;AC:23:3F:C1:DD:4B;AC:23:3F:C1:DD:4E;AC:23:3F:C1:DC:CD | |||||
| C3:00:00:57:B9:E6;195;1425;0;-76.000;-82.000;-75.000;nan;nan;nan;nan;nan;-80.000;-52.000;-74.000;-64.000;-70.000;-78.000;nan;-76.000 | |||||
| @@ -0,0 +1,2 @@ | |||||
| mac;x;y;z;AC:23:3F:C1:DD:3C;AC:23:3F:C1:DD:49;AC:23:3F:C1:DC:EE;AC:23:3F:C1:DD:40;AC:23:3F:C1:DD:51;AC:23:3F:C1:DD:48;AC:23:3F:C1:DD:50;AC:23:3F:C1:DC:D3;AC:23:3F:C1:DD:55;AC:23:3F:C1:DC:D1;AC:23:3F:C1:DC:CB;AC:23:3F:C1:DC:D2;AC:23:3F:C1:DD:31;AC:23:3F:C1:DD:4B;AC:23:3F:C1:DD:4E;AC:23:3F:C1:DC:CD | |||||
| C3:00:00:57:B9:E6;2114;796;0;-72.000;-82.000;-76.000;nan;nan;nan;nan;nan;-80.000;-52.000;-75.000;-64.000;-70.500;-78.000;nan;-75.000 | |||||
| @@ -0,0 +1,2 @@ | |||||
| mac;x;y;z;AC:23:3F:C1:DD:3C;AC:23:3F:C1:DD:49;AC:23:3F:C1:DC:EE;AC:23:3F:C1:DD:40;AC:23:3F:C1:DD:51;AC:23:3F:C1:DD:48;AC:23:3F:C1:DD:50;AC:23:3F:C1:DC:D3;AC:23:3F:C1:DD:55;AC:23:3F:C1:DC:D1;AC:23:3F:C1:DC:CB;AC:23:3F:C1:DC:D2;AC:23:3F:C1:DD:31;AC:23:3F:C1:DD:4B;AC:23:3F:C1:DD:4E;AC:23:3F:C1:DC:CD | |||||
| C3:00:00:57:B9:E6;2115;697;0;-76.000;-82.000;-76.000;nan;nan;nan;nan;nan;-80.000;-52.000;-75.000;-64.000;-71.000;-77.000;nan;-76.000 | |||||
| @@ -0,0 +1,2 @@ | |||||
| mac;x;y;z;AC:23:3F:C1:DD:3C;AC:23:3F:C1:DD:49;AC:23:3F:C1:DC:EE;AC:23:3F:C1:DD:40;AC:23:3F:C1:DD:51;AC:23:3F:C1:DD:48;AC:23:3F:C1:DD:50;AC:23:3F:C1:DC:D3;AC:23:3F:C1:DD:55;AC:23:3F:C1:DC:D1;AC:23:3F:C1:DC:CB;AC:23:3F:C1:DC:D2;AC:23:3F:C1:DD:31;AC:23:3F:C1:DD:4B;AC:23:3F:C1:DD:4E;AC:23:3F:C1:DC:CD | |||||
| C3:00:00:57:B9:E6;2142;616;0;-77.000;-82.000;-76.000;nan;nan;nan;nan;nan;-80.000;-52.000;-75.000;-64.000;-72.000;-78.000;nan;-76.000 | |||||
| @@ -0,0 +1,2 @@ | |||||
| mac;x;y;z;AC:23:3F:C1:DD:3C;AC:23:3F:C1:DD:49;AC:23:3F:C1:DC:EE;AC:23:3F:C1:DD:40;AC:23:3F:C1:DD:51;AC:23:3F:C1:DD:48;AC:23:3F:C1:DD:50;AC:23:3F:C1:DC:D3;AC:23:3F:C1:DD:55;AC:23:3F:C1:DC:D1;AC:23:3F:C1:DC:CB;AC:23:3F:C1:DC:D2;AC:23:3F:C1:DD:31;AC:23:3F:C1:DD:4B;AC:23:3F:C1:DD:4E;AC:23:3F:C1:DC:CD | |||||
| C3:00:00:57:B9:D4;2491;543;0;-77.000;nan;-79.000;nan;-84.000;nan;nan;nan;-74.000;nan;-75.000;-67.000;-77.000;-78.000;-83.000;-73.000 | |||||
| @@ -0,0 +1,2 @@ | |||||
| mac;x;y;z;AC:23:3F:C1:DD:3C;AC:23:3F:C1:DD:49;AC:23:3F:C1:DC:EE;AC:23:3F:C1:DD:40;AC:23:3F:C1:DD:51;AC:23:3F:C1:DD:48;AC:23:3F:C1:DD:50;AC:23:3F:C1:DC:D3;AC:23:3F:C1:DD:55;AC:23:3F:C1:DC:D1;AC:23:3F:C1:DC:CB;AC:23:3F:C1:DC:D2;AC:23:3F:C1:DD:31;AC:23:3F:C1:DD:4B;AC:23:3F:C1:DD:4E;AC:23:3F:C1:DC:CD | |||||
| C3:00:00:57:B9:E6;2568;946;0;-76.000;-83.000;-76.000;nan;nan;nan;nan;nan;-80.000;-51.000;-74.500;-64.000;-73.000;-77.000;nan;-76.000 | |||||
| @@ -0,0 +1,2 @@ | |||||
| mac;x;y;z;AC:23:3F:C1:DD:3C;AC:23:3F:C1:DD:49;AC:23:3F:C1:DC:EE;AC:23:3F:C1:DD:40;AC:23:3F:C1:DD:51;AC:23:3F:C1:DD:48;AC:23:3F:C1:DD:50;AC:23:3F:C1:DC:D3;AC:23:3F:C1:DD:55;AC:23:3F:C1:DC:D1;AC:23:3F:C1:DC:CB;AC:23:3F:C1:DC:D2;AC:23:3F:C1:DD:31;AC:23:3F:C1:DD:4B;AC:23:3F:C1:DD:4E;AC:23:3F:C1:DC:CD | |||||
| C3:00:00:57:B9:E6;400;100;0;-77.000;-83.000;-75.000;nan;nan;nan;nan;nan;-80.000;-51.000;-76.000;-64.000;-73.000;-78.000;nan;-76.000 | |||||
| @@ -0,0 +1,2 @@ | |||||
| mac;x;y;z;AC:23:3F:C1:DD:3C;AC:23:3F:C1:DD:49;AC:23:3F:C1:DC:EE;AC:23:3F:C1:DD:40;AC:23:3F:C1:DD:51;AC:23:3F:C1:DD:48;AC:23:3F:C1:DD:50;AC:23:3F:C1:DC:D3;AC:23:3F:C1:DD:55;AC:23:3F:C1:DC:D1;AC:23:3F:C1:DC:CB;AC:23:3F:C1:DC:D2;AC:23:3F:C1:DD:31;AC:23:3F:C1:DD:4B;AC:23:3F:C1:DD:4E;AC:23:3F:C1:DC:CD | |||||
| C3:00:00:57:B9:D4;534;999;0;-77.000;nan;-79.000;nan;-84.000;nan;nan;nan;-76.000;nan;-73.500;-66.000;-76.000;-78.000;-83.000;-73.000 | |||||
| @@ -0,0 +1,2 @@ | |||||
| mac;x;y;z;AC:23:3F:C1:DD:3C;AC:23:3F:C1:DD:49;AC:23:3F:C1:DC:EE;AC:23:3F:C1:DD:40;AC:23:3F:C1:DD:51;AC:23:3F:C1:DD:48;AC:23:3F:C1:DD:50;AC:23:3F:C1:DC:D3;AC:23:3F:C1:DD:55;AC:23:3F:C1:DC:D1;AC:23:3F:C1:DC:CB;AC:23:3F:C1:DC:D2;AC:23:3F:C1:DD:31;AC:23:3F:C1:DD:4B;AC:23:3F:C1:DD:4E;AC:23:3F:C1:DC:CD | |||||
| C3:00:00:57:B9:E7;562;239;0;-83.500;nan;-83.000;nan;nan;nan;nan;nan;-78.000;-48.000;-71.000;-75.000;-74.000;-78.500;-83.000;-80.000 | |||||
| @@ -0,0 +1,2 @@ | |||||
| mac;x;y;z;AC:23:3F:C1:DD:3C;AC:23:3F:C1:DD:49;AC:23:3F:C1:DC:EE;AC:23:3F:C1:DD:40;AC:23:3F:C1:DD:51;AC:23:3F:C1:DD:48;AC:23:3F:C1:DD:50;AC:23:3F:C1:DC:D3;AC:23:3F:C1:DD:55;AC:23:3F:C1:DC:D1;AC:23:3F:C1:DC:CB;AC:23:3F:C1:DC:D2;AC:23:3F:C1:DD:31;AC:23:3F:C1:DD:4B;AC:23:3F:C1:DD:4E;AC:23:3F:C1:DC:CD | |||||
| C3:00:00:57:B9:E6;619;122;0;-72.000;-82.000;-75.000;nan;nan;nan;nan;nan;-80.000;-52.000;-75.500;-64.000;-73.000;-78.000;nan;-76.000 | |||||
| @@ -0,0 +1,2 @@ | |||||
| mac;x;y;z;AC:23:3F:C1:DD:3C;AC:23:3F:C1:DD:49;AC:23:3F:C1:DC:EE;AC:23:3F:C1:DD:40;AC:23:3F:C1:DD:51;AC:23:3F:C1:DD:48;AC:23:3F:C1:DD:50;AC:23:3F:C1:DC:D3;AC:23:3F:C1:DD:55;AC:23:3F:C1:DC:D1;AC:23:3F:C1:DC:CB;AC:23:3F:C1:DC:D2;AC:23:3F:C1:DD:31;AC:23:3F:C1:DD:4B;AC:23:3F:C1:DD:4E;AC:23:3F:C1:DC:CD | |||||
| C3:00:00:57:B9:F1;1050;400;1;nan;nan;nan;nan;nan;nan;nan;nan;-77.000;nan;-69.000;-65.000;-71.000;-77.000;-82.000;-81.000 | |||||
| @@ -0,0 +1,2 @@ | |||||
| mac;x;y;z;AC:23:3F:C1:DD:3C;AC:23:3F:C1:DD:49;AC:23:3F:C1:DC:EE;AC:23:3F:C1:DD:40;AC:23:3F:C1:DD:51;AC:23:3F:C1:DD:48;AC:23:3F:C1:DD:50;AC:23:3F:C1:DC:D3;AC:23:3F:C1:DD:55;AC:23:3F:C1:DC:D1;AC:23:3F:C1:DC:CB;AC:23:3F:C1:DC:D2;AC:23:3F:C1:DD:31;AC:23:3F:C1:DD:4B;AC:23:3F:C1:DD:4E;AC:23:3F:C1:DC:CD | |||||
| C3:00:00:57:B9:E6;1050;500;1;-76.000;-82.000;-75.000;nan;nan;nan;nan;nan;-80.000;-51.000;-75.000;-64.000;-73.000;-77.000;nan;-76.000 | |||||
| @@ -0,0 +1,2 @@ | |||||
| mac;x;y;z;AC:23:3F:C1:DD:3C;AC:23:3F:C1:DD:49;AC:23:3F:C1:DC:EE;AC:23:3F:C1:DD:40;AC:23:3F:C1:DD:51;AC:23:3F:C1:DD:48;AC:23:3F:C1:DD:50;AC:23:3F:C1:DC:D3;AC:23:3F:C1:DD:55;AC:23:3F:C1:DC:D1;AC:23:3F:C1:DC:CB;AC:23:3F:C1:DC:D2;AC:23:3F:C1:DD:31;AC:23:3F:C1:DD:4B;AC:23:3F:C1:DD:4E;AC:23:3F:C1:DC:CD | |||||
| C3:00:00:57:B9:E8;130;1386;1;-80.000;-82.000;-84.000;nan;-85.000;nan;nan;nan;-76.000;-59.000;-71.000;-64.000;-77.000;-78.000;-85.000;-74.000 | |||||
| @@ -0,0 +1,2 @@ | |||||
| mac;x;y;z;AC:23:3F:C1:DD:3C;AC:23:3F:C1:DD:49;AC:23:3F:C1:DC:EE;AC:23:3F:C1:DD:40;AC:23:3F:C1:DD:51;AC:23:3F:C1:DD:48;AC:23:3F:C1:DD:50;AC:23:3F:C1:DC:D3;AC:23:3F:C1:DD:55;AC:23:3F:C1:DC:D1;AC:23:3F:C1:DC:CB;AC:23:3F:C1:DC:D2;AC:23:3F:C1:DD:31;AC:23:3F:C1:DD:4B;AC:23:3F:C1:DD:4E;AC:23:3F:C1:DC:CD | |||||
| C3:00:00:57:B9:E7;1450;600;1;-83.500;nan;-83.000;nan;nan;nan;nan;nan;-79.000;-48.000;-71.000;-75.000;-77.000;-77.000;-83.000;-80.000 | |||||
| @@ -0,0 +1,2 @@ | |||||
| mac;x;y;z;AC:23:3F:C1:DD:3C;AC:23:3F:C1:DD:49;AC:23:3F:C1:DC:EE;AC:23:3F:C1:DD:40;AC:23:3F:C1:DD:51;AC:23:3F:C1:DD:48;AC:23:3F:C1:DD:50;AC:23:3F:C1:DC:D3;AC:23:3F:C1:DD:55;AC:23:3F:C1:DC:D1;AC:23:3F:C1:DC:CB;AC:23:3F:C1:DC:D2;AC:23:3F:C1:DD:31;AC:23:3F:C1:DD:4B;AC:23:3F:C1:DD:4E;AC:23:3F:C1:DC:CD | |||||
| C3:00:00:57:B9:E8;1800;600;1;-80.000;-82.000;-83.000;nan;nan;nan;nan;nan;-76.000;-59.000;-71.000;-64.000;-76.500;-79.000;-84.500;-72.000 | |||||
| @@ -0,0 +1,2 @@ | |||||
| mac;x;y;z;AC:23:3F:C1:DD:3C;AC:23:3F:C1:DD:49;AC:23:3F:C1:DC:EE;AC:23:3F:C1:DD:40;AC:23:3F:C1:DD:51;AC:23:3F:C1:DD:48;AC:23:3F:C1:DD:50;AC:23:3F:C1:DC:D3;AC:23:3F:C1:DD:55;AC:23:3F:C1:DC:D1;AC:23:3F:C1:DC:CB;AC:23:3F:C1:DC:D2;AC:23:3F:C1:DD:31;AC:23:3F:C1:DD:4B;AC:23:3F:C1:DD:4E;AC:23:3F:C1:DC:CD | |||||
| C3:00:00:57:B9:F1;1900;1400;1;nan;nan;nan;nan;nan;nan;nan;nan;-77.000;nan;-69.000;-65.000;-71.000;-77.000;-82.000;-81.000 | |||||
| @@ -0,0 +1,2 @@ | |||||
| mac;x;y;z;AC:23:3F:C1:DD:3C;AC:23:3F:C1:DD:49;AC:23:3F:C1:DC:EE;AC:23:3F:C1:DD:40;AC:23:3F:C1:DD:51;AC:23:3F:C1:DD:48;AC:23:3F:C1:DD:50;AC:23:3F:C1:DC:D3;AC:23:3F:C1:DD:55;AC:23:3F:C1:DC:D1;AC:23:3F:C1:DC:CB;AC:23:3F:C1:DC:D2;AC:23:3F:C1:DD:31;AC:23:3F:C1:DD:4B;AC:23:3F:C1:DD:4E;AC:23:3F:C1:DC:CD | |||||
| C3:00:00:57:B9:F1;2200;1000;1;nan;nan;nan;nan;nan;nan;nan;nan;-77.000;nan;-69.000;-65.000;-71.000;-78.000;-82.000;-81.000 | |||||
| @@ -0,0 +1,2 @@ | |||||
| mac;x;y;z;AC:23:3F:C1:DD:3C;AC:23:3F:C1:DD:49;AC:23:3F:C1:DC:EE;AC:23:3F:C1:DD:40;AC:23:3F:C1:DD:51;AC:23:3F:C1:DD:48;AC:23:3F:C1:DD:50;AC:23:3F:C1:DC:D3;AC:23:3F:C1:DD:55;AC:23:3F:C1:DC:D1;AC:23:3F:C1:DC:CB;AC:23:3F:C1:DC:D2;AC:23:3F:C1:DD:31;AC:23:3F:C1:DD:4B;AC:23:3F:C1:DD:4E;AC:23:3F:C1:DC:CD | |||||
| C3:00:00:57:B9:E7;2400;300;1;-83.000;nan;-83.000;nan;nan;nan;nan;nan;-78.000;-48.000;-71.000;-76.500;-77.000;-77.000;-83.000;-80.000 | |||||
| @@ -0,0 +1,2 @@ | |||||
| mac;x;y;z;AC:23:3F:C1:DD:3C;AC:23:3F:C1:DD:49;AC:23:3F:C1:DC:EE;AC:23:3F:C1:DD:40;AC:23:3F:C1:DD:51;AC:23:3F:C1:DD:48;AC:23:3F:C1:DD:50;AC:23:3F:C1:DC:D3;AC:23:3F:C1:DD:55;AC:23:3F:C1:DC:D1;AC:23:3F:C1:DC:CB;AC:23:3F:C1:DC:D2;AC:23:3F:C1:DD:31;AC:23:3F:C1:DD:4B;AC:23:3F:C1:DD:4E;AC:23:3F:C1:DC:CD | |||||
| C3:00:00:57:B9:E7;2500;1300;1;-84.000;nan;-83.000;nan;nan;nan;nan;nan;-79.000;-48.000;-71.000;-77.000;-77.000;-77.500;-83.000;-80.000 | |||||
| @@ -0,0 +1,2 @@ | |||||
| mac;x;y;z;AC:23:3F:C1:DD:3C;AC:23:3F:C1:DD:49;AC:23:3F:C1:DC:EE;AC:23:3F:C1:DD:40;AC:23:3F:C1:DD:51;AC:23:3F:C1:DD:48;AC:23:3F:C1:DD:50;AC:23:3F:C1:DC:D3;AC:23:3F:C1:DD:55;AC:23:3F:C1:DC:D1;AC:23:3F:C1:DC:CB;AC:23:3F:C1:DC:D2;AC:23:3F:C1:DD:31;AC:23:3F:C1:DD:4B;AC:23:3F:C1:DD:4E;AC:23:3F:C1:DC:CD | |||||
| C3:00:00:57:B9:E6;2750;200;1;-72.000;-83.000;-76.000;nan;nan;nan;nan;nan;-80.000;-51.000;-74.500;-64.000;-70.000;-78.000;nan;-76.000 | |||||
| @@ -0,0 +1,2 @@ | |||||
| mac;x;y;z;AC:23:3F:C1:DD:3C;AC:23:3F:C1:DD:49;AC:23:3F:C1:DC:EE;AC:23:3F:C1:DD:40;AC:23:3F:C1:DD:51;AC:23:3F:C1:DD:48;AC:23:3F:C1:DD:50;AC:23:3F:C1:DC:D3;AC:23:3F:C1:DD:55;AC:23:3F:C1:DC:D1;AC:23:3F:C1:DC:CB;AC:23:3F:C1:DC:D2;AC:23:3F:C1:DD:31;AC:23:3F:C1:DD:4B;AC:23:3F:C1:DD:4E;AC:23:3F:C1:DC:CD | |||||
| C3:00:00:57:B9:E6;950;750;1;-72.000;-82.000;-75.500;nan;nan;nan;nan;nan;-80.000;-52.000;-74.000;-64.000;-71.500;-77.000;nan;-76.000 | |||||
| @@ -3,6 +3,7 @@ services: | |||||
| build: . | build: . | ||||
| image: ble-ai-localizer:0.1.0 | image: ble-ai-localizer:0.1.0 | ||||
| environment: | environment: | ||||
| PYTHONWARNINGS: "ignore:Unverified HTTPS request" | |||||
| CONFIG: "/config/config.yaml" # Nome usato da main.py | CONFIG: "/config/config.yaml" # Nome usato da main.py | ||||
| SECRETS_FILE: "/config/secrets.yaml" | SECRETS_FILE: "/config/secrets.yaml" | ||||
| UI_USER: "Admin" | UI_USER: "Admin" | ||||
| @@ -1,14 +1,25 @@ | |||||
| pandas==2.2.2 | |||||
| # --- CORE AI & DATA --- | |||||
| numpy==1.26.4 | numpy==1.26.4 | ||||
| scikit-learn==1.5.1 | scikit-learn==1.5.1 | ||||
| joblib==1.4.2 | joblib==1.4.2 | ||||
| pandas==2.2.2 | |||||
| pyarrow==15.0.0 | |||||
| # --- COMUNICAZIONE & SISTEMA --- | |||||
| paho-mqtt==2.1.0 | paho-mqtt==2.1.0 | ||||
| requests==2.32.3 | requests==2.32.3 | ||||
| PyYAML==6.0.2 | PyYAML==6.0.2 | ||||
| psutil==5.9.8 | |||||
| pathlib==1.0.1 | |||||
| # --- WEB & MAPPE --- | |||||
| streamlit==1.49.1 | |||||
| pillow==10.2.0 | |||||
| fastapi==0.115.0 | fastapi==0.115.0 | ||||
| uvicorn[standard]==0.30.6 | uvicorn[standard]==0.30.6 | ||||
| folium==0.15.1 | |||||
| streamlit-folium==0.26.1 | |||||
| # --- VISUALIZZAZIONE --- | |||||
| matplotlib==3.9.2 | matplotlib==3.9.2 | ||||
| seaborn==0.13.2 | seaborn==0.13.2 | ||||