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());
      }
    }
  }
}
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