diff --git a/include/ad7606.h b/include/ad7606.h new file mode 100644 index 0000000..8be2a19 --- /dev/null +++ b/include/ad7606.h @@ -0,0 +1,47 @@ +#ifndef AD7606_H +#define AD7606_H + +#include +#include + +// 可按需修改为你的实际接线。若 OS 引脚已硬连到 GND/3V3,可将其保持为 -1(不配置)。 +namespace ad7606 { + +// 数据/控制引脚 +static const int PIN_AD_MISO = 8; // DOUTA -> ESP32 MISO +static const int PIN_AD_CS = 12; // CS_N +static const int PIN_AD_CONVST = 13; // CONVST(CO-A/CO-B 共用) +static const int PIN_AD_RESET = 5; // RESET +static const int PIN_AD_BUSY = 14; // BUSY + +// 可选:过采样和模式引脚(-1 表示不配置/已硬件固定) +static const int PIN_AD_OS0 = -1; +static const int PIN_AD_OS1 = -1; +static const int PIN_AD_OS2 = -1; +static const int PIN_AD_SER = -1; // SER=1 串行模式(若已焊 3.3V,可 -1) +static const int PIN_AD_STBY = -1; // STBY=1 正常工作(若已焊 3.3V,可 -1) + +// 量程:用于将 16-bit 原码转换为电压(缺省 ±10V) +static constexpr float AD_FS_VOLTS = 5.0f; // 根据硬件 RANGE 引脚实际设置改成 5.0/2.5 等 + +// 初始化(与 TFT 共用 SPI:外部需已调用 SPI.begin 并指定 MISO=SENSOR_DOUT) +void init(); + +// 触发并读取 8 路原始数据(阻塞等待 BUSY 变低) +void readAll(int16_t* data8); + +// 读取 CH0 原始码 +inline int16_t readCH0() { + int16_t tmp[8]; + readAll(tmp); + return tmp[0]; +} + +// 原码转电压,范围 ±AD_FS_VOLTS +inline float codeToVolts(int16_t code) { + return (static_cast(code) / 32768.0f) * AD_FS_VOLTS; +} + +} // namespace ad7606 + +#endif diff --git a/include/game.h b/include/game.h deleted file mode 100644 index 20a05b3..0000000 --- a/include/game.h +++ /dev/null @@ -1,55 +0,0 @@ -#ifndef GAME_H -#define GAME_H - -#include -#include "ui.h" // for DisplayList and colors - -namespace esp32 { -namespace game { - -using namespace esp32::ui; - -// 棋盘尺寸(经典 10x20) -static const int COLS = 10; -static const int ROWS = 20; - -enum class State : uint8_t { Playing = 0, GameOver = 1 }; - -struct Piece { - uint8_t type; // 0..6 - int8_t x; // 左上角/参考点 - int8_t y; - // 仅使用一个朝向(简化:不提供旋转) - int8_t blocks[4][2]; // 4 个方块偏移 - uint16_t color; -}; - -struct Tetris { - uint8_t board[ROWS][COLS]; // 0=空,其它=颜色索引或标记 - uint16_t colors[8]; // 颜色表(index->RGB565) - Piece current; - uint32_t lastDropMs = 0; - uint32_t dropInterval = 700; // ms - State state = State::Playing; - uint32_t score = 0; - uint32_t rng = 1; -}; - -void init(Tetris& g); -void reset(Tetris& g); -void update(Tetris& g, uint32_t nowMs); - -// 输入 -void moveLeft(Tetris& g); -void moveRight(Tetris& g); -void hardDrop(Tetris& g); - -// UI 构建 -void buildScene(const Tetris& g, DisplayList& out, - int16_t screenW, int16_t screenH, - int16_t cell, int16_t originX, int16_t originY); - -} // namespace game -} // namespace esp32 - -#endif // GAME_H diff --git a/include/timer.h b/include/timer.h deleted file mode 100644 index 16f7eeb..0000000 --- a/include/timer.h +++ /dev/null @@ -1,30 +0,0 @@ -#ifndef TIMER_H -#define TIMER_H - -#include -#include "ui.h" - -namespace esp32 { -namespace timer { - -struct ClockState { - uint32_t startMs = 0; // 起始时间 - uint32_t elapsedMs = 0; // 当前经过时间 - static const int MAX_LAPS = 12; - uint16_t lapCount = 0; - float lapAngles[MAX_LAPS]; // 存储圈记录的角度(秒针位置) - uint32_t lapMs[MAX_LAPS]; // 每次记录的时间戳 (ms) - bool running = true; -}; - -void init(ClockState& s); -void reset(ClockState& s); -void update(ClockState& s, uint32_t nowMs); -void addLap(ClockState& s); -void buildScene(const ClockState& s, esp32::ui::DisplayList& out, - int16_t screenW, int16_t screenH); - -} // namespace timer -} // namespace esp32 - -#endif // TIMER_H diff --git a/src/ad7606.cpp b/src/ad7606.cpp new file mode 100644 index 0000000..c70d7f3 --- /dev/null +++ b/src/ad7606.cpp @@ -0,0 +1,79 @@ +#include "ad7606.h" + +namespace ad7606 { + +static inline void pinWriteOpt(int pin, int level) { + if (pin >= 0) digitalWrite(pin, level); +} +static inline void pinModeOpt(int pin, uint8_t mode) { + if (pin >= 0) pinMode(pin, mode); +} + +static void resetPulse() { + pinWriteOpt(PIN_AD_RESET, LOW); + delayMicroseconds(2); + pinWriteOpt(PIN_AD_RESET, HIGH); + delayMicroseconds(2); + pinWriteOpt(PIN_AD_RESET, LOW); +} + +static void startConvst() { + pinWriteOpt(PIN_AD_CONVST, LOW); + delayMicroseconds(1); + pinWriteOpt(PIN_AD_CONVST, HIGH); + delayMicroseconds(1); +} + +void init() { + // 仅配置 GPIO;SPI 总线由外部统一 SPI.begin(...) 完成(与 TFT 共用) + pinMode(PIN_AD_CS, OUTPUT); + pinMode(PIN_AD_BUSY, INPUT); + pinMode(PIN_AD_RESET, OUTPUT); + pinMode(PIN_AD_CONVST, OUTPUT); + + pinModeOpt(PIN_AD_OS0, OUTPUT); + pinModeOpt(PIN_AD_OS1, OUTPUT); + pinModeOpt(PIN_AD_OS2, OUTPUT); + pinModeOpt(PIN_AD_SER, OUTPUT); + pinModeOpt(PIN_AD_STBY, OUTPUT); + + digitalWrite(PIN_AD_CS, HIGH); + digitalWrite(PIN_AD_CONVST, HIGH); + + // 若引脚未硬件固定,软件拉到默认工作状态 + pinWriteOpt(PIN_AD_SER, HIGH); // 串行模式 + pinWriteOpt(PIN_AD_STBY, HIGH); // 正常工作 + + // 不过采样 OS[2:0] = 000(若 OS 引脚未连,可忽略) + pinWriteOpt(PIN_AD_OS0, LOW); + pinWriteOpt(PIN_AD_OS1, LOW); + pinWriteOpt(PIN_AD_OS2, LOW); + + resetPulse(); + delay(1); + + // 做一次假触发,确保内部状态正常 + startConvst(); +} + +void readAll(int16_t* data8) { + // 触发转换 + startConvst(); + + // 等 BUSY 低(高=转换进行中) + while (digitalRead(PIN_AD_BUSY) == HIGH) { + // 可根据需要加入超时 + } + + // 读取8个16位(RD/SC 由 SPI SCLK 提供) + SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0)); + digitalWrite(PIN_AD_CS, LOW); + for (int i = 0; i < 8; ++i) { + uint16_t raw = SPI.transfer16(0x0000); + data8[i] = static_cast(raw); + } + digitalWrite(PIN_AD_CS, HIGH); + SPI.endTransaction(); +} + +} // namespace ad7606 diff --git a/src/game.cpp b/src/game.cpp deleted file mode 100644 index 6d9062a..0000000 --- a/src/game.cpp +++ /dev/null @@ -1,215 +0,0 @@ -#include "game.h" - -namespace esp32 { -namespace game { - -using namespace esp32::ui; -using namespace esp32::utils; - -static uint32_t xorshift32(uint32_t& s){ - uint32_t x = s; - x ^= x << 13; x ^= x >> 17; x ^= x << 5; s = x; return x; -} - -static uint8_t rand7(uint32_t& s){ return (uint8_t)(xorshift32(s) % 7); } - -static void setPieceShape(Piece& p){ - // 定义 7 种方块在默认朝向下的 4 个格子偏移 - // 坐标以 (0,0) 为参考,落点左上角附近 - switch (p.type){ - case 0: // I 形: 水平 - p.blocks[0][0] = -1; p.blocks[0][1] = 0; - p.blocks[1][0] = 0; p.blocks[1][1] = 0; - p.blocks[2][0] = 1; p.blocks[2][1] = 0; - p.blocks[3][0] = 2; p.blocks[3][1] = 0; break; - case 1: // O 方块 - p.blocks[0][0] = 0; p.blocks[0][1] = 0; - p.blocks[1][0] = 1; p.blocks[1][1] = 0; - p.blocks[2][0] = 0; p.blocks[2][1] = 1; - p.blocks[3][0] = 1; p.blocks[3][1] = 1; break; - case 2: // T - p.blocks[0][0] = -1; p.blocks[0][1] = 0; - p.blocks[1][0] = 0; p.blocks[1][1] = 0; - p.blocks[2][0] = 1; p.blocks[2][1] = 0; - p.blocks[3][0] = 0; p.blocks[3][1] = 1; break; - case 3: // L - p.blocks[0][0] = -1; p.blocks[0][1] = 0; - p.blocks[1][0] = 0; p.blocks[1][1] = 0; - p.blocks[2][0] = 1; p.blocks[2][1] = 0; - p.blocks[3][0] = 1; p.blocks[3][1] = 1; break; - case 4: // J - p.blocks[0][0] = -1; p.blocks[0][1] = 0; - p.blocks[1][0] = 0; p.blocks[1][1] = 0; - p.blocks[2][0] = 1; p.blocks[2][1] = 0; - p.blocks[3][0] = -1; p.blocks[3][1] = 1; break; - case 5: // S - p.blocks[0][0] = 0; p.blocks[0][1] = 0; - p.blocks[1][0] = 1; p.blocks[1][1] = 0; - p.blocks[2][0] = -1; p.blocks[2][1] = 1; - p.blocks[3][0] = 0; p.blocks[3][1] = 1; break; - case 6: // Z - p.blocks[0][0] = -1; p.blocks[0][1] = 0; - p.blocks[1][0] = 0; p.blocks[1][1] = 0; - p.blocks[2][0] = 0; p.blocks[2][1] = 1; - p.blocks[3][0] = 1; p.blocks[3][1] = 1; break; - } -} - -static bool collide(const Tetris& g, const Piece& p, int nx, int ny){ - for (int i=0;i<4;++i){ - int cx = nx + p.blocks[i][0]; - int cy = ny + p.blocks[i][1]; - if (cx < 0 || cx >= COLS || cy >= ROWS) return true; - if (cy >= 0 && g.board[cy][cx] != 0) return true; - } - return false; -} - -static void spawnPiece(Tetris& g){ - g.current.type = rand7(g.rng); - g.current.x = COLS/2 - 1; - g.current.y = -1; // 允许出屏生成 - setPieceShape(g.current); - // 颜色:从表里取 1..7 - g.current.color = g.colors[1 + (g.current.type % 7)]; - if (collide(g, g.current, g.current.x, g.current.y+1)){ - g.state = State::GameOver; - } -} - -static void lockPiece(Tetris& g){ - for (int i=0;i<4;++i){ - int cx = g.current.x + g.current.blocks[i][0]; - int cy = g.current.y + g.current.blocks[i][1]; - if (cy>=0 && cy=0 && cx=0; --y){ - bool full = true; - for (int x=0; x0; --yy){ - for (int x=0;x0){ g.score += cleared * 100; } - return cleared; -} - -void init(Tetris& g){ - // 初始化颜色表 - g.colors[0] = 0; // 未用 - g.colors[1] = WHITE; - g.colors[2] = rgbTo565(0x00FFFF); // CYAN - g.colors[3] = YELLOW; - g.colors[4] = GREEN; - g.colors[5] = BLUE; - g.colors[6] = RED; - g.colors[7] = rgbTo565(0xFF00FF); // MAGENTA - reset(g); -} - -void reset(Tetris& g){ - for (int y=0;y= g.dropInterval){ - g.lastDropMs = nowMs; - if (!collide(g, g.current, g.current.x, g.current.y+1)){ - g.current.y += 1; - } else { - lockPiece(g); - clearLines(g); - spawnPiece(g); - } - } -} - -void moveLeft(Tetris& g){ - if (g.state != State::Playing) return; - if (!collide(g, g.current, g.current.x-1, g.current.y)){ - g.current.x -= 1; - } -} - -void moveRight(Tetris& g){ - if (g.state != State::Playing) return; - if (!collide(g, g.current, g.current.x+1, g.current.y)){ - g.current.x += 1; - } -} - -void hardDrop(Tetris& g){ - if (g.state != State::Playing) return; - while (!collide(g, g.current, g.current.x, g.current.y+1)){ - g.current.y += 1; - } - lockPiece(g); - clearLines(g); - spawnPiece(g); -} - -void buildScene(const Tetris& g, DisplayList& out, - int16_t screenW, int16_t screenH, - int16_t cell, int16_t originX, int16_t originY){ - // 边框 - out.addFillRect(900, originX-2, originY-2, COLS*cell+4, ROWS*cell+4, WHITE); - out.addFillRect(901, originX-1, originY-1, COLS*cell+2, ROWS*cell+2, BLACK); - - // 棋盘格子 - for (int y=0; y=0){ - out.addFillRect(3000 + i, originX + cx*cell, originY + cy*cell, cell-1, cell-1, g.current.color); - } - } - } - - // 顶部文字:Score - char buf[24]; - snprintf(buf, sizeof(buf), "Score: %lu", (unsigned long)g.score); - out.addText(8000, 4, 4, WHITE, buf); - - if (g.state == State::GameOver){ - out.addText(8001, 8, screenH - 24, YELLOW, "Game Over"); - out.addText(8002, 8, screenH - 14, WHITE, "Left: Home Right: Retry"); - } else { - out.addText(8003, 8, screenH - 14, WHITE, "Up: Drop Left/Right: Move"); - } -} - -} // namespace game -} // namespace esp32 diff --git a/src/main.cpp b/src/main.cpp index c0f7e4a..83de1e6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2,11 +2,10 @@ #include #include #include -#include "game.h" #include "init.h" -#include "timer.h" #include "ui.h" #include "utils.h" +#include "ad7606.h" U8G2_FOR_ADAFRUIT_GFX u8g2; // 给 Adafruit_GFX 套一层 U8g2 的“文字引擎” using namespace esp32; @@ -29,27 +28,22 @@ const int BTN_UP = 10; // 上按钮 // 颜色宏兼容已由 utils.h 提供 -// 应用状态(主屏轮播) +// 应用状态 struct AppState { - // 轮播:当前索引与动画偏移(像素) - int currentIndex = 0; // 0: Timer, 1: Web, 2: Game - float carouselOffset = 0.0f; // 动画帧间偏移,完成后归零 - int targetIndex = -1; // 动画结束时要切换到的索引(无动画=-1) - - // 图标资源 - Icon iconTimer; - Icon iconWeb; - Icon iconGame; - - // 场景管理 - enum class Scene : uint8_t { Home = 0, Timer = 1, Game = 2 }; - Scene scene = Scene::Home; - game::Tetris tetris; - timer::ClockState clock; + // 环形缓冲:宽度=屏幕宽;时间窗口=3秒 + static const int GraphW = 128; // 与屏幕宽度一致 + int16_t samples[GraphW]; + int writeIdx = 0; // 下一个写入位置 + int sampleCount = 0; // 已写入样本(<=GraphW) + int16_t lastRaw = 0; // 最新原始码 + uint32_t lastSampleMs = 0; // 上一次采样时间 + static const uint32_t WindowMs = 3000; // 3 秒窗口 + static const uint32_t SampleIntervalMs = WindowMs / GraphW; // 约 23ms }; static UI appUI; static AppState app; + static bool prevLeft = false; static bool prevRight = false; static bool prevUp = false; @@ -67,53 +61,68 @@ static const int LABEL_MARGIN = 12; // 图标与文字竖向间距 static const int CHAR_W = 14; static const int CHAR_H = 14; +// 工具:推入一条新样本(原码) +static inline void pushSample(AppState& s, int16_t code, uint32_t nowMs) { + // 控制写入速率,保持一屏=3秒 + if (nowMs - s.lastSampleMs < AppState::SampleIntervalMs) return; + s.lastSampleMs = nowMs; + s.samples[s.writeIdx] = code; + s.writeIdx = (s.writeIdx + 1) % AppState::GraphW; + if (s.sampleCount < AppState::GraphW) s.sampleCount++; + s.lastRaw = code; +} + // 根据状态输出显示列表 static void buildScene(void* ctx, DisplayList& out) { - AppState* s = (AppState*)ctx; - if (s->scene == AppState::Scene::Home) { - // 顶部标题居中 - 支持中文 - const char* title = "主菜单"; // 中文标题 - out.addText(100, SCREEN_W / 2, 8, WHITE, title, true); // 使用居中 + auto* st = static_cast(ctx); - // 图标与文字 - for (int i = 0; i < 3; ++i) { - int rel = i - s->currentIndex; // 相对索引 - int x = CENTER_X + rel * SPACING + (int)(s->carouselOffset); - if (x + ICON_W < 0 || x > SCREEN_W) - continue; - const Icon* ic = nullptr; - const char* label = ""; - switch (i) { - case 0: - ic = &s->iconTimer; - label = "计时器"; // 中文标签 - break; - case 1: - ic = &s->iconWeb; - label = "网页"; // 中文标签 - break; - case 2: - ic = &s->iconGame; - label = "游戏"; // 中文标签 - break; - } - out.addIcon(10 + i, x, BASE_Y, ic); - // 标签居中显示 - int labelX = x + ICON_W / 2; // 图标中心点 - int labelY = BASE_Y + ICON_H + LABEL_MARGIN; - out.addText(200 + i, labelX, labelY, WHITE, label, true); // 使用居中 - } - } else if (s->scene == AppState::Scene::Game) { - // 游戏画面:棋盘居中 - const int cell = 6; - int boardW = game::COLS * cell; - int boardH = game::ROWS * cell; - int originX = (SCREEN_W - boardW) / 2; - int originY = 16; - game::buildScene(s->tetris, out, SCREEN_W, SCREEN_H, cell, originX, - originY); - } else if (s->scene == AppState::Scene::Timer) { - timer::buildScene(s->clock, out, SCREEN_W, SCREEN_H); + // 背景整屏清除 + out.addFillRect(1, 0, 0, SCREEN_W, SCREEN_H, BLACK); + + // 显示当前原始码(居中顶部,去掉电压) + char line[48]; + snprintf(line, sizeof(line), "CH0: %d", st->lastRaw); + out.addText(2, SCREEN_W / 2, 10, WHITE, line, true); + + // 图形区域:最大化剩余高度,无边框 + const int topY = 24; // 标题高度 + const int graphH = SCREEN_H - topY; // 剩余高度 + const int gw = AppState::GraphW; // 128 + const int gx = 0; + + if (st->sampleCount == 0) return; // 尚无数据 + + // 动态范围(min-200, max+200) + int16_t minV = st->samples[0]; + int16_t maxV = st->samples[0]; + for (int i = 1; i < st->sampleCount; ++i) { + int16_t v = st->samples[i]; + if (v < minV) minV = v; + if (v > maxV) maxV = v; + } + int32_t rangeMin = (int32_t)minV - 200; + int32_t rangeMax = (int32_t)maxV + 200; + if (rangeMin < -32768) rangeMin = -32768; + if (rangeMax > 32767) rangeMax = 32767; + int32_t span = rangeMax - rangeMin; + if (span <= 0) span = 1; + + auto mapY = [&](int16_t code) -> int { + float rel = (float)((int32_t)code - rangeMin) / (float)span; // 0..1 + if (rel < 0.0f) rel = 0.0f; if (rel > 1.0f) rel = 1.0f; + float y = (1.0f - rel) * (graphH - 1); // 倒置 + int iy = topY + (int)(y + 0.5f); + if (iy < topY) iy = topY; + if (iy > topY + graphH - 1) iy = topY + graphH - 1; + return iy; + }; + + int prevY = mapY(st->samples[(st->writeIdx) % gw]); + for (int x = 1; x < gw; ++x) { + int idx = (st->writeIdx + x) % gw; + int y = mapY(st->samples[idx]); + out.addLine(100 + x, gx + x - 1, prevY, gx + x, y, GREEN); + prevY = y; } } @@ -123,8 +132,8 @@ void setup() { pinMode(BTN_RIGHT, INPUT_PULLDOWN); pinMode(BTN_UP, INPUT_PULLDOWN); - // 1. 把硬件 SPI 绑定到你现有的引脚 - SPI.begin(TFT_SCLK, -1 /*MISO 不用*/, TFT_MOSI, TFT_CS); + // 1. 把硬件 SPI 绑定到现有的引脚(与 AD7606 共用:MISO=AD7606 DOUTA) + SPI.begin(TFT_SCLK, ad7606::PIN_AD_MISO, TFT_MOSI, TFT_CS); SPI.setFrequency(27000000); // ST7735 通常 27MHz 左右比较稳 // 2. 初始化屏幕 @@ -156,14 +165,16 @@ void setup() { // 启用内存帧缓冲,避免闪烁(若内存不足会返回 false) appUI.enableBackBuffer(SCREEN_W, SCREEN_H); - // 加载图标(LittleFS: /icons/*.r565,格式: [w][h][pixels]) - // 若不存在,将在渲染时以蓝色占位矩形显示 - UI::loadIconFromFS("/icons/timer.r565", app.iconTimer); - UI::loadIconFromFS("/icons/web.r565", app.iconWeb); - UI::loadIconFromFS("/icons/game.r565", app.iconGame); - // 绑定场景构建器 appUI.setScene(buildScene, &app); + + // 初始化 AD7606(与 TFT 共用 SPI) + ad7606::init(); + + // 清空波形缓存并初始化计时 + for (int i = 0; i < AppState::GraphW; ++i) app.samples[i] = 0; + app.sampleCount = 0; + app.lastSampleMs = millis(); } void loop() { @@ -171,87 +182,14 @@ void loop() { bool rightPressed = digitalRead(BTN_RIGHT) == HIGH; bool upPressed = digitalRead(BTN_UP) == HIGH; - if (app.scene == AppState::Scene::Home) { - // 主页:左右滑动选择,Up 进入游戏 - if (leftPressed && !prevLeft && !appUI.hasActiveTweens()) { - if (app.currentIndex > 0 && app.targetIndex == -1) { - app.targetIndex = app.currentIndex - 1; - app.carouselOffset = 0.0f; - appUI.addTweenFloat(&app.carouselOffset, (float)SPACING, 300); - } - } - if (rightPressed && !prevRight && !appUI.hasActiveTweens()) { - if (app.currentIndex < 2 && app.targetIndex == -1) { - app.targetIndex = app.currentIndex + 1; - app.carouselOffset = 0.0f; - appUI.addTweenFloat(&app.carouselOffset, -(float)SPACING, 300); - } - } - // 进入计时器:index==0 - if (upPressed && !prevUp && app.currentIndex == 0) { - timer::init(app.clock); - app.scene = AppState::Scene::Timer; - } - // 进入游戏:index==2 - if (upPressed && !prevUp && app.currentIndex == 2) { - game::init(app.tetris); - app.scene = AppState::Scene::Game; - } - } else { - if (app.scene == AppState::Scene::Game) { - // 游戏内:左右移动,Up 硬降;GameOver 时 左=返回主页,右=重玩 - if (leftPressed && !prevLeft) { - if (app.tetris.state == game::State::GameOver) { - app.scene = AppState::Scene::Home; - app.targetIndex = -1; - } else { - game::moveLeft(app.tetris); - } - } - if (rightPressed && !prevRight) { - if (app.tetris.state == game::State::GameOver) { - game::reset(app.tetris); - } else { - game::moveRight(app.tetris); - } - } - if (upPressed && !prevUp) { - if (app.tetris.state == game::State::Playing) { - game::hardDrop(app.tetris); - } - } - game::update(app.tetris, millis()); - } else if (app.scene == AppState::Scene::Timer) { - // 计时器:Left 返回主页,Right 重置,Up 增加 lap 标记 - if (leftPressed && !prevLeft) { - app.scene = AppState::Scene::Home; - app.targetIndex = -1; - } - if (rightPressed && !prevRight) { - timer::reset(app.clock); - } - if (upPressed && !prevUp) { - timer::addLap(app.clock); - } - timer::update(app.clock, millis()); - } - } - - prevLeft = leftPressed; - prevRight = rightPressed; - prevUp = upPressed; + // 读取 AD7606 CH0 原始码并按速率写入缓冲 + uint32_t now = millis(); + int16_t ch0 = ad7606::readCH0(); + pushSample(app, ch0, now); // 驱动 UI 帧刷新 appUI.tick(millis()); - // 动画结束:如果没有活动 tween 且存在 targetIndex,则完成索引切换并重置偏移 - if (app.scene == AppState::Scene::Home) { - if (!appUI.hasActiveTweens() && app.targetIndex != -1) { - app.currentIndex = app.targetIndex; - app.targetIndex = -1; - app.carouselOffset = 0.0f; - } - } delay(16); // 简单节流 ~60FPS } diff --git a/src/timer.cpp b/src/timer.cpp deleted file mode 100644 index 3e4168d..0000000 --- a/src/timer.cpp +++ /dev/null @@ -1,139 +0,0 @@ -#include "timer.h" -#include "utils.h" -#include - -namespace esp32 { -namespace timer { - -using namespace esp32::ui; -using namespace esp32::utils; - -static float deg2rad(float d){ return d * 3.1415926f / 180.0f; } -static const int CHAR_W = 6; -static const int CHAR_H = 8; -static const uint16_t CYAN_COLOR = rgbTo565(0x00FFFF); - -void init(ClockState& s){ - s.startMs = millis(); - s.elapsedMs = 0; - s.lapCount = 0; - s.running = true; -} - -void reset(ClockState& s){ - s.startMs = millis(); - s.elapsedMs = 0; - s.lapCount = 0; - s.running = true; -} - -void update(ClockState& s, uint32_t nowMs){ - if (s.running){ - s.elapsedMs = nowMs - s.startMs; - } -} - -void addLap(ClockState& s){ - if (s.lapCount >= ClockState::MAX_LAPS) return; - // 记录当前秒针角度(0-60秒 -> 0-360度) - uint32_t sec = (s.elapsedMs / 1000) % 60; - float angleDeg = (sec / 60.0f) * 360.0f; - s.lapAngles[s.lapCount] = angleDeg; - s.lapMs[s.lapCount] = s.elapsedMs; - s.lapCount++; -} - -void buildScene(const ClockState& s, DisplayList& out, - int16_t screenW, int16_t screenH){ - // 标题 - out.addText(6000, 4, 4, WHITE, "Timer"); - // 时间文本 mm:ss.cc (cc=厘秒) - uint32_t totalCs = s.elapsedMs / 10; - uint32_t totalSec = totalCs / 100; - uint32_t mm = totalSec / 60; - uint32_t ss = totalSec % 60; - uint32_t cs = totalCs % 100; - char buf[16]; - snprintf(buf, sizeof(buf), "%02lu:%02lu.%02lu", (unsigned long)mm, (unsigned long)ss, (unsigned long)cs); - // 背景擦除该区域避免残影 - // out.addFillRect(5999, 0, 14, 72, 9, BLACK); - out.addText(6001, 4, 14, YELLOW, buf); - - // 钟表圆心与半径 - int16_t radius = 54; // 缩小一点,右侧留空间展示 laps - int16_t cx = screenW / 2 ; // 向左偏移为右侧列表腾位置 - int16_t cy = screenH / 2 + 5; // 稍微偏下 - - // 外圈 - out.addCircle(6002, cx, cy, radius, WHITE); - - // 刻度 (12 个, 每 30°),使用短线 - for (int i=0;i<12;++i){ - float ang = deg2rad(i * 30.0f - 90.0f); // 使 0 点在顶端 - int16_t x1 = cx + (int16_t)((radius - 8) * cosf(ang)); - int16_t y1 = cy + (int16_t)((radius - 8) * sinf(ang)); - int16_t x2 = cx + (int16_t)((radius - 2) * cosf(ang)); - int16_t y2 = cy + (int16_t)((radius - 2) * sinf(ang)); - out.addLine(6100 + i, x1, y1, x2, y2, WHITE); - } - - // 时针、分针、秒针角度 - float totalMinutes = s.elapsedMs / 60000.0f; - float hours = fmodf(totalMinutes / 60.0f, 12.0f); - float minutes = fmodf(totalMinutes, 60.0f); - float seconds = fmodf(s.elapsedMs / 1000.0f, 60.0f); - - float hourDeg = (hours / 12.0f) * 360.0f - 90.0f; - float minuteDeg = (minutes / 60.0f) * 360.0f - 90.0f; - float secondDeg = (seconds / 60.0f) * 360.0f - 90.0f; - - // 指针长度 - int16_t hourLen = (int16_t)(radius * 0.5f); - int16_t minuteLen = (int16_t)(radius * 0.7f); - int16_t secondLen = (int16_t)(radius * 0.85f); - - // 计算并添加指针 - auto addHand = [&](uint16_t key, float deg, int16_t len, uint16_t color){ - float rad = deg2rad(deg); - int16_t x2 = cx + (int16_t)(len * cosf(rad)); - int16_t y2 = cy + (int16_t)(len * sinf(rad)); - out.addLine(key, cx, cy, x2, y2, color); - }; - addHand(6200, hourDeg, hourLen, WHITE); - addHand(6201, minuteDeg, minuteLen, GREEN); - addHand(6202, secondDeg, secondLen, RED); - - // Laps:在圆周上标记记录点(用小线段或小圆点) - for (uint16_t i=0;i 6) visible = 6; // 最多显示 6 条 - // 显示最近的 visible 条(倒序) - for (int i = 0; i < visible; ++i) { - int idx = s.lapCount - 1 - i; - uint32_t lapCs = s.lapMs[idx] / 10; - uint32_t lapSec = lapCs / 100; - uint32_t lmm = lapSec / 60; - uint32_t lss = lapSec % 60; - uint32_t lcs = lapCs % 100; - char lapBuf[16]; - snprintf(lapBuf, sizeof(lapBuf), "%02lu:%02lu.%02lu", (unsigned long)lmm, (unsigned long)lss, (unsigned long)lcs); - out.addText(6401 + i, lapListX, lapListY + (i + 1) * CHAR_H + 2, CYAN_COLOR, lapBuf); - } - - // 底部操作提示 - out.addText(6450, 2, screenH - 12, WHITE, "Up: Lap Right: Reset Left: Home"); -} - -} // namespace timer -} // namespace esp32