// -----------------------------------------------------------------------------
// M5Atom S3 + WS2812 1616 v12.8-hourglass90 + shake-gate
// Modes: CLOCK / FLOW / HOURGLASS(90s, fully packed & symmetric) / MAZE
// Btn: Single=next / Double=IMU invert save / Triple=Clock mirror save / Long(2s)=OFF
// Wi-Fi + NTP(JST) SSID: khome / PASS: Kiwamot0
// -----------------------------------------------------------------------------
#include <M5Unified.h>
#include <Adafruit_NeoPixel.h>
#include <Preferences.h>
#include <WiFi.h>
#include <esp_random.h>
#include <time.h>
#include <cmath>
#include <cstring>
// ===== Board / LED ===========================================================
constexpr uint8_t DIN_PIN = 2; // AtomS3: G2 WS2812 DIN
constexpr uint8_t PWR_PIN = 16; // FET
constexpr int W = 16, H = 16;
constexpr int NLED = W * H;
Adafruit_NeoPixel strip(NLED, DIN_PIN, NEO_GRB + NEO_KHZ800);
//
inline uint16_t idx(int x,int y){ return (y & 1) ? (y*W + (W-1-x)) : (y*W + x); }
// ===== Wi-Fi / NTP ===========================================================
const char* WIFI_SSID = "khome";
const char* WIFI_PASS = "Kiwamot0";
constexpr uint32_t WIFI_TIMEOUT_MS = 10000;
// ===== Modes / Buttons =======================================================
enum Mode : uint8_t { MODE_CLOCK, MODE_FLOW, MODE_HOURGLASS, MODE_MAZE };
Mode mode = MODE_CLOCK;
bool powerOn = true;
constexpr uint16_t LONG_PRESS_MS = 2000;
uint32_t btnHoldStart = 0;
uint32_t lastActive_ms = 0; // FLOW
constexpr uint32_t FLOW_IDLE_TO_CLOCK_MS = 30000; //30
static uint32_t lastClick=0;
static uint8_t clickCnt=0;
constexpr uint16_t MULTICLICK_WINDOW_MS = 350;
// ===== Preferences ===========================================================
Preferences prefs;
bool invertX=false, invertY=false; // IMU
bool clockMirrorX = true; //
constexpr const char* PREF_NS = "lab_v128";
constexpr const char* KEY_INV = "invMask";
constexpr const char* KEY_CMIR = "clockMX";
void saveIMU(){ prefs.putUChar(KEY_INV, (invertX?1:0)|(invertY?2:0)); }
void saveClockMirror(){ prefs.putBool(KEY_CMIR, clockMirrorX); }
void loadPrefs(){
prefs.begin(PREF_NS, false);
uint8_t m = prefs.getUChar(KEY_INV, 0);
invertX = (m & 1);
invertY = (m & 2);
clockMirrorX = prefs.getBool(KEY_CMIR, true);
}
// ===== Common Tuning =========================================================
constexpr uint8_t MAIN_BRIGHT = 48;
// ===== FLOWCLOCK ========================
//
constexpr float LP_ALPHA = 0.12f; // 0..1
constexpr float SHAKE_JERK_THR = 0.35f; //
constexpr uint16_t SHAKE_DEBOUNCE_MS= 250; //
constexpr float STILL_JERK_THR = 0.05f; //
constexpr uint32_t STILL_HOLD_MS = 10000; // CLOCK10s
static float axLP=0, ayLP=0, azLP=0; // LP
static uint32_t lastShakeMs = 0;
static uint32_t stillSinceMs = 0;
// ===== FLOW ====================================================
constexpr float G_GAIN = 36000.0f;
constexpr float DEADZONE_G = 0.03f;
constexpr float CURVE_GAMMA = 1.4f;
constexpr float FRICTION = 0.995f;
constexpr float BOUNCE = 0.75f;
constexpr int32_t VEL_CLAMP = 16*256*6;
constexpr bool GRAVITY_INVERT_Y = true;
constexpr uint8_t TAIL_KEEP = 216;
constexpr float KERNEL_SHARP= 1.6f;
constexpr uint8_t SPARK_GAIN = 60;
constexpr int SEP_DIST = 180;
constexpr float SEP_PUSH = 0.5f;
constexpr int BALLS = 35;
struct Ball{ int32_t px,py,vx,vy; uint32_t col; };
Ball balls[BALLS];
static uint32_t accR[NLED], accG[NLED], accB[NLED];
inline uint8_t fade8(uint8_t v, uint8_t keep){ return (uint16_t(v)*keep)>>8; }
uint32_t hsv(uint8_t h, uint8_t s, uint8_t v){
uint8_t r,g,b; uint8_t region=h/43, rem=(h-region*43)*6;
uint8_t p=(v*(255-s))>>8, q=(v*(255-((s*rem)>>8)))>>8, t=(v*(255-((s*(255-rem))>>8)))>>8;
switch(region){
case 0: r=v; g=t; b=p; break; case 1: r=q; g=v; b=p; break;
case 2: r=p; g=v; b=t; break; case 3: r=p; g=q; b=v; break;
case 4: r=t; g=p; b=v; break; default:r=v; g=p; b=q; break;
}
return (uint32_t)r<<16 | (uint32_t)g<<8 | b;
}
void initBalls(){
for(int i=0;i<BALLS;++i){
balls[i].px = (W/2)*256; balls[i].py = (H/2)*256;
balls[i].vx = (int32_t)((int32_t)(esp_random()%401) - 200);
balls[i].vy = (int32_t)((int32_t)(esp_random()%401) - 200);
uint8_t hue = (uint8_t)((i * 255) / BALLS);
balls[i].col = hsv(hue, 255, 255);
}
}
static uint8_t lutPow[256], lutPowInv[256];
void buildLUT(){
for(int i=0;i<256;++i){
float x = i/255.0f;
float a = powf(x, KERNEL_SHARP);
float b = powf(1.0f - x, KERNEL_SHARP);
lutPow[i] = (uint8_t)(a*255.0f + 0.5f);
lutPowInv[i] = (uint8_t)(b*255.0f + 0.5f);
}
}
inline void accAdd(uint16_t p, uint32_t col, uint16_t w){
uint8_t cr = col >> 16, cg = (col >> 8) & 0xFF, cb = col & 0xFF;
accR[p] += (uint32_t)cr * w;
accG[p] += (uint32_t)cg * w;
accB[p] += (uint32_t)cb * w;
}
inline void drawSubpixelSharpLUT(int32_t px, int32_t py, uint32_t col){
int x0 = px >> 8, y0 = py >> 8;
uint8_t fx = px & 0xFF, fy = py & 0xFF;
uint16_t u0 = lutPowInv[fx], u1 = lutPow[fx];
uint16_t v0 = lutPowInv[fy], v1 = lutPow[fy];
uint16_t w00 = (u0*v0)>>8, w10 = (u1*v0)>>8, w01 = (u0*v1)>>8, w11 = (u1*v1)>>8;
if((unsigned)x0 < W && (unsigned)y0 < H) accAdd(idx(x0, y0 ), col, w00);
if((unsigned)(x0+1) < W && (unsigned)y0 < H) accAdd(idx(x0+1, y0 ), col, w10);
if((unsigned)x0 < W && (unsigned)(y0+1) < H) accAdd(idx(x0, y0+1), col, w01);
if((unsigned)(x0+1) < W && (unsigned)(y0+1) < H) accAdd(idx(x0+1, y0+1), col, w11);
if((unsigned)x0 < W && (unsigned)y0 < H) accAdd(idx(x0, y0 ), col, SPARK_GAIN);
}
uint32_t tPrev_us = 0;
// ===== Clock (3x5) ===========================================================
const uint8_t FONT[10][5] = {
{0b111,0b101,0b101,0b101,0b111}, {0b010,0b110,0b010,0b010,0b111},
{0b111,0b001,0b111,0b100,0b111}, {0b111,0b001,0b111,0b001,0b111},
{0b101,0b101,0b111,0b001,0b001}, {0b111,0b100,0b111,0b001,0b111},
{0b111,0b100,0b111,0b101,0b111}, {0b111,0b001,0b001,0b001,0b001},
{0b111,0b101,0b111,0b101,0b111}, {0b111,0b101,0b111,0b001,0b111}
};
inline uint16_t clockIndex(int x, int y){
int xx = clockMirrorX ? (W-1-x) : x; //
return idx(xx, y);
}
inline void clockPix(int x, int y, uint32_t c){
if ((unsigned)x < W && (unsigned)y < H) strip.setPixelColor(clockIndex(x,y), c);
}
void drawDigit3x5_clock(int d,int ox,int oy,uint32_t col){
for(int y=0;y<5;++y) for(int x=0;x<3;++x)
if(FONT[d][y] & (1<<(2-x))) clockPix(ox+x, oy+y, col);
}
void renderClock(bool colonOn){
strip.clear();
strip.setBrightness(MAIN_BRIGHT);
time_t now=time(nullptr); struct tm lt; localtime_r(&now,<);
int hh=lt.tm_hour, mm=lt.tm_min;
int x=0, y=5; // 354 + 1 = 15
uint32_t cH = strip.Color(0,128,255);
uint32_t cM = strip.Color(0,255,96);
drawDigit3x5_clock(hh/10, x, y, cH); x+=4;
drawDigit3x5_clock(hh%10, x, y, cH); x+=4;
if(colonOn){ clockPix(x, y+1, cH); clockPix(x, y+3, cH); }
x+=1;
drawDigit3x5_clock(mm/10, x, y, cM); x+=4;
drawDigit3x5_clock(mm%10, x, y, cM);
strip.show();
}
// ===== Gravity helper ========================================================
void xyGravityCurve(float ax, float ay, float &gx, float &gy){
if(invertX) ax = -ax;
if(invertY) ay = -ay;
float mag = sqrtf(ax*ax + ay*ay);
if(mag < 1e-6f){ gx=gy=0; return; }
float norm = (mag - DEADZONE_G)/(1.0f - DEADZONE_G);
if(norm < 0){ gx=gy=0; return; }
norm = powf(norm, CURVE_GAMMA);
gx = (ax/mag) * norm;
gy = (ay/mag) * norm;
if(GRAVITY_INVERT_Y) gy = -gy; //
}
// ===== FLOW step =============================================================
void physicsStepFLOW(float gx, float gy, float dt){
memset(accR, 0, sizeof(accR));
memset(accG, 0, sizeof(accG));
memset(accB, 0, sizeof(accB));
int32_t minU=0, maxU=(W-1)*256;
for(int i=0;i<BALLS;++i){
balls[i].vx += (int32_t)(gx*G_GAIN*dt);
balls[i].vy += (int32_t)(gy*G_GAIN*dt);
balls[i].vx = (int32_t)(balls[i].vx * FRICTION);
balls[i].vy = (int32_t)(balls[i].vy * FRICTION);
if(balls[i].vx > VEL_CLAMP) balls[i].vx = VEL_CLAMP;
if(balls[i].vx < -VEL_CLAMP) balls[i].vx = -VEL_CLAMP;
if(balls[i].vy > VEL_CLAMP) balls[i].vy = VEL_CLAMP;
if(balls[i].vy < -VEL_CLAMP) balls[i].vy = -VEL_CLAMP;
balls[i].px += (int32_t)(balls[i].vx * dt);
balls[i].py += (int32_t)(balls[i].vy * dt);
bool hit=false;
if(balls[i].px < minU){ balls[i].px=minU; balls[i].vx = -(int32_t)(balls[i].vx*BOUNCE); hit=true; }
if(balls[i].px > maxU){ balls[i].px=maxU; balls[i].vx = -(int32_t)(balls[i].vx*BOUNCE); hit=true; }
if(balls[i].py < minU){ balls[i].py=minU; balls[i].vy = -(int32_t)(balls[i].vy*BOUNCE); hit=true; }
if(balls[i].py > maxU){ balls[i].py=maxU; balls[i].vy = -(int32_t)(balls[i].vy*BOUNCE); hit=true; }
if(hit){ balls[i].vx = (int32_t)(balls[i].vx * 0.90f); balls[i].vy = (int32_t)(balls[i].vy * 0.90f); }
drawSubpixelSharpLUT(balls[i].px, balls[i].py, balls[i].col);
}
//
for(int i=0;i<BALLS;++i){
for(int j=i+1;j<BALLS;++j){
int32_t dx = balls[j].px - balls[i].px;
int32_t dy = balls[j].py - balls[i].py;
if(abs(dx) < SEP_DIST && abs(dy) < SEP_DIST){
int64_t d2 = (int64_t)dx*dx + (int64_t)dy*dy;
int32_t thresh2 = (int32_t)SEP_DIST * (int32_t)SEP_DIST;
if(d2 == 0){
int32_t jx = (int32_t)((int32_t)(esp_random()%101) - 50);
int32_t jy = (int32_t)((int32_t)(esp_random()%101) - 50);
balls[i].px -= jx; balls[i].py -= jy;
balls[j].px += jx; balls[j].py += jy;
} else if(d2 < thresh2){
float d = sqrtf((float)d2);
float nx = dx / d, ny = dy / d;
float corr = (SEP_DIST - d) * SEP_PUSH;
int32_t cx = (int32_t)(nx * corr);
int32_t cy = (int32_t)(ny * corr);
balls[i].px -= cx; balls[i].py -= cy;
balls[j].px += cx; balls[j].py += cy;
}
}
}
}
// HDR max
for(int p=0; p<NLED; ++p){
uint32_t prev = strip.getPixelColor(p);
uint8_t rTail = fade8((prev>>16)&0xFF, TAIL_KEEP);
uint8_t gTail = fade8((prev>> 8)&0xFF, TAIL_KEEP);
uint8_t bTail = fade8((prev )&0xFF, TAIL_KEEP);
uint16_t rAcc = accR[p] >> 8; if(rAcc>255) rAcc=255;
uint16_t gAcc = accG[p] >> 8; if(gAcc>255) gAcc=255;
uint16_t bAcc = accB[p] >> 8; if(bAcc>255) bAcc=255;
uint8_t r = (rTail > (uint8_t)rAcc) ? rTail : (uint8_t)rAcc;
uint8_t g = (gTail > (uint8_t)gAcc) ? gTail : (uint8_t)gAcc;
uint8_t b = (bTail > (uint8_t)bAcc) ? bTail : (uint8_t)bAcc;
strip.setPixelColor(p, ((uint32_t)r<<16)|((uint32_t)g<<8)|b);
}
strip.show();
}
// ===== HOURGLASS (90s, fully packed & symmetric) ============================
static bool hgInside[H][W]; //
static bool hgSand[H][W]; //
constexpr float HG_DURATION_SEC = 90.0f; // 90
constexpr float HG_MIN_TILT = 0.30f; // |gy|
static float hgRateTPS = 0.0f; // /
static float hgTokens = 0.0f; //
static uint32_t hgLastMs = 0;
uint32_t hgSandColor = 0xFFD070;
uint32_t hgWallColor = 0x203050;
uint8_t hgBGFade = 190;
// (0..7)(15..8)
void buildHourglassMask(){
memset(hgInside, 0, sizeof(hgInside));
const float midX = (W-1)*0.5f; // 7.5
const float neckHalf = 1.0f; // =1 x=7,8
const float maxHalf = 7.0f; //
const float curve = 1.35f; //
auto buildRow=[&](int y)->void{
float t = (7 - y) / 7.0f; // y=0t=1, y=7t=0
float half = neckHalf + (maxHalf - neckHalf) * powf(t, curve);
// +0.5
int xL = (int)ceilf (midX - half);
int xR = (int)floorf(midX + half);
if(xL<0) xL=0; if(xR>=W) xR=W-1;
for(int x=xL;x<=xR;++x) hgInside[y][x]=true;
};
for(int y=0;y<=7;++y) buildRow(y);
for(int y=0;y<=7;++y){
int ym = H-1-y; // 15..8
for(int x=0;x<W;++x) hgInside[ym][x] = hgInside[y][x];
}
// 22y=7/8, x=7/8
for(int y=0;y<H;y++){
if(y==7 || y==8){
for(int x=0;x<W;x++) hgInside[y][x]=false;
hgInside[y][7]=hgInside[y][8]=true;
}
}
}
inline bool isUpperRow(int y, int gySign){ return gySign>=0 ? (y<=7) : (y>=8); }
void fillUpperBulb(int gySign){
memset(hgSand, 0, sizeof(hgSand));
for(int y=0;y<H;y++){
if(!isUpperRow(y,gySign)) continue;
for(int x=0;x<W;x++){
if(hgInside[y][x]) hgSand[y][x]=true;
}
}
}
void hgRecalcRate(int gySign){
int upper=0;
for(int y=0;y<H;y++){
if(!isUpperRow(y,gySign)) continue;
for(int x=0;x<W;x++) if(hgInside[y][x] && hgSand[y][x]) upper++;
}
hgRateTPS = (upper>0) ? ((float)upper / HG_DURATION_SEC) : 0.0f;
hgTokens = 0.0f;
hgLastMs = millis();
}
inline bool isThroatDest(int x,int y){ return ( (x==7||x==8) && (y==7||y==8) ); }
inline bool isCrossingThroat(int sy,int sx,int ny,int nx){
if(!isThroatDest(nx,ny)) return false;
return ( (sy==7 && ny==8) || (sy==8 && ny==7) );
}
inline bool insideHG(int x,int y){ return (unsigned)x<W && (unsigned)y<H && hgInside[y][x]; }
static inline void hgDir(float gx, float gy,
int &dx0,int &dy0,int &dxa,int &dya,int &dxb,int &dyb,
int &sx,int &sy,int &ex,int &ey){
if(fabsf(gy) >= fabsf(gx)){
dy0 = (gy>=0)? 1:-1; dx0 = 0;
dxa = (gx>=0)? +1:-1; dya = dy0;
dxb = -dxa; dyb = dy0;
sy = (dy0>0)? H-1:0; ey = (dy0>0)? -1:H; sx=0; ex=W;
}else{
dx0 = (gx>=0)? 1:-1; dy0 = 0;
dya = (gy>=0)? +1:-1; dxa = dx0;
dyb = -dya; dxb = dx0;
sx = (dx0>0)? W-1:0; ex = (dx0>0)? -1:W; sy=0; ey=H;
}
}
void stepHourglass(float gx, float gy){
//
uint32_t now = millis();
if(hgLastMs==0) hgLastMs = now;
float dt = (now - hgLastMs)*0.001f;
hgLastMs = now;
if(fabsf(gy) > HG_MIN_TILT) hgTokens += hgRateTPS * dt;
//
static int lastSign = +1;
int gySign = (gy>=0)? +1 : -1;
if(gySign != lastSign && fabsf(gy) > 0.6f){
fillUpperBulb(gySign);
hgRecalcRate(gySign);
lastSign = gySign;
}
int dx0,dy0,dxa,dya,dxb,dyb,sx,sy,ex,ey;
hgDir(gx,gy,dx0,dy0,dxa,dya,dxb,dyb,sx,sy,ex,ey);
auto tryMove = [&](int y,int x,int ny,int nx)->bool{
if(!insideHG(nx,ny) || hgSand[ny][nx]) return false;
if(isCrossingThroat(y,x,ny,nx)){
if(hgTokens >= 1.0f){
hgSand[y][x]=false; hgSand[ny][nx]=true; hgTokens -= 1.0f;
return true;
} else return false;
}else{
hgSand[y][x]=false; hgSand[ny][nx]=true; return true;
}
};
if(dy0!=0){ //
for(int y=(dy0>0? H-1:0); (dy0>0? y>=0:y<H); y+=(dy0>0? -1:+1)){
for(int x=0;x<W;++x){
if(!hgSand[y][x]) continue;
int ny=y+dy0, nx=x+dx0;
if(tryMove(y,x,ny,nx)) continue;
int ay=y+dya, ax=x+dxa;
int by=y+dyb, bx=x+dxb;
if(tryMove(y,x,ay,ax)) continue;
(void)tryMove(y,x,by,bx);
}
}
}else{ //
for(int x=(dx0>0? W-1:0); (dx0>0? x>=0:x<W); x+=(dx0>0? -1:+1)){
for(int y=0;y<H;++y){
if(!hgSand[y][x]) continue;
int ny=y+dy0, nx=x+dx0;
if(tryMove(y,x,ny,nx)) continue;
int ay=y+dya, ax=x+dxa;
int by=y+dyb, bx=x+dxb;
if(tryMove(y,x,ay,ax)) continue;
(void)tryMove(y,x,by,bx);
}
}
}
}
void renderHourglass(){
//
for(int p=0;p<NLED;++p){
uint32_t c = strip.getPixelColor(p);
uint8_t r = fade8((c>>16)&0xFF, hgBGFade);
uint8_t g = fade8((c>> 8)&0xFF, hgBGFade);
uint8_t b = fade8((c )&0xFF, hgBGFade);
strip.setPixelColor(p, ((uint32_t)r<<16)|((uint32_t)g<<8)|b);
}
//
for(int y=0;y<H;++y){
for(int x=0;x<W;++x){
if(!hgInside[y][x]){
strip.setPixelColor(idx(x,y), hgWallColor);
}
}
}
//
for(int y=0;y<H;++y){
for(int x=0;x<W;++x){
if(hgSand[y][x]){
strip.setPixelColor(idx(x,y), hgSandColor);
}
}
}
strip.show();
}
// /90s
bool hgFirstEnter = true;
void enterHourglass(float gy){
int gySign = (gy>=0)? +1:-1;
fillUpperBulb(gySign);
hgRecalcRate(gySign);
hgFirstEnter = false;
}
// ===== MAZE =========================================
constexpr int MZ_CW=7, MZ_CH=7;
static bool mzVisited[MZ_CH][MZ_CW];
static uint8_t mzHWall[MZ_CH+1][MZ_CW];
static uint8_t mzVWall[MZ_CH][MZ_CW+1];
int mzPx=1, mzPy=1;
int mzGx=13, mzGy=13;
uint32_t mzWallColor = 0x203030;
uint32_t mzPathColor = 0x001010;
uint32_t mzBallColor = 0x80FF40;
uint32_t mzGoalBase = 0xFF20FF; //
void mzClear(){
memset(mzVisited,0,sizeof(mzVisited));
for(int y=0;y<=MZ_CH;y++) for(int x=0;x<MZ_CW;x++) mzHWall[y][x]=1;
for(int y=0;y<MZ_CH;y++) for(int x=0;x<=MZ_CW;x++) mzVWall[y][x]=1;
}
void mzDFS(int cx,int cy){
mzVisited[cy][cx]=true;
int dirs[4]={0,1,2,3};
for(int i=0;i<4;i++){ int j=esp_random()%4; int t=dirs[i]; dirs[i]=dirs[j]; dirs[j]=t; }
for(int k=0;k<4;k++){
int d=dirs[k]; int nx=cx, ny=cy;
if(d==0) ny=cy-1; else if(d==1) nx=cx+1; else if(d==2) ny=cy+1; else nx=cx-1;
if(nx<0||ny<0||nx>=MZ_CW||ny>=MZ_CH) continue;
if(!mzVisited[ny][nx]){
if(d==0) mzHWall[cy][cx]=0;
if(d==2) mzHWall[cy+1][cx]=0;
if(d==1) mzVWall[cy][cx+1]=0;
if(d==3) mzVWall[cy][cx]=0;
mzDFS(nx,ny);
}
}
}
inline bool mzIsWallPixel(int X,int Y){
if(X==0||Y==0||X==14||Y==14) return true;
if((X%2==1)&&(Y%2==1)) return false;
if(Y%2==0 && X%2==1){ int cx=(X-1)/2, ry=Y/2; return mzHWall[ry][cx]; }
if(X%2==0 && Y%2==1){ int cy=(Y-1)/2, cx=X/2; return mzVWall[cy][cx]; }
return true;
}
void initMaze(){
mzClear(); mzDFS(0,0);
mzPx=1; mzPy=1; mzGx=13; mzGy=13;
}
static inline uint32_t scaleColor(uint32_t c, float s){
uint8_t r=(c>>16)&0xFF, g=(c>>8)&0xFF, b=c&0xFF;
int R=(int)(r*s); if(R>255)R=255;
int G=(int)(g*s); if(G>255)G=255;
int B=(int)(b*s); if(B>255)B=255;
return ((uint32_t)R<<16)|((uint32_t)G<<8)|B;
}
void stepMaze(float gx, float gy){
static uint32_t lastStep = 0;
if (millis() - lastStep < 60) return;
lastStep = millis();
int dx = (fabsf(gx) >= fabsf(gy)) ? ((gx > 0) ? +2 : (gx < 0 ? -2 : 0)) : 0;
int dy = (fabsf(gy) > fabsf(gx)) ? ((gy > 0) ? +2 : (gy < 0 ? -2 : 0)) : 0;
auto canMove = [&](int x,int y,int dx,int dy)->bool{
int mx = x + dx/2, my = y + dy/2;
int tx = x + dx, ty = y + dy;
if (mx<0||my<0||mx>14||my>14||tx<0||ty<0||tx>14||ty>14) return false;
if (mzIsWallPixel(mx,my)) return false;
if (mzIsWallPixel(tx,ty)) return false;
return true;
};
int nx=mzPx, ny=mzPy;
if (dx!=0 && canMove(nx,ny,dx,0)) nx += dx;
if (dy!=0 && canMove(nx,ny,0,dy)) ny += dy;
mzPx = nx; mzPy = ny;
if (mzPx==mzGx && mzPy==mzGy){
for (int r=0; r<7; r++){
for (int y=0; y<15; y++) for (int x=0; x<15; x++){
uint32_t c = 0;
if (!mzIsWallPixel(x,y))
c = ((abs(x-mzGx)+abs(y-mzGy))<=r) ? 0xFFFFFF : 0x000000;
strip.setPixelColor(idx(x,y), c);
}
strip.show(); delay(25);
}
initMaze();
}
}
void renderMaze(){
for(int y=0;y<15;y++){
for(int x=0;x<15;x++){
uint32_t c = mzIsWallPixel(x,y) ? mzWallColor : mzPathColor;
strip.setPixelColor(idx(x,y), c);
}
}
//
float pulse = 0.5f + 0.5f * sinf(millis()*0.010f);
strip.setPixelColor(idx(mzGx,mzGy), scaleColor(mzGoalBase, 0.4f + 0.6f*pulse));
//
strip.setPixelColor(idx(mzPx,mzPy), 0xFFFFFF);
strip.show();
}
// ===== Power / Wi-Fi / Setup / Loop =========================================
void enterOff(){ strip.clear(); strip.show(); digitalWrite(PWR_PIN, LOW); powerOn=false; }
void leaveOff(){
digitalWrite(PWR_PIN, HIGH);
strip.setBrightness(MAIN_BRIGHT);
initBalls();
tPrev_us = micros();
lastActive_ms = millis();
mode = MODE_CLOCK;
powerOn = true;
}
void setupWiFiAndTime(){
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
uint32_t start = millis();
while (WiFi.status() != WL_CONNECTED && (millis() - start) < WIFI_TIMEOUT_MS){ delay(200); }
configTime(9*3600, 0, "ntp.jst.mfeed.ad.jp", "ntp.nict.jp", "pool.ntp.org");
struct tm tinfo; getLocalTime(&tinfo, 5000);
}
void setup(){
pinMode(PWR_PIN, OUTPUT); digitalWrite(PWR_PIN, HIGH);
M5.begin();
strip.begin(); strip.setBrightness(MAIN_BRIGHT); strip.show();
loadPrefs();
setupWiFiAndTime();
buildLUT();
initBalls();
buildHourglassMask();
initMaze();
tPrev_us = micros();
lastActive_ms = millis();
renderClock(true); //
}
void loop(){
M5.update();
if(!powerOn){
if(M5.BtnA.wasPressed()){ leaveOff(); }
delay(50);
return;
}
//
if(M5.BtnA.wasPressed()){
uint32_t now = millis();
if(now - lastClick <= MULTICLICK_WINDOW_MS){ clickCnt++; }
else { clickCnt=1; }
lastClick = now;
}
//
if(clickCnt && (millis()-lastClick) > MULTICLICK_WINDOW_MS){
if(clickCnt>=3){ clockMirrorX = !clockMirrorX; saveClockMirror(); if(mode==MODE_CLOCK) renderClock(true); }
else if(clickCnt==2){ invertX=!invertX; invertY=!invertY; saveIMU(); }
else {
mode = (Mode)((mode + 1) % 4);
if(mode==MODE_CLOCK) renderClock(true);
if(mode==MODE_FLOW){ initBalls(); lastActive_ms=millis(); stillSinceMs=0; }
if(mode==MODE_HOURGLASS){ hgFirstEnter=true; } //
}
clickCnt=0;
}
// 2s OFF
if(M5.BtnA.isPressed()){
if(btnHoldStart==0) btnHoldStart = millis();
else if(millis()-btnHoldStart >= LONG_PRESS_MS){ enterOff(); btnHoldStart=0; return; }
} else { btnHoldStart = 0; }
// ==== IMU & ==========================================
float ax,ay,az; M5.Imu.getAccel(&ax,&ay,&az);
// LP
axLP = axLP + LP_ALPHA*(ax - axLP);
ayLP = ayLP + LP_ALPHA*(ay - ayLP);
azLP = azLP + LP_ALPHA*(az - azLP);
// LPXY
float jx = ax - axLP;
float jy = ay - ayLP;
float jerk = sqrtf(jx*jx + jy*jy);
//
uint32_t nowMs = millis();
if(jerk < STILL_JERK_THR){
if(stillSinceMs==0) stillSinceMs = nowMs;
}else{
stillSinceMs = 0;
}
// FLOW
float gx, gy; xyGravityCurve(ax, ay, gx, gy);
// ==== CLOCK ================================================================
if(mode==MODE_CLOCK){
static uint32_t prevHalf = 0xFFFFFFFF;
uint32_t half = millis()/500;
if(half != prevHalf){ renderClock((half % 2)==0); prevHalf = half; }
// FLOW
if(jerk > SHAKE_JERK_THR && (nowMs - lastShakeMs) > SHAKE_DEBOUNCE_MS){
lastShakeMs = nowMs;
mode=MODE_FLOW;
initBalls();
lastActive_ms=nowMs;
stillSinceMs=0;
}
delay(5);
return;
}
// ==== FLOW ================================================================
if(mode==MODE_FLOW){
//
if(jerk > SHAKE_JERK_THR){ lastActive_ms = nowMs; lastShakeMs = nowMs; }
//
if(stillSinceMs && (nowMs - stillSinceMs) > STILL_HOLD_MS){
mode = MODE_CLOCK; renderClock(true); return;
}
//
if(nowMs - lastActive_ms > FLOW_IDLE_TO_CLOCK_MS){
mode = MODE_CLOCK; renderClock(true); return;
}
uint32_t tNow = micros();
float dt = (tNow - tPrev_us) * 1e-6f; tPrev_us = tNow;
physicsStepFLOW(gx, gy, dt);
delayMicroseconds(200);
return;
}
// ==== HOURGLASS ===========================================================
if(mode==MODE_HOURGLASS){
if(hgFirstEnter){ enterHourglass(gy); }
stepHourglass(gx, gy);
renderHourglass();
delay(18);
return;
}
// ==== MAZE ================================================================
if(mode==MODE_MAZE){
stepMaze(gx, gy);
renderMaze();
delay(10);
return;
}
}
Comments