Differences

This shows you the differences between two versions of the page.

Link to this comparison view

iothings:laboratoare:2025_code:lab7_3 [2025/11/06 18:26] (current)
dan.tudose created
Line 1: Line 1:
 +<code C main.cpp>​
 +// src/​main.cpp
 +// ESP32-C6 Sparrow: supports LTR303 or LTR308 digital light sensors
 +// - Auto-detects which sensor is present
 +// - LTR303: uses CH0 counts (robust brightness metric)
 +// - LTR308: computes approximate lux using datasheet scaling
 +// - Normalizes, runs online k-means, and maps to room-state labels
 +//
 +// PlatformIO deps (platformio.ini):​
 +// lib_deps =
 +//   ​adafruit/​Adafruit LTR329 and LTR303@^3.0.1
 +//   ​https://​github.com/​DFRobot/​DFRobot_LTR308.git
  
 +#include <​Arduino.h>​
 +#include <​Wire.h>​
 +#include <​Preferences.h>​
 +#include <​math.h>​
 +
 +// --- Sparrow I2C pins ---
 +static const int SDA_PIN = 21;
 +static const int SCL_PIN = 22;
 +// Optional INT pin (not required for basic reads)
 +static const int LTR_INT_PIN = 15;
 +
 +// --- Sensor libraries ---
 +#include <​Adafruit_LTR329_LTR303.h> ​ // LTR303 @ 0x29
 +#include <​DFRobot_LTR308.h> ​         // LTR308 @ 0x53
 +
 +// ---------------- Sensor abstraction ----------------
 +enum class SensorType { NONE, LTR303, LTR308 };
 +SensorType gType = SensorType::​NONE;​
 +
 +Adafruit_LTR303 g303;
 +DFRobot_LTR308 ​ g308;
 +
 +// Track LTR308 configuration we apply (so lux math is consistent)
 +DFRobot_LTR308::​eResolution_t g308_res ​ = DFRobot_LTR308::​eConversion_100ms_18b;​
 +// Numeric gain we set {1,​3,​6,​9,​18}
 +uint8_t g308_gain = 3;
 +
 +// Try to initialize either sensor
 +bool sensorBegin() {
 +  // Try LTR303 first (I2C addr 0x29)
 +  if (g303.begin(&​Wire)) {
 +    g303.setGain(LTR3XX_GAIN_1);​
 +    g303.setIntegrationTime(LTR3XX_INTEGTIME_100);​
 +    g303.setMeasurementRate(LTR3XX_MEASRATE_100);​
 +    gType = SensorType::​LTR303;​
 +    return true;
 +  }
 +  // Then LTR308 (I2C addr 0x53)
 +  if (g308.begin()) {
 +    g308_res ​ = DFRobot_LTR308::​eConversion_100ms_18b;​
 +    g308_gain = 3; // 3x is a good default indoors
 +    g308.setMeasurementRate(g308_res,​ DFRobot_LTR308::​eRate_100ms);​
 +    g308.setGain(DFRobot_LTR308::​eGain_3X);​
 +    g308.setPowerUp();​
 +    gType = SensorType::​LTR308;​
 +    return true;
 +  }
 +  gType = SensorType::​NONE;​
 +  return false;
 +}
 +
 +// Read brightness from whichever sensor is present
 +// - LTR303: returns CH0 counts (Visible+IR). Stable and monotonic; great for clustering.
 +//           (If you want slight IR reduction, see the commented Option B below.)
 +// - LTR308: returns a lux estimate using the datasheet scaling based on our config.
 +float readLux() {
 +  if (gType == SensorType::​LTR303) {
 +    uint16_t ch0 = 0, ch1 = 0;
 +
 +    // Short wait to reduce stale reads
 +    uint32_t t0 = millis();
 +    while (!g303.newDataAvailable() && (millis() - t0) < 20) { delay(1); }
 +
 +    if (!g303.readBothChannels(ch0,​ ch1)) return 0.0f;
 +
 +    // Option A (recommended):​ use CH0 counts directly
 +    float brightness = (float)ch0;
 +
 +    // Option B (IR-reduced proxy): uncomment if desired
 +    // float brightness = (float)ch0 - 0.6f * (float)ch1;
 +    // if (brightness < 0) brightness = 0;
 +
 +    return brightness; // counts (not true lux) — we normalize later
 +  }
 +
 +  if (gType == SensorType::​LTR308) {
 +    // Datasheet approximation:​ Lux ≈ 0.6 * counts / (gain * int_factor)
 +    // int_factor depends on resolution (integration time)
 +    uint32_t counts = g308.getData();​
 +
 +    float intF = 1.0f;
 +    switch (g308_res) {
 +      case DFRobot_LTR308::​eConversion_25ms_16b: ​ intF = 0.25f; break;
 +      case DFRobot_LTR308::​eConversion_50ms_17b: ​ intF = 0.50f; break;
 +      case DFRobot_LTR308::​eConversion_100ms_18b:​ intF = 1.00f; break;
 +      case DFRobot_LTR308::​eConversion_200ms_19b:​ intF = 2.00f; break;
 +      case DFRobot_LTR308::​eConversion_400ms_20b:​ intF = 4.00f; break;
 +      default: break;
 +    }
 +
 +    float gain = 3.0f; // numeric gain we set
 +    switch (g308_gain) {
 +      case 1:  gain = 1.0f; break;
 +      case 3:  gain = 3.0f; break;
 +      case 6:  gain = 6.0f; break;
 +      case 9:  gain = 9.0f; break;
 +      case 18: gain = 18.0f; break;
 +      default: break;
 +    }
 +
 +    return (gain > 0 && intF > 0) ? (0.6f * (float)counts / (gain * intF)) : 0.0f;
 +  }
 +
 +  return 0.0f;
 +}
 +
 +// ---------------- Unsupervised clustering + labeling ----------------
 +
 +// Sampling / smoothing
 +constexpr uint32_t SAMPLE_MS ​    = 100;   // 10 Hz
 +constexpr float    EMA_ALPHA ​    = 0.3f;  // EMA smoothing
 +
 +// Online K-Means config
 +constexpr int   ​K ​               = 5;     // clusters for: night / shade / lights / sun / transition
 +constexpr int   ​TRAIN_SAMPLES ​   = 400;   // ~40 s training at 10 Hz
 +
 +// Short window variability (detect movement/​changes)
 +constexpr int   ​WIN ​             = 30;    // 3 s at 10 Hz
 +
 +struct ClusterStats {
 +  unsigned long n = 0;
 +  float mean = 0.0f;   // on normalized scale
 +  float M2 = 0.0f;
 +};
 +ClusterStats clusters[K];​
 +
 +float ring[WIN]; int rpos=0; int rcount=0;
 +
 +Preferences prefs;
 +String clusterLabel[K]; ​   // manual overrides via Serial REPL
 +
 +// Utils
 +static inline float clamp01(float x){ return x < 0 ? 0 : (x > 1 ? 1 : x); }
 +static inline float fastAbs(float x){ return x < 0 ? -x : x; }
 +
 +void onlineUpdate(ClusterStats&​ c, float x) {
 +  c.n++;
 +  float delta = x - c.mean;
 +  c.mean += delta / c.n;
 +  c.M2   += delta * (x - c.mean);
 +}
 +float stddev(const ClusterStats&​ c) {
 +  if (c.n < 2) return 0.0f;
 +  return sqrtf(c.M2 / (c.n - 1));
 +}
 +int nearestCluster(float x, float *outDist=nullptr) {
 +  int argmin = 0; float dmin = 1e9;
 +  for (int k=0;​k<​K;​k++){
 +    float d = (clusters[k].n ? fastAbs(x - clusters[k].mean) : 0);
 +    if (d < dmin){ dmin = d; argmin = k; }
 +  }
 +  if (outDist) *outDist = dmin;
 +  return argmin;
 +}
 +void seedClusters(){
 +  for(int k=0;​k<​K;​k++){
 +    clusters[k].n = 1;
 +    clusters[k].mean = (k + 1) / float(K + 1); // 0.17..0.83 for K=5
 +    clusters[k].M2 = 0;
 +  }
 +}
 +float stdev_window(){
 +  if (rcount < 5) return 0;
 +  float mean=0; for(int i=0;​i<​rcount;​i++) mean += ring[i]; mean /= rcount;
 +  float m2=0; for(int i=0;​i<​rcount;​i++){ float d=ring[i]-mean;​ m2+=d*d; }
 +  return sqrtf(m2/​rcount);​
 +}
 +
 +// Persist manual labels
 +void loadLabels(){
 +  prefs.begin("​labels",​ true);
 +  for(int i=0;​i<​K;​i++){ clusterLabel[i] = prefs.getString(String(i).c_str(),​ ""​);​ }
 +  prefs.end();​
 +}
 +void saveLabels(){
 +  prefs.begin("​labels",​ false);
 +  for(int i=0;​i<​K;​i++){ prefs.putString(String(i).c_str(),​ clusterLabel[i]);​ }
 +  prefs.end();​
 +}
 +
 +// Heuristic label if no manual override present
 +String inferLabel(int /*k*/, float normLux, float sdw){
 +  // Tune thresholds to your environment if needed
 +  if (normLux < 0.20f) ​                        ​return "​night";​
 +  if (normLux > 0.85f && sdw < 0.02f) ​         return "​full_sun";​
 +  if (normLux > 0.25f && sdw > 0.06f) ​         return "​lights_on";​
 +  if (normLux > 0.45f && normLux < 0.85f &&
 +      sdw < 0.03f) ​                            ​return "​shade/​day_indirect";​
 +  return "​transition";​
 +}
 +
 +// Log normalization to squeeze wide dynamic range into [0,1]
 +float normalize_lux(float v){
 +  // For LTR303: "​counts"​ scale; for LTR308: lux. Both get log-compressed.
 +  // Using 64k as a generous upper bound reference.
 +  const float denom = log10f(1.0f + 64000.0f);
 +  return clamp01( log10f(1.0f + v) / denom );
 +}
 +
 +// Runtime state
 +uint32_t lastSample=0;​
 +int trainCount = 0;
 +float ema = -1.0f;
 +
 +void setup(){
 +  Serial.begin(115200);​
 +  delay(300);
 +
 +  pinMode(LTR_INT_PIN,​ INPUT_PULLUP);​ // optional
 +  Wire.begin(SDA_PIN,​ SCL_PIN); ​      // Sparrow I2C pins
 +
 +  bool ok = sensorBegin();​
 +  if (!ok){
 +    Serial.println("​No LTR303 (0x29) or LTR308 (0x53) detected on I2C."​);​
 +  } else {
 +    Serial.print("​Sensor:​ "); Serial.println(gType==SensorType::​LTR303 ? "​LTR303"​ : "​LTR308"​);​
 +  }
 +
 +  seedClusters();​
 +  loadLabels();​
 +
 +  Serial.println("​\nSparrow light-state (unsupervised k-means)"​);​
 +  Serial.println("​Commands:​ setlabel <k> <​name>​ | savelabels | labels"​);​
 +}
 +
 +void loop(){
 +  uint32_t now = millis();
 +  if (now - lastSample < SAMPLE_MS) return;
 +  lastSample = now;
 +
 +  // 1) Read raw brightness
 +  float raw = readLux();
 +
 +  // 2) Normalize (log scale), then EMA smooth
 +  float x = normalize_lux(raw);​
 +  if (ema < 0) ema = x;
 +  ema = EMA_ALPHA * x + (1 - EMA_ALPHA) * ema;
 +
 +  // 3) Update short window buffer
 +  ring[rpos] = ema; rpos = (rpos+1) % WIN; if (rcount < WIN) rcount++;
 +
 +  // 4) Training phase
 +  if (trainCount < TRAIN_SAMPLES){
 +    int k = nearestCluster(ema);​
 +    onlineUpdate(clusters[k],​ ema);
 +    trainCount++;​
 +    if (trainCount % 50 == 0){
 +      Serial.printf("​Training %d/%d  means:",​ trainCount, TRAIN_SAMPLES);​
 +      for (int i=0;​i<​K;​i++) Serial.printf("​ %.3f", clusters[i].mean);​
 +      Serial.println();​
 +    }
 +    return;
 +  }
 +
 +  // 5) Inference
 +  float dist; int k = nearestCluster(ema,​ &dist);
 +  float sd  = stddev(clusters[k]);​
 +  float sdw = stdev_window();​
 +
 +  // Choose label (manual override if set, else heuristic)
 +  String label = clusterLabel[k].length() ? clusterLabel[k]
 +                                          : inferLabel(k,​ ema, sdw);
 +
 +  // Hysteresis so labels don't flicker
 +  static String lastLabel="";​ static String pending="";​ static int pendCount=0;​
 +  if (label != lastLabel){
 +    if (label == pending) { pendCount++;​ } else { pending = label; pendCount = 1; }
 +    if (pendCount >= 2){ lastLabel = label; pendCount = 0; }
 +  }
 +  String stableLabel = lastLabel;
 +
 +  // Slow adaptation so clusters follow daylight drift
 +  onlineUpdate(clusters[k],​ ema);
 +
 +  // 6) Log
 +  Serial.printf("​[%s] raw=%.1f norm=%.3f ema=%.3f k=%d mean=%.3f sd=%.3f sdWin=%.3f label=%s\n",​
 +                (gType==SensorType::​LTR303 ? "​LTR303"​ :
 +                 ​gType==SensorType::​LTR308 ? "​LTR308"​ : "​NONE"​),​
 +                raw, x, ema, k, clusters[k].mean,​ sd, sdw, stableLabel.c_str());​
 +
 +  // 7) Tiny serial REPL
 +  if (Serial.available()){
 +    String cmd = Serial.readStringUntil('​\n'​);​ cmd.trim();
 +    if (cmd.startsWith("​setlabel"​)){
 +      int sp1 = cmd.indexOf('​ '), sp2 = cmd.indexOf('​ ', sp1+1);
 +      int idx = cmd.substring(sp1+1,​ sp2).toInt();​
 +      String name = cmd.substring(sp2+1);​
 +      if (idx>=0 && idx<K){ clusterLabel[idx]=name;​ Serial.println("​OK"​);​ }
 +      else { Serial.println("​ERR"​);​ }
 +    } else if (cmd=="​savelabels"​){ saveLabels();​ Serial.println("​saved"​);​ }
 +    else if (cmd=="​labels"​){
 +      for(int i=0;​i<​K;​i++){
 +        Serial.printf("​k=%d mean=%.3f n=%lu label=\"​%s\"​\n",​
 +          i, clusters[i].mean,​ clusters[i].n,​ clusterLabel[i].c_str());​
 +      }
 +    }
 +  }
 +}
 +
 +</​code>​
iothings/laboratoare/2025_code/lab7_3.txt · Last modified: 2025/11/06 18:26 by dan.tudose
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0