A la demande: en plus du sentinel 0xFFFF dans le champ CO2, on remonte le
defaut via error_flags byte 66 bit 7.
- ERR_CO2 = 0x80 (bit 7, double sens vent/CO2 selon device_type cote serveur)
- s88_disconnected -> error_flags |= ERR_CO2 dans SARA_send_data_v2.py
- error_flags.md: bit 7 documente comme WIND_ERROR / CO2_ERROR
Confirme par le user: le serveur lit le bit 7 comme CO2_ERROR sur ces unites.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Probleme vu sur pro100: sonde S88 muette (panne cablage) mais write_data.py
n'ecrivait rien -> la base gardait la derniere valeur (487 ppm d'hier) et la
loop d'envoi la transmettait en boucle.
- data_S88: nouvelle colonne s88_status (0=OK, 0xFF=sonde muette), comme
npm_status/noise_status. Migration via create_db.py + set_config.py + self-heal.
- S88/write_data.py: ecrit DESORMAIS une ligne a chaque cycle (CO2=0 + 0xFF si
pas de reponse). Connexion SQLite timeout=10 (anti database-is-locked).
- SARA_send_data_v2.py: lit s88_status; si 0xFF -> bytes 81-82 restent 0xFFFF
(CO2 absent) au lieu d'envoyer une valeur perimee. Compatible bases non migrees.
- database.html + launcher.php: badge statut + colonne dans les exports CSV.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Vérif terrain sur pro100 : à 100 kHz le CCS811 renvoie des valeurs corrompues
0x8000 (32768) par clock-stretching, et le modèle oneshot-reset-toutes-les-10s
ne donne que le 1er échantillon post-init (garbage). Refonte :
- CCS811/daemon.py: service long-running (Type=simple, Restart=always). Init 1x,
boucle lecture/écriture 10s, filtre eCO2 dans [400,8192], re-init auto sur
erreurs I2C répétées. Remplace write_data.py (supprimé).
- CCS811/get_data.py: lit la dernière ligne data_CCS811 au lieu du capteur
(évite la collision I2C avec le daemon -> corruption observée).
- setup_services.sh: service daemon + self-heal suppression de l'ancien .timer;
activation hors boucle timers.
- launcher.php: .timer -> .service (map statut + allowedServices x2).
- update_firmware.sh: redémarre le daemon à l'OTA.
- doc: README (archi daemon + I2C 10kHz confirmé requis), CLAUDE.md, changelog.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Découvert en vérif SSH sur nebuleair-pro100 : le timer CCS811 échouait en
ModuleNotFoundError car l'OTA fait git pull mais ne réinstallait jamais les
dépendances pip (installation_part1.sh ne tourne qu'à l'install neuve).
- requirements.txt: source unique de vérité des deps Python
- installation_part1.sh: install via requirements.txt (chemin relatif au script,
le repo n'est pas encore cloné dans /var/www à cette étape)
- update_firmware.sh: nouvelle étape 2a, pip install -r requirements.txt
(idempotent) -> les capteurs déjà déployés récupèrent les libs manquantes à l'OTA
- CCS811/write_data.py + get_data.py: skip des lectures eCO2 < 400 ppm
(échantillon 0/0 parasite juste après init du driver, plancher physique = 400)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Nouveau capteur de qualité d'air CCS811 sur le bus I2C, calqué sur le
pattern S88 (local-only, pas encore dans le payload de transmission).
- CCS811/get_data.py (lecture live) + write_data.py (timer 10s, self-heal table)
- table data_CCS811 (timestamp, eCO2, TVOC) dans create_db.py
- config CCS811 (bool) + CCS811_address (0x5A/0x5B, défaut 0x5A) dans set_config.py
- service+timer systemd nebuleair-ccs811-data (10s) + ajout boucle d'activation
- admin.html: case d'activation + dropdown adresse I2C
- sensors.html: carte Get Data (TVOC + eCO2)
- database.html + launcher.php: consultation/export/stats data_CCS811
- lib adafruit-circuitpython-ccs811 dans installation_part1.sh
- CCS811/README.md: câblage, adresses, warning clock-stretching I2C sur Pi
- CLAUDE.md + changelog mis à jour
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Protège contre le cas où l'OTA n'appelle pas create_db.py (problème
bash classique: le script d'OTA est chargé en mémoire au lancement,
le git pull du step 1 met à jour le fichier sur disque mais bash
continue d'exécuter l'ancienne version).
Le script crée la table data_S88 lui-même au démarrage. Schéma dupliqué
de create_db.py — à garder synchro. Pattern à appliquer aux futurs
capteurs pour éviter cette classe de bug.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Les MAJ OTA ne lancent pas create_db.py, donc toute nouvelle table
ajoutée par une release (ex: data_S88) reste inexistante en base.
Les timers tournent, le script s'exécute, mais l'INSERT échoue
silencieusement avec 'no such table' — capturé par le try/except,
exit 0, systemd voit success.
Symptôme observé sur les capteurs avec S88 activé: 'Get Data' marche
(live read), mais 'Mesures CO2 (Senseair S88)' montre 'Aucune donnée
disponible dans cette table'.
Fix: create_db.py est appelé en step 2 juste avant set_config.py
dans les deux scripts d'update (git pull et upload fichier).
Idempotent (CREATE IF NOT EXISTS + ALTER ADD COLUMN in try/except).
Self-bootstrap: après cette OTA, tous les capteurs auront toutes
les tables, y compris celles introduites dans des releases passées.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refonte des boutons 'Consulter la base de donnée': ils ouvrent
désormais un grand modal Bootstrap (modal-xl scrollable) avec
pagination 20 lignes/page (Précédent/Suivant + indicateur de plage).
Le dropdown 'Nombre de mesures' est supprimé.
Ajout des boutons Senseair S88 dans les 3 cartes pointant sur
data_S88, et renommage du bouton MH-Z19 pour le distinguer.
Backend: sqlite/read.py accepte un OFFSET optionnel (3e argument,
défaut 0) et launcher.php endpoint table_mesure transmet ?offset=N.
Rétrocompatible.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Le sélecteur affiche 'port NPM1 (/dev/ttyAMA5)' au lieu de juste
'/dev/ttyAMA5', pour matcher le silkscreen de la PCB et éviter
les erreurs de branchement. ttyAMA0 et ttyAMA2 (SARA) sont retirés.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ajoute un dropdown ttyAMA0/2/3/4/5 sous la checkbox 'Send CO2
sensor data (Senseair S88)' qui modifie S88_port en base.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
read_co2() lit IR1..IR4 en une trame (status + CO2) à 9600 8N1,
adresse 0xFE 'any address', avec vérification CRC16-Modbus et rejet
de la mesure si status non-nul (warm-up ou erreur).
CRC requête/réponse validés contre les exemples du datasheet TDE14367.
Doc protocole consolidée dans S88/README.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Table data_S88, flag config S88 + port configurable S88_port
(default /dev/ttyAMA5), service/timer systemd 10s, carte
sensors.html, endpoint launcher.php, toggle admin.html.
read_co2() est un stub NotImplementedError en attente du datasheet.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Quand AT+CSQ ne repond pas (hoquet serie temporaire), retente le
AT+CSQ jusqu'a 3x (0.5s d'intervalle) avant d'escalader. Si le modem
repond, le flux normal reprend sans reboot -> evite les coupures
d'alim GPIO et l'usure du modem pour une absence de reponse ponctuelle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ajoute un bouton à côté de 'Run Self Test' (4 pages) qui lance
uniquement le check sous-tension dans un petit modal dédié, sans
dérouler tout le Self Test. Réutilise l'endpoint type=throttled.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ajoute un test 'Power Supply' au Self Test pour détecter une
sous-tension du Pi (cause fréquente de capteurs USB instables,
corruptions SD, reboots). Endpoint launcher.php?type=throttled
+ script power/get_throttled.py (lancé via sudo python3, déjà
whitelisté — pas de modif sudoers). Affiché en tête des résultats
et dans le rapport copiable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Le NSRT MK4 (USB CDC-ACM) se ré-énumère ~1s par cycle sur le pro150
(dmesg: USB disconnect/reconnect en boucle), ce qui faisait échouer
l'ouverture de /dev/ttyACM0 et enregistrait des points isolés 0.0
"Déconnecté". On retente jusqu'à 3 fois (1s d'intervalle) avant de
conclure à une déconnexion. Cause racine matérielle (alim/câble USB)
à traiter en parallèle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Fix Envea Gas Sensors: scan physique via detect_envea_device (read_ref.py)
sur ttyAMA3/4/5 au lieu de juste vérifier envea_sondes_table.connected=1.
L'ancien check disait Passed même sans sonde branchée car read_value_v2.py -d
imprime un en-tête de debug non vide et utilise "Failed" pas "error".
- Ajout Firmware Version dans les logs et le rapport (via firmware_version
déjà retourné par get_config_sqlite, pas d'AJAX supplémentaire).
- Renommage titre modal "Modem Self Test" -> "Self Test" (couvre aussi
capteurs et RTC, pas uniquement le modem).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Au lieu d'afficher l'erreur Apache "404 Not Found" qui faisait croire
à un bug, le modal indique maintenant que le log n'existe que lorsqu'une
tentative de connexion WiFi a déjà eu lieu. Inclut aussi un échappement
HTML basique du contenu et limite l'affichage aux 1000 dernières lignes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ajoute un bouton 'WiFi connect logs' dans logs.html qui ouvre un modal
avec le contenu de logs/wifi_connect.log (créé en v1.9.3). Permet de
diagnostiquer une tentative de connexion WiFi sans passer en SSH.
Le contenu est chargé à l'ouverture du modal (pas au chargement de la
page) pour éviter de saturer la limite de 6 connexions du navigateur
sur la page Journal.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bugs corrigés:
- launcher.php passait SSID/PASS au shell sans escapeshellarg(): un
mot de passe avec $/&/;/espace cassait silencieusement la commande
avant que nmcli ne soit appelé. Cause probable des retours clients
"ça bloque au cliquer sur Se connecter".
- wifi.html n'encodait pas SSID/PASS dans l'URL: caractères &/+/=
corrompaient la requête.
Observabilité:
- Nouveau fichier logs/wifi_connect.log avec timestamps stricts
- launcher.php log la requête entrante (IP, longueurs SSID/PASS)
- connexion.sh: fonction log_wc(), snapshots NM avant/après,
capture stdout+stderr nmcli, code retour explicite, fallback SSID
dérivé du serial si deviceName indisponible.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- boot_hotspot.sh: busy timeout 5s sur les requêtes SQLite
- boot_hotspot.sh: SSID fallback nebuleair-pro-<serial> si deviceName vide
- Corrige le cas où le hotspot ne démarrait pas quand la DB était
lockée par les timers systemd au boot (SSID vide → nmcli refuse)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- admin.html: nouveau card 'Reseau Tailscale' avec statut connecte/deconnecte,
IP tailnet, hostname, serveur Headscale et bouton Actualiser
- admin.html: bloc deroulant pour consulter les 50 dernieres lignes du log
bootstrap (logs/tailscale_bootstrap.log)
- launcher.php: nouvelles actions get_tailscale_info (status + IP + hostname
via sudo tailscale ip/status) et get_tailscale_log (tail -n 50)
Complete la v1.9.0 avec la visibilite UI necessaire pour valider/diagnostiquer
la connexion Tailscale sur chaque capteur sans avoir a passer en SSH.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- installation_part1.sh: install paquet Tailscale + sudoers /usr/bin/tailscale
- services/tailscale_bootstrap.sh (nouveau): script idempotent d'enrolement au boot
- services/setup_services.sh: service systemd nebuleair-tailscale-bootstrap (one-shot)
- update_firmware.sh: nouvelle etape 3d 'Bootstrap Tailscale' (self-heal install + fetch
authkey depuis data.nebuleair.fr/pro_4G/get_tailscale_key.php + enrolement). Fallback
HTTPS->HTTP en attendant le cert TLS cote serveur.
Permet l'acces SSH distant aux 200 capteurs deployes via le tailnet une fois que leur
client a clique sur 'Update' dans l'admin web. Necessite l'endpoint serveur
get_tailscale_key.php en place sur data.nebuleair.fr (a deployer en parallele cote
AirCarto, auth par deviceID + rate limit + audit log).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ajoute la version firmware sous le nom du capteur dans la sidebar, visible
sur toutes les pages. Permet d'identifier d'un coup d'oeil le chemin de
mise a jour disponible (online git pull vs offline ZIP upload >= v1.4.0).
- launcher.php: get_config_sqlite injecte firmware_version (lu depuis VERSION)
- sidebar.html: ajout d'un <small> sous sideBar_sensorName (statique)
- topbar-logo.js: peuple .sideBar_firmwareVersion via le fetch existant +
MutationObserver (aucun nouveau fetch -> respecte la limite 6 connexions)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sur les anciens capteurs sans regle sudoers NOPASSWD pour
/var/www/nebuleair_pro_4g/*, l'update echouait avec un message
sudo cryptique.
Nouveau:
- preflight_sudo_check() en PHP teste 'sudo -n -l <script>' avant
de lancer l'update (online ou offline)
- Si KO: la route retourne error_type=sudoers_missing avec un
message clair et la sortie technique de sudo
- L'UI affiche une alerte warning structuree avec etapes numerotees,
contenu du fichier /etc/sudoers.d/nebuleair pret a coller, et un
bouton 'Copier le contenu' (presse-papier)
- Echec immediat (<1s) au lieu d'attendre le timeout du script
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
L'upload offline avait deux defauts vs l'update online:
- pas de self-heal des services (pas de Step 3c equivalent)
- ancienne UX synchrone (spinner sans feedback pendant 60-90s)
Maintenant:
- update_firmware_from_file.sh: nouveau Step 4c qui appelle
setup_services.sh (alignement avec online)
- launcher.php upload_firmware: lance le script en background et
reutilise le mecanisme log/done de l'update online
- admin.html uploadFirmware: apres l'upload du ZIP, bascule sur
le meme systeme de polling/progress que l'online (avec mapping
d'etapes specifique au script offline)
- Detection de fin par substring 'completed successfully!' (matche
les 2 markers finaux differents)
Fix au passage: le bouton 'Upload & Install' restait bloque sur
'Installing...' apres succes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
L'ancien flow etait un AJAX bloquant qui attendait ~90s sans aucun
retour visuel autre qu'un spinner.
Nouveau flow:
- Backend: launcher.php lance update_firmware.sh en background
(route update_firmware_start) et expose une route de polling
incremental (update_firmware_progress) avec offset.
- Frontend: progress bar Bootstrap animee + label de l'etape en
cours + timer mm:ss / estimation, plus streaming des logs
toutes les 700ms.
- Sous-etape Step 3c (la plus longue): interpolation fine de la
progression en comptant les 'Started X' (services demarres).
- Logs techniques masques par defaut dans <details>, ouverts
automatiquement en cas d'echec pour faciliter le debug.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Les requetes MIN/MAX(timestamp) sur les tables capteur etaient
faussees par les lignes accumulees pendant le bug RTC (v1.7.4):
la chaine 'not connected' > toute date ISO en tri ASCII, donc
MAX() retournait 'not connected' au lieu de la vraie date la
plus recente.
Fix: WHERE timestamp != 'not connected' dans les requetes MIN/MAX.
Les lignes 'not connected' restent en base, elles sont juste
ignorees pour le calcul des bornes temporelles affichees.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ajout dans le tableau admin et dans les listes allowed restart/toggle:
- rtc_save_to_db.service
- nebuleair-wifi-powersave.timer
- nebuleair-cpu-power.service
Aussi: nebuleair-noise-data.timer etait dans get_systemd_services
mais absent des listes restart/toggle (les boutons n'auraient pas
fonctionne). Corrige.
Nouveau: support d'un display_name explicite par service (override
optionnel) pour les noms qui ne suivent pas la convention
'nebuleair-*-data.timer'.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Le check -x echouait car git config core.fileMode=false (defini dans
installation_part1.sh) strippe le bit executable. Resultat: Step 3c
logguait 'not found or not executable, skipping' au lieu de
reellement appeler setup_services.sh.
Fix: chmod +x avant l'execution, comme le fait deja
installation_part2.sh sur le meme script.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
setup_services.sh devient la source unique pour les services systemd
(le service rtc_save_to_db etait auparavant cree inline dans
installation_part2.sh, en doublon avec un commentaire dans save_to_db.py).
update_firmware.sh appelle maintenant setup_services.sh apres le git
pull. Resultat: les capteurs deja deployes peuvent se reparer tout
seuls au prochain update firmware (services manquants, masques, ou
nouveaux services ajoutes au repo apres l'installation initiale).
Defensif: systemctl unmask sur rtc_save_to_db avant creation du
fichier .service, pour eviter d'ecrire dans /dev/null si le service
avait ete masque (cas observe sur un capteur en production).
Pas de risque sur les capteurs sains: reecriture des .service avec
le meme contenu, comportement inchange.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Le check SIM sur CSQ=99 ajoutait 2 commandes AT a chaque coupure
signal temporaire (frequent en zone marginale) pour un gain
marginal: le check sur la branche USOCR/PDP attrape le cas avec
un delai de 1-2 cycles max (le modem sans SIM alterne CSQ=99
et CSQ>0).
On garde uniquement le check v1.7.1 dans la branche USOCR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sans SIM, le modem retourne CSQ=99 systematique et le script
sortait avant d'atteindre le check SIM ajoute en v1.7.1.
- Branche CSQ=99: appelle aussi check_sim_status()
- Si SIM absente: alerte rouge + notification WiFi
- Si SIM OK: message vert (coupure reseau temporaire)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Meme modele que NPM: ecriture en base avec valeurs a 0 et noise_status=0xFF
si capteur deconnecte, flag ERR_NOISE (bit 5) dans byte 66 UDP, messages
explicites sur page capteurs et self-test.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Lecture npm_status depuis derniere mesure en base (rowid DESC, pas de moyenne)
- Independant du RTC (pas de dependance au timestamp)
- Byte 67 du payload UDP = registre status NextPM
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- NPM: mode --dry-run (print JSON sans ecriture en base)
- launcher.php: endpoint npm appelle get_data_modbus_v3.py --dry-run
- sensors.html: affichage PM + temp + humidite + status NPM decode
- Suppression unite ug/m3 sur le champ status
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Lecture fichier VERSION et pack major.minor.patch dans bytes 69-71
- README: documentation complete structure 100 bytes + conso data
- Changelog mis a jour
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Bytes 66-68 (error_flags, npm_status, device_status) initialises a 0x00
au lieu de 0xFF pour eviter faux positifs cote serveur
- Implementation flag RTC (byte 66) + methodes SensorPayload
- Escalade PDP reset: si echec → notification + hardware reboot + exit
- Changelog et VERSION mis a jour
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extraction du code self-test dans des fichiers partagés (selftest.js +
selftest-modal.html) pour éviter la duplication. Ajout du bouton Run
Self Test sur les pages index, sensors et admin. Nouveau test RTC qui
vérifie la connexion du module DS3231 et la synchronisation horloge.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>