This shows you the differences between two versions of the page.
| — |
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> | ||