#include <Arduino.h> #include <Wire.h> #include <LSM6DSL.h> // dycodex/LSM6DSL-Arduino #include <Adafruit_NeoPixel.h> // for the on-board WS2812 // ===== Sparrow pinout (from board README) ===== // I2C: SDA=GPIO21, SCL=GPIO22 // LSM6DSL I2C address = 0x6A // NeoPixel on GPIO3 // (INT pin for LSM6DSL is GPIO11 if you later want interrupts) constexpr int PIN_SDA = 21; constexpr int PIN_SCL = 22; constexpr uint8_t LSM6DSL_ADDR = 0x6A; constexpr int NEOPIXEL_PIN = 3; constexpr int NEOPIXEL_COUNT = 1; Adafruit_NeoPixel pixel(NEOPIXEL_COUNT, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800); LSM6DSL imu(LSM6DSL_ADDR); // ===== Sampling & features ===== static const float G_SI = 9.80665f; static const int SAMPLE_RATE_HZ = 104; // ODR we'll ask the IMU to use static const int WINDOW_MS = 2000; // 2 s windows static const int WINDOW_SAMPLES = SAMPLE_RATE_HZ * WINDOW_MS / 1000; // We'll compute simple 4-D features per window: // [ mean(|a|), std(|a|), mean(|g|), std(|g|) ] struct Feature { float f[4]; }; // ===== Simple K-means (from scratch) ===== template<int K> struct KMeans { Feature centroids[K]; bool initialized = false; static float dist2(const Feature& a, const Feature& b) { float d=0; for (int i=0;i<4;i++){ float t=a.f[i]-b.f[i]; d+=t*t; } return d; } int assign(const Feature& x) const { int best = 0; float bestd = dist2(x, centroids[0]); for (int k=1;k<K;k++){ float d=dist2(x, centroids[k]); if (d<bestd){best=k; bestd=d;} } return best; } // k-means++ init from a small set of bootstrap features void init_plus_plus(const Feature *bootstrap, int n) { // pick first centroid centroids[0] = bootstrap[random(n)]; // next centroids for (int c=1;c<K;c++){ // compute D(x)^2 float sumD=0; static float D[128]; // bootstrap up to 128 windows int cap = min(n, 128); for (int i=0;i<cap;i++){ float bestd = dist2(bootstrap[i], centroids[0]); for (int j=1;j<c;j++) bestd = min(bestd, dist2(bootstrap[i], centroids[j])); D[i]=bestd; sumD += bestd; } float r = random(0, 1000000) / 1000000.0f * sumD; int pick=0; float acc=0; for (; pick<cap; pick++){ acc += D[pick]; if (acc>=r) break; } centroids[c] = bootstrap[ pick < cap ? pick : cap-1 ]; } initialized = true; } // batch k-means iterations void fit(const Feature *X, int n, int iters=10) { if (!initialized) { // fallback random init for (int k=0;k<K;k++) centroids[k] = X[random(n)]; initialized = true; } int assignBuf[128]; int cap = min(n, 128); for (int it=0; it<iters; it++){ // assign for (int i=0;i<cap;i++) assignBuf[i]=assign(X[i]); // recompute Feature sums[K]; int counts[K]; for (int k=0;k<K;k++){ for(int j=0;j<4;j++) sums[k].f[j]=0; counts[k]=0; } for (int i=0;i<cap;i++){ int k = assignBuf[i]; for (int j=0;j<4;j++) sums[k].f[j]+=X[i].f[j]; counts[k]++; } for (int k=0;k<K;k++){ if (counts[k]>0){ for (int j=0;j<4;j++) centroids[k].f[j]=sums[k].f[j]/counts[k]; } } } } }; KMeans<3> kmeans; // K = 3 clusters static Feature bootstrap[128]; // store first ~128 windows static int bootstrap_count = 0; static bool model_ready = false; static bool stage_prompted[3] = {false, false, false}; constexpr int BOOTSTRAP_STAGE_WINDOWS = 10; // ~20 s total per instruction phase // ===== helpers ===== void neopixel_show_cluster(int c){ uint8_t r=0,g=0,b=0; if (c==0){ r=255; } // red else if (c==1){ g=255; } // green else { b=255; } // blue pixel.setPixelColor(0, pixel.Color(r,g,b)); pixel.show(); } void print_feature(const Feature& x){ Serial.printf("feat: aMean=%.3f, aStd=%.3f, gMean=%.3f, gStd=%.3f\n", x.f[0], x.f[1], x.f[2], x.f[3]); } void prompt_bootstrap_stage() { if (model_ready) { return; } if (bootstrap_count < BOOTSTRAP_STAGE_WINDOWS && !stage_prompted[0]) { Serial.println("Stage 1/3: move the board UP and DOWN for the first cluster."); Serial.println("Collecting motion windows... keep moving!"); stage_prompted[0] = true; } else if (bootstrap_count >= BOOTSTRAP_STAGE_WINDOWS && bootstrap_count < 2 * BOOTSTRAP_STAGE_WINDOWS && !stage_prompted[1]) { Serial.println("Stage 2/3: move the board LEFT and RIGHT for the second cluster."); Serial.println("Keep sweeping side-to-side until prompted again."); stage_prompted[1] = true; } else if (bootstrap_count >= 2 * BOOTSTRAP_STAGE_WINDOWS && bootstrap_count < 3 * BOOTSTRAP_STAGE_WINDOWS && !stage_prompted[2]) { Serial.println("Stage 3/3: hold the board STILL to capture the stationary cluster."); Serial.println("Stay steady while we finish collecting samples."); stage_prompted[2] = true; } } // Compute features for one window (blocking read loop) Feature compute_window_features() { // Welford variance on |a| and |g| double meanA=0, M2A=0, meanG=0, M2G=0; for (int i=1;i<=WINDOW_SAMPLES;i++){ // read sensors // Library returns accel in g and gyro in deg/s based on configured ranges float aX = imu.readFloatAccelX() * G_SI; // g -> m/s^2 float aY = imu.readFloatAccelY() * G_SI; float aZ = imu.readFloatAccelZ() * G_SI; float gX = imu.readFloatGyroX() * DEG_TO_RAD; // deg/s -> rad/s float gY = imu.readFloatGyroY() * DEG_TO_RAD; float gZ = imu.readFloatGyroZ() * DEG_TO_RAD; float amag = sqrtf(aX*aX + aY*aY + aZ*aZ); float gmag = sqrtf(gX*gX + gY*gY + gZ*gZ); // Welford updates double deltaA = amag - meanA; meanA += deltaA / i; M2A += deltaA*(amag - meanA); double deltaG = gmag - meanG; meanG += deltaG / i; M2G += deltaG*(gmag - meanG); delayMicroseconds(1000000 / SAMPLE_RATE_HZ); // crude pacing ~1/ODR } Feature x; x.f[0] = (float)meanA; x.f[1] = (float)sqrt(M2A / (WINDOW_SAMPLES-1)); x.f[2] = (float)meanG; x.f[3] = (float)sqrt(M2G / (WINDOW_SAMPLES-1)); return x; } void setupIMU() { // Configure desired ranges and ODR before starting the sensor imu.settings.accelRange = 2; // ±2g imu.settings.accelSampleRate = 104; imu.settings.gyroRange = 245; // ±245 dps imu.settings.gyroSampleRate = 104; if (imu.begin() != IMU_SUCCESS) { Serial.println("LSM6DSL not found. Check wiring/address."); while (1) { delay(1000); } } } void setup() { Serial.begin(115200); delay(200); // I2C Wire.begin(PIN_SDA, PIN_SCL, 400000); // fast-mode for quicker reads // NeoPixel pixel.begin(); pixel.clear(); pixel.show(); // IMU setupIMU(); Serial.println("IMU Activity Clustering (K-means, K=3)."); Serial.println("Bootstrapping ~80 s of guided motion for unsupervised init..."); prompt_bootstrap_stage(); } void loop() { prompt_bootstrap_stage(); Feature feat = compute_window_features(); print_feature(feat); if (!model_ready) { // collect bootstrap windows (cap 128) if (bootstrap_count < 128) { bootstrap[bootstrap_count++] = feat; } // After ~60 seconds (30 windows @ 2s) kick off training if (bootstrap_count >= 30 && !kmeans.initialized) { kmeans.init_plus_plus(bootstrap, bootstrap_count); kmeans.fit(bootstrap, bootstrap_count, /*iters=*/12); Serial.println("K-means initialized."); } // Consider model ready once we have a decent bootstrap if (bootstrap_count >= 40) { model_ready = true; Serial.println("Model ready. Streaming cluster assignments..."); } } else { int c = kmeans.assign(feat); neopixel_show_cluster(c); Serial.printf("cluster: %d\n", c); // Optional: small online centroid update (streaming k-means) // move centroid slightly toward new sample const float alpha = 0.05f; for (int j=0;j<4;j++) { kmeans.centroids[c].f[j] = (1.0f - alpha) * kmeans.centroids[c].f[j] + alpha * feat.f[j]; } } }