/* Quiet Bee - SHT85 + ESP32 classique + SIM7080, 24/10/2026.
   UART ESP32 RX16 <- TX modem ; TX17 -> RX modem (niveaux adaptes).
   Parcours M5Stamp CatM S003 : 5V/GND, TX/RX externes adaptes 3.3V.
   POWERKEY/STATUS internes non exposes ; aucun fil de commande a ajouter.
   GPIO25 (relais) et GPIO26 (buzzer) reserves, non pilotes.
   UDP signe HMAC-SHA256, NON chiffre. Voir guide et collector.py.
   Arduino ESP32 3.x : Wire, Preferences, mbedtls sont fournis par le core.
*/
#include <Arduino.h>
#include <Wire.h>
#include <Preferences.h>
#include <esp_sleep.h>
#include <esp_system.h>
#include <mbedtls/md.h>

// Renseigner APN du portail 1NCE : sensor.net OU iot.1nce.net.
constexpr char APN[] = "";
constexpr char SERVER[] = ""; // IPv4/nom DNS du collecteur fourni, pas un site HTTP.
constexpr uint16_t PORT = 9001; // choix de ce projet, a ouvrir en UDP sur le serveur.
constexpr char DEVICE[] = "station01"; // identifiant technique, sans localisation.
constexpr char KEY[] = ""; // 64 caracteres hex aleatoires, identiques au collecteur.
constexpr bool HARDWARE_VERIFIED = false; // apres controle schema et niveaux UART.
constexpr bool DEEP_SLEEP = false; // ESP32 seul ; modem reste alimente, radio CFUN=0.
constexpr uint32_t PERIOD_MS = 600000; // choix pedagogique : 10 minutes APRES cycle.
constexpr uint32_t UART_BAUD = 115200; // a confirmer sur votre modem.
constexpr int RX_PIN=16, TX_PIN=17, SDA_PIN=21, SCL_PIN=22;
// GPIO25 : reserve commande d'alimentation ; GPIO26 : reserve buzzer.
// Ils restent en entree. POWERKEY et STATUS ne sont pas assignes.

HardwareSerial modem(2);
Preferences prefs;
enum State {DISABLED, IDLE, SENSOR_WAIT, BOOT, REG, PDP, OPEN, SEND, ACK,
            CLOSE, DEACT, RADIO_OFF, REST};
State state=DISABLED;
uint32_t due=0, cycleStart=0, regStart=0, pdpStart=0, cmdAt=0, cmdTimeout=0, backoff=60000;
uint8_t step=0, sensorTries=0;
bool busy=false, done=false, good=false, okSeen=false, prompt=false, requireData=false;
bool dataSeen=false, registered=false, pdpActive=false, socketOpen=false, acknowledged=false;
String line, response, expected, pending, packet;
uint64_t sequence=0;

bool reached(uint32_t t) { return int32_t(millis()-t)>=0; }
uint8_t crc8(const uint8_t *p, size_t n) {
  uint8_t c=0xFF;
  while(n--) {c^=*p++; for(int i=0;i<8;i++) c=(c&0x80)?(c<<1)^0x31:c<<1;}
  return c;
}
String sign(const String& value) {
  uint8_t out[32]; char hex[65];
  const auto *info=mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
  if(!info || mbedtls_md_hmac(info,(const uint8_t*)KEY,strlen(KEY),
      (const uint8_t*)value.c_str(),value.length(),out)!=0) return "";
  for(int i=0;i<32;i++) snprintf(hex+2*i,3,"%02x",out[i]);
  return String(hex);
}
void finish(bool success) { busy=false; done=true; good=success; }
void onLine(String s) {
  s.trim(); if(!s.length()) return;
  // Pas de trace brute du trafic : la cle n'est jamais envoyee ni journalisee.
  if(s.startsWith("ack|")) {
    const String body="ack|"+String(DEVICE)+"|"+String((unsigned long long)sequence);
    const String mac=sign(body), want=body+"|"+mac;
    if(mac.length()==64 && s.length()==want.length()) {
      unsigned diff=0; for(size_t i=0;i<s.length();i++) diff|=s[i]^want[i];
      if(!diff) acknowledged=true;
    }
  }
  if(!busy) return;
  if(s=="ERROR" || s.startsWith("+CME ERROR:") || s.startsWith("+CMS ERROR:")) {
    finish(false); return;
  }
  if(response.length()+s.length()<1600) response+=s+"\n";
  else {finish(false);return;}
  if(expected.length() && s.startsWith(expected)) dataSeen=true;
  if(s=="OK") okSeen=true;
  if(okSeen && (!requireData || dataSeen)) finish(true);
}
void pollModem() {
  int budget=512;
  while(modem.available() && budget-->0) {
    char c=modem.read();
    if(busy && prompt && c=='>') {
      modem.write((const uint8_t*)packet.c_str(),packet.length());
      prompt=false; line=""; continue;
    }
    if(c=='\n') {onLine(line);line="";}
    else if(c!='\r') {if(line.length()<512) line+=c;else {line="";if(busy)finish(false);}}
  }
  if(busy && millis()-cmdAt>=cmdTimeout) finish(false);
}
void command(const String& s,uint32_t timeout=10000,const String& prefix="",bool askPrompt=false) {
  response="";line="";expected=prefix;requireData=prefix.length()>0;
  okSeen=false;dataSeen=false;done=false;good=false;busy=true;prompt=askPrompt;
  cmdAt=millis();cmdTimeout=timeout;modem.print(s);modem.print('\r');
}
void enter(State s) {state=s;step=0;done=false;due=millis();}
void rest(uint32_t waitMs) {
  if(DEEP_SLEEP) { Serial.flush();esp_sleep_enable_timer_wakeup(uint64_t(waitMs)*1000ULL);esp_deep_sleep_start(); }
  enter(REST);due=millis()+waitMs;
}
void failed() {
  Serial.println("Cycle interrompu : mesure conservee ; nouvelle tentative differee.");
  busy=false;enter(CLOSE); // nettoyage best effort puis radio reduite.
}
void sensorStart() {
  Wire.beginTransmission(0x44);Wire.write(0x24);Wire.write(0x00);
  if(Wire.endTransmission()!=0) {
    if(++sensorTries<3) {due=millis()+100;return;}
    Serial.println("SHT85 absent : aucune valeur fabriquee.");rest(PERIOD_MS);return;
  }
  enter(SENSOR_WAIT);due=millis()+20;
}
void saveSample() {
  uint8_t b[6];
  bool valid=Wire.requestFrom(uint8_t(0x44),uint8_t(6))==6;
  if(valid) for(auto &v:b) v=Wire.read();
  if(!valid || crc8(b,2)!=b[2] || crc8(b+3,2)!=b[5]) {
    Serial.println("Lecture SHT85 invalide (I2C/CRC).");
    if(++sensorTries<3) {enter(IDLE);due=millis()+100;}else rest(PERIOD_MS);
    return;
  }
  float t=-45.0f+175.0f*((b[0]<<8)|b[1])/65535.0f;
  float h=100.0f*((b[3]<<8)|b[4])/65535.0f;
  sequence=prefs.getULong64("seq",0)+1;
  if(!prefs.putULong64("seq",sequence)) {rest(PERIOD_MS);return;}
  pending="v1|"+String(DEVICE)+"|"+String((unsigned long long)sequence)+"|"+String(t,2)+"|"+String(h,2);
  if(!prefs.putString("pending",pending)) {pending="";rest(PERIOD_MS);return;}
  Serial.printf("Mesure valide : %.2f C, %.2f %%HR\n",t,h);
  enter(BOOT);
}
void setup() {
  Serial.begin(115200);pinMode(25,INPUT);pinMode(26,INPUT);
  bool configOK=HARDWARE_VERIFIED && strlen(APN)>0 && strlen(SERVER)>0 && strlen(SERVER)<=64 && strlen(KEY)==64;
  for(const char *p=KEY;*p;p++) if(!isxdigit((unsigned char)*p)) configOK=false;
  for(const char *p=APN;*p;p++) if(!isalnum((unsigned char)*p)&&*p!='.'&&*p!='-') configOK=false;
  for(const char *p=SERVER;*p;p++) if(!isalnum((unsigned char)*p)&&*p!='.'&&*p!='-') configOK=false;
  for(const char *p=DEVICE;*p;p++) if(!isalnum((unsigned char)*p)&&*p!='_'&&*p!='-') configOK=false;
  if(strlen(DEVICE)<1 || strlen(DEVICE)>32)configOK=false;
  if(!configOK) {Serial.println("Configurer carte, APN, collecteur et cle avant utilisation.");return;}
  Wire.begin(SDA_PIN,SCL_PIN);Wire.setClock(100000);Wire.setTimeOut(50);
  modem.begin(UART_BAUD,SERIAL_8N1,RX_PIN,TX_PIN);modem.setTimeout(50);
  if(!prefs.begin("terrain",false)) {Serial.println("NVS indisponible.");return;}
  pending=prefs.getString("pending","");sequence=prefs.getULong64("seq",0);
  backoff=constrain(prefs.getUInt("backoff",60000),uint32_t(60000),uint32_t(1800000));
  cycleStart=millis();enter(pending.length()?BOOT:IDLE);due=millis()+50;
}
void loop() {
  pollModem(); if(state==DISABLED)return;
  if(state>=BOOT && state<=ACK && millis()-cycleStart>240000) {failed();return;}
  if(busy)return;
  if(state==IDLE) {if(reached(due))sensorStart();return;}
  if(state==SENSOR_WAIT) {if(reached(due))saveSample();return;}
  if(state==BOOT) {
    if(done) {
      if(!good || (step==4 && response.indexOf("+CPIN: READY")<0)) {failed();return;}
      done=false;step++;
    }
    switch(step) {
      case 0:command("AT");break;
      case 1:command("ATE0");break;
      case 2:command("AT+CMEE=2");break;
      case 3:command("AT+CFUN=1",60000);break;
      case 4:command("AT+CPIN?",10000,"+CPIN:");break;
      case 5:command("AT+CNMP=38");break;
      case 6:command("AT+CMNB=1");break;
      case 7:command("AT+CEREG=0");break; // interrogation, pas d'URC ambigue.
      case 8:command("AT+CGDCONT=1,\"IP\",\""+String(APN)+"\"");break;
      case 9:command("AT+CNCFG=0,1,\""+String(APN)+"\",\"\",\"\",1");break;
      default:regStart=millis();enter(REG);break;
    }
    return;
  }
  if(state==REG) {
    if(done) {
      int n=-1,stat=-1;int at=response.indexOf("+CEREG:");
      if(good && at>=0)sscanf(response.c_str()+at,"+CEREG: %d,%d",&n,&stat);
      done=false;registered=stat==1||stat==5;
      if(registered){pdpStart=millis();enter(PDP);return;}
      if(stat==3 || millis()-regStart>180000){failed();return;}
      due=millis()+3000;
    }
    if(reached(due))command("AT+CEREG?",10000,"+CEREG:");return;
  }
  if(state==PDP) {
    if(done) {
      if(!good){failed();return;}done=false;
      if(step==1) {
        pdpActive=response.indexOf("+CNACT: 0,1,\"")>=0 && response.indexOf("+CNACT: 0,1,\"0.0.0.0\"")<0;
        if(pdpActive){enter(OPEN);return;}
        if(millis()-pdpStart>90000){failed();return;}
        due=millis()+2000;
      } else {
        step=1;due=millis()+500;
      }
    }
    if(step==0)command("AT+CNACT=0,1",60000);
    else if(reached(due))command("AT+CNACT?",10000,"+CNACT:");return;
  }
  if(state==OPEN) {
    if(done){int cid=-1,result=-1;int at=response.indexOf("+CAOPEN:");if(at>=0)sscanf(response.c_str()+at,"+CAOPEN: %d,%d",&cid,&result);socketOpen=good&&cid==0&&result==0;if(!socketOpen){failed();return;}enter(SEND);return;}
    command("AT+CAOPEN=0,0,\"UDP\",\""+String(SERVER)+"\","+String(PORT)+",1",60000,"+CAOPEN:");return;
  }
  if(state==SEND) {
    if(done){if(!good){failed();return;}enter(ACK);due=millis()+15000;return;}
    String mac=sign(pending);if(mac.length()!=64){failed();return;}
    packet=pending+"|"+mac+"\n";
    command("AT+CASEND=0,"+String(packet.length())+",5000",15000,"",true);return;
  }
  if(state==ACK) {
    if(acknowledged) {
      // Effacer seulement APRES ACK signe : coupure avant effacement => doublon deduplique.
      prefs.remove("pending");pending="";backoff=60000;prefs.putUInt("backoff",backoff);Serial.println("Mesure recue et confirmee par le serveur.");enter(CLOSE);
    } else if(reached(due))failed();
    return;
  }
  if(state==CLOSE) {
    if(done){enter(DEACT);return;}command("AT+CACLOSE=0",10000);return;
  }
  if(state==DEACT) {
    if(done){enter(RADIO_OFF);return;}command("AT+CNACT=0,0",20000);return;
  }
  if(state==RADIO_OFF) {
    if(done) {
      bool radioOff=good;uint32_t waitMs=acknowledged?PERIOD_MS:backoff+(esp_random()%10000);
      if(!acknowledged){backoff=min(backoff*2,uint32_t(1800000));prefs.putUInt("backoff",backoff);}
      // Pas de sommeil profond si la reduction radio n'a pas ete confirmee.
      if(DEEP_SLEEP && radioOff)rest(waitMs);
      else {enter(REST);due=millis()+waitMs;}
      return;
    }
    command("AT+CFUN=0",20000);return;
  }
  if(state==REST && reached(due)) {
    cycleStart=millis();sensorTries=0;acknowledged=false;
    registered=false;pdpActive=false;socketOpen=false;
    enter(pending.length()?BOOT:IDLE);
  }
}
