上传数据

This commit is contained in:
feie9456 2025-11-15 15:22:29 +08:00
parent 042af6036e
commit 003d96fdca
5 changed files with 430 additions and 47 deletions

View File

@ -2,5 +2,8 @@
"wifi": { "wifi": {
"ssid": "Ti", "ssid": "Ti",
"password": "94544549" "password": "94544549"
},
"backend": {
"url": "http://192.168.5.18:3000/api/data"
} }
} }

View File

@ -19,6 +19,9 @@ void connectWiFi();
// 初始化系统 // 初始化系统
void initSystem(Adafruit_ST7735& tft); void initSystem(Adafruit_ST7735& tft);
// 获取后端接口 URL来自 settings.json
const String& getBackendUrl();
} // namespace init } // namespace init
} // namespace esp32 } // namespace esp32

View File

@ -66,7 +66,8 @@ void readAll(int16_t* data8) {
} }
// 读取8个16位RD/SC 由 SPI SCLK 提供) // 读取8个16位RD/SC 由 SPI SCLK 提供)
SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0)); // 提升串行读取速率以缩短读出时间(与 TFT 共用 SPI使用事务隔离
SPI.beginTransaction(SPISettings(16000000, MSBFIRST, SPI_MODE0));
digitalWrite(PIN_AD_CS, LOW); digitalWrite(PIN_AD_CS, LOW);
for (int i = 0; i < 8; ++i) { for (int i = 0; i < 8; ++i) {
uint16_t raw = SPI.transfer16(0x0000); uint16_t raw = SPI.transfer16(0x0000);

View File

@ -10,6 +10,7 @@ namespace init {
// WiFi 配置变量 // WiFi 配置变量
static String wifiSSID = ""; static String wifiSSID = "";
static String wifiPassword = ""; static String wifiPassword = "";
static String backendUrl = "";
// TFT 显示器引用 (在 initSystem 中设置) // TFT 显示器引用 (在 initSystem 中设置)
static Adafruit_ST7735* tftPtr = nullptr; static Adafruit_ST7735* tftPtr = nullptr;
@ -53,6 +54,15 @@ bool loadSettings() {
wifiSSID = doc["wifi"]["ssid"].as<String>(); wifiSSID = doc["wifi"]["ssid"].as<String>();
wifiPassword = doc["wifi"]["password"].as<String>(); wifiPassword = doc["wifi"]["password"].as<String>();
// 读取后端接口(可选)
if (doc.containsKey("backend")) {
backendUrl = doc["backend"]["url"].as<String>();
}
if (backendUrl.isEmpty()) {
// 回退:保持为空或设为默认
backendUrl = "http://nf.xn--876a.net/api/data";
}
tftLog("Config loaded:", GREEN); tftLog("Config loaded:", GREEN);
tftPtr->print(" SSID: "); tftPtr->print(" SSID: ");
tftLog(wifiSSID.c_str(), WHITE); tftLog(wifiSSID.c_str(), WHITE);
@ -103,5 +113,8 @@ void initSystem(Adafruit_ST7735& tft) {
} }
} }
// 提供后端 URL 给外部使用
const String& getBackendUrl() { return backendUrl; }
} // namespace init } // namespace init
} // namespace esp32 } // namespace esp32

View File

@ -1,11 +1,18 @@
#include <Adafruit_GFX.h> #include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h> #include <Adafruit_ST7735.h>
#include <ArduinoJson.h>
#include <HTTPClient.h>
#include <SPI.h> #include <SPI.h>
#include <U8g2_for_Adafruit_GFX.h> #include <U8g2_for_Adafruit_GFX.h>
#include <WiFi.h>
#include <math.h>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <freertos/task.h>
#include "ad7606.h"
#include "init.h" #include "init.h"
#include "ui.h" #include "ui.h"
#include "utils.h" #include "utils.h"
#include "ad7606.h"
U8G2_FOR_ADAFRUIT_GFX u8g2; // 给 Adafruit_GFX 套一层 U8g2 的“文字引擎” U8G2_FOR_ADAFRUIT_GFX u8g2; // 给 Adafruit_GFX 套一层 U8g2 的“文字引擎”
using namespace esp32; using namespace esp32;
@ -22,7 +29,7 @@ Adafruit_ST7735 tft(TFT_CS, TFT_DC, TFT_RST);
using namespace esp32::utils; using namespace esp32::utils;
using namespace esp32::ui; using namespace esp32::ui;
const int BTN_LEFT = 12; // 左按钮 const int BTN_LEFT = 9; // 左按钮
const int BTN_RIGHT = 11; // 右按钮 const int BTN_RIGHT = 11; // 右按钮
const int BTN_UP = 10; // 上按钮 const int BTN_UP = 10; // 上按钮
@ -31,14 +38,37 @@ const int BTN_UP = 10; // 上按钮
// 应用状态 // 应用状态
struct AppState { struct AppState {
// 环形缓冲:宽度=屏幕宽;时间窗口=3秒 // 环形缓冲:宽度=屏幕宽;时间窗口=3秒
static const int GraphW = 128; // 与屏幕宽度一致 static const int GraphW = 128; // 与屏幕宽度一致
int16_t samples[GraphW]; int16_t samples[GraphW];
int writeIdx = 0; // 下一个写入位置 int writeIdx = 0; // 下一个写入位置
int sampleCount = 0; // 已写入样本(<=GraphW int sampleCount = 0; // 已写入样本(<=GraphW
int16_t lastRaw = 0; // 最新原始码 int16_t lastRaw = 0; // 最新原始码
uint32_t lastSampleMs = 0; // 上一次采样时间 uint32_t lastSampleMs = 0; // 上一次采样时间
static const uint32_t WindowMs = 3000; // 3 秒窗口 static const uint32_t WindowMs = 3000; // 3 秒窗口
static const uint32_t SampleIntervalMs = WindowMs / GraphW; // 约 23ms static const uint32_t SampleIntervalMs = WindowMs / GraphW; // 约 23ms
// 标定/拟合mN = a*code + b至少2点生效
float target_mN[4]; // 预设档位0, 3.6, 7.2, 10.8
float capturedCode[4]; // 各档记录到的原始码
bool captured[4]; // 是否已记录
int presetIdx = 0; // 当前要记录的档位索引
int capturedCount = 0; // 已记录的档位数
bool fitReady = false; // 已具备>=2点
float fitA = 0.0f; // mN = a*code + b
float fitB = 0.0f;
// 录制
static const int RecCap = 16384;
int16_t recBuf[RecCap];
int recLen = 0;
bool recActive = false;
uint32_t recStartMs = 0;
uint32_t recEndMs = 0;
// 上传状态提示
char statusMsg[64];
uint32_t statusUntilMs = 0;
uint16_t statusColor = WHITE;
}; };
static UI appUI; static UI appUI;
@ -55,23 +85,129 @@ static const int ICON_W = 48; // 图标宽度
static const int ICON_H = 48; // 图标高度 static const int ICON_H = 48; // 图标高度
static const int SPACING = 72; // 图标中心间距(略大于宽度,保证边缘留白) static const int SPACING = 72; // 图标中心间距(略大于宽度,保证边缘留白)
static const int CENTER_X = (SCREEN_W - ICON_W) / 2; // 中心图标左上角 X static const int CENTER_X = (SCREEN_W - ICON_W) / 2; // 中心图标左上角 X
static const int BASE_Y = 40; // 图标顶端 Y上方留空间给标题 static const int BASE_Y = 40; // 图标顶端 Y上方留空间给标题
static const int LABEL_MARGIN = 12; // 图标与文字竖向间距 static const int LABEL_MARGIN = 12; // 图标与文字竖向间距
static const int CHAR_W = 14; static const int CHAR_W = 14;
static const int CHAR_H = 14; static const int CHAR_H = 14;
// 采样队列:将高速采样与 UI 渲染解耦
static QueueHandle_t g_sampleQ = nullptr; // 队列元素int16_tCH0 原始码)
// 采样任务:固定频率触发 AD7606 读取,推入队列
static void samplerTask(void* param) {
// 目标采样率(可按需调高/调低。示例1000Hz
const TickType_t period = pdMS_TO_TICKS(1);
TickType_t last = xTaskGetTickCount();
for (;;) {
int16_t s = ad7606::readCH0();
if (g_sampleQ) {
// 队列满则丢弃最旧样本先试一次无阻塞发送失败则腾挪1个旧样本
if (xQueueSendToBack(g_sampleQ, &s, 0) != pdTRUE) {
int16_t dummy;
xQueueReceive(g_sampleQ, &dummy, 0);
xQueueSendToBack(g_sampleQ, &s, 0);
}
}
vTaskDelayUntil(&last, period);
}
}
// 工具:推入一条新样本(原码) // 工具:推入一条新样本(原码)
static inline void pushSample(AppState& s, int16_t code, uint32_t nowMs) { static inline void pushSample(AppState& s, int16_t code, uint32_t nowMs) {
// 控制写入速率,保持一屏=3秒 // 控制写入速率,保持一屏=3秒
if (nowMs - s.lastSampleMs < AppState::SampleIntervalMs) return; if (nowMs - s.lastSampleMs < AppState::SampleIntervalMs)
return;
s.lastSampleMs = nowMs; s.lastSampleMs = nowMs;
s.samples[s.writeIdx] = code; s.samples[s.writeIdx] = code;
s.writeIdx = (s.writeIdx + 1) % AppState::GraphW; s.writeIdx = (s.writeIdx + 1) % AppState::GraphW;
if (s.sampleCount < AppState::GraphW) s.sampleCount++; if (s.sampleCount < AppState::GraphW)
s.sampleCount++;
s.lastRaw = code; s.lastRaw = code;
} }
// 发送录制数据到服务器JSON
static void postRecording(AppState& s) {
if (s.recLen <= 0) {
s.statusColor = YELLOW;
snprintf(s.statusMsg, sizeof(s.statusMsg), "无数据可上传");
s.statusUntilMs = millis() + 2000;
return;
}
if (WiFi.status() != WL_CONNECTED) {
s.statusColor = RED;
snprintf(s.statusMsg, sizeof(s.statusMsg), "未连接WiFi");
s.statusUntilMs = millis() + 2000;
return;
}
String payload;
// 估算预留空间每个数最多6字符+逗号
payload.reserve(32 + s.recLen * 7 + 48);
payload += "{";
payload += "\"code\":[";
for (int i = 0; i < s.recLen; ++i) {
payload += String((int)s.recBuf[i]);
if (i + 1 < s.recLen)
payload += ",";
}
payload += "]";
// 附带录制起止时间(毫秒)
payload += ",\"startTime\":";
payload += String(s.recStartMs);
payload += ",\"endTime\":";
payload += String(s.recEndMs);
if (s.fitReady) {
payload += ",\"fit\":{\"a\":";
payload += String(s.fitA, 6);
payload += ",\"b\":";
payload += String(s.fitB, 6);
payload += "}";
}
payload += "}";
printf("上传数据:%s\n", payload.c_str());
HTTPClient http;
http.begin(esp32::init::getBackendUrl());
http.addHeader("Content-Type", "application/json");
http.setTimeout(10000);
int code = http.POST(payload);
String resp = http.getString();
http.end();
if (code > 0) {
// 尝试解析 { success: true, id, timestamp }
JsonDocument doc;
DeserializationError err = deserializeJson(doc, resp);
bool ok = false;
String idStr = "";
String tsStr = "";
if (!err) {
ok = doc["success"].as<bool>();
idStr = doc["id"].as<String>();
tsStr = doc["timestamp"].as<String>();
} else {
ok = resp.indexOf("\"success\":true") >= 0; // 兜底
}
if (ok) {
s.statusColor = GREEN;
if (idStr.length())
snprintf(s.statusMsg, sizeof(s.statusMsg), "上传成功\nID:%s",
idStr.c_str());
else
snprintf(s.statusMsg, sizeof(s.statusMsg), "上传成功");
s.statusUntilMs = millis() + 3000;
} else {
s.statusColor = RED;
snprintf(s.statusMsg, sizeof(s.statusMsg), "上传失败(%d)", code);
s.statusUntilMs = millis() + 3000;
}
} else {
s.statusColor = RED;
snprintf(s.statusMsg, sizeof(s.statusMsg), "HTTP错误(%d)", code);
s.statusUntilMs = millis() + 3000;
}
}
// 根据状态输出显示列表 // 根据状态输出显示列表
static void buildScene(void* ctx, DisplayList& out) { static void buildScene(void* ctx, DisplayList& out) {
auto* st = static_cast<AppState*>(ctx); auto* st = static_cast<AppState*>(ctx);
@ -79,44 +215,176 @@ static void buildScene(void* ctx, DisplayList& out) {
// 背景整屏清除 // 背景整屏清除
out.addFillRect(1, 0, 0, SCREEN_W, SCREEN_H, BLACK); out.addFillRect(1, 0, 0, SCREEN_W, SCREEN_H, BLACK);
// 显示当前原始码(居中顶部,去掉电压) // 第一行:显示原始码 并在拟合可用时附加 mN
char line[48]; char line[64];
snprintf(line, sizeof(line), "CH0: %d", st->lastRaw); if (st->fitReady) {
float currM = st->fitA * (float)st->lastRaw + st->fitB;
snprintf(line, sizeof(line), "%d it %.3f mN", st->lastRaw, currM);
} else {
snprintf(line, sizeof(line), "%d it", st->lastRaw);
}
out.addText(2, SCREEN_W / 2, 10, WHITE, line, true); out.addText(2, SCREEN_W / 2, 10, WHITE, line, true);
// 右上角录制提示:红色圆点(空心两层加粗)+ 录制点数
if (st->recActive) {
int cx = SCREEN_W - 30;
int cy = SCREEN_H - 6;
int r = 4;
out.addCircle(5, cx, cy, r, RED);
out.addCircle(6, cx, cy, r - 1, RED);
char recCnt[16];
snprintf(recCnt, sizeof(recCnt), "%d", st->recLen);
uint16_t cntColor = esp32::utils::rgbTo565(0xFFCCCC);
out.addText(7, cx + 8, cy - 4, cntColor, recCnt, false);
}
// 第二行:格式如 "[0*] 3.6 7.2 10.8"[]表示当前选择,*表示该档已记录
char info[64];
info[0] = '\0';
for (int i = 0; i < 4; ++i) {
char valStr[12];
float tgt = st->target_mN[i];
if (fabsf(tgt) < 0.05f) {
snprintf(valStr, sizeof(valStr), "0");
} else {
snprintf(valStr, sizeof(valStr), "%.1f", tgt);
}
char seg[16];
if (i == st->presetIdx) {
if (st->captured[i])
snprintf(seg, sizeof(seg), "[%s*]", valStr);
else
snprintf(seg, sizeof(seg), "[%s]", valStr);
} else {
if (st->captured[i])
snprintf(seg, sizeof(seg), "%s*", valStr);
else
snprintf(seg, sizeof(seg), "%s", valStr);
}
size_t cur = strlen(info);
if (cur + strlen(seg) + 2 < sizeof(info)) {
if (cur > 0)
strcat(info, " ");
strcat(info, seg);
}
}
uint16_t infoColor = esp32::utils::rgbTo565(0xFFD060);
out.addText(3, SCREEN_W / 2, 22, infoColor, info, true);
// 图形区域:最大化剩余高度,无边框 // 图形区域:最大化剩余高度,无边框
const int topY = 24; // 标题高度 const int topY = 40; // 标题高度(两行信息)
const int graphH = SCREEN_H - topY; // 剩余高度 const int graphH = SCREEN_H - topY - 4; // 剩余高度
const int gw = AppState::GraphW; // 128 const int gw = AppState::GraphW; // 128
const int gx = 0; const int gx = 0;
if (st->sampleCount == 0) return; // 尚无数据 if (st->sampleCount == 0)
return; // 尚无数据
// 动态范围min-200, max+200 // 动态范围原始码模式min-200/max+200拟合后mN 加适度留白)
int16_t minV = st->samples[0]; bool useFit = st->fitReady;
int16_t maxV = st->samples[0]; float fMin, fMax, fSpan, fPad;
for (int i = 1; i < st->sampleCount; ++i) { if (!useFit) {
int16_t v = st->samples[i]; int16_t minV = st->samples[0];
if (v < minV) minV = v; int16_t maxV = st->samples[0];
if (v > maxV) maxV = v; 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;
fMin = (float)rangeMin;
fMax = (float)rangeMax;
fSpan = fMax - fMin;
if (fSpan <= 0.0f)
fSpan = 1.0f;
} else {
// 将样本转换为 mN 后求范围
float minM = st->fitA * (float)st->samples[0] + st->fitB;
float maxM = minM;
for (int i = 1; i < st->sampleCount; ++i) {
float m = st->fitA * (float)st->samples[i] + st->fitB;
if (m < minM)
minM = m;
if (m > maxM)
maxM = m;
}
// mN 下给一个小的固定留白0.2mN
fMin = minM - 0.2f;
fMax = maxM + 0.2f;
if (fMin > fMax) {
float t = fMin;
fMin = fMax;
fMax = t;
}
fSpan = fMax - fMin;
if (fSpan <= 1e-6f)
fSpan = 1.0f; // 避免除0
} }
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 { auto mapY = [&](int16_t code) -> int {
float rel = (float)((int32_t)code - rangeMin) / (float)span; // 0..1 float val = useFit ? (st->fitA * (float)code + st->fitB) : (float)code;
if (rel < 0.0f) rel = 0.0f; if (rel > 1.0f) rel = 1.0f; float rel = (val - fMin) / fSpan; // 0..1
float y = (1.0f - rel) * (graphH - 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); int iy = topY + (int)(y + 0.5f);
if (iy < topY) iy = topY; if (iy < topY)
if (iy > topY + graphH - 1) iy = topY + graphH - 1; iy = topY;
if (iy > topY + graphH - 1)
iy = topY + graphH - 1;
return iy; return iy;
}; };
// 绘制 Y 轴刻度与网格
uint16_t gridColor = esp32::utils::rgbTo565(0x303030);
uint16_t tickColor = esp32::utils::rgbTo565(0x606060);
const int ticks = 4; // 分成4段5条刻度线
for (int i = 0; i <= ticks; ++i) {
float val = fMin + (fSpan * i) / ticks;
// 为了绘制刻度线,构造一个虚拟 code 值逆映射并用 mapY 仅为获取 y 像素
int yPix;
if (useFit) {
// 近似:在当前 y 下对应的 val 直接用反推像素到值,再直接求 y 像素
float rel = (val - fMin) / fSpan;
if (rel < 0)
rel = 0;
if (rel > 1)
rel = 1;
float y = (1.0f - rel) * (graphH - 1);
yPix = topY + (int)(y + 0.5f);
} else {
// 同上,直接由范围转 y 像素
float rel = (val - fMin) / fSpan;
if (rel < 0)
rel = 0;
if (rel > 1)
rel = 1;
float y = (1.0f - rel) * (graphH - 1);
yPix = topY + (int)(y + 0.5f);
}
// 网格横线
out.addLine(20 + i, 0, yPix, SCREEN_W - 1, yPix, gridColor);
// 左侧短刻度
out.addLine(40 + i, 0, yPix, 3, yPix, tickColor);
// 标签
char lab[20];
if (useFit) {
snprintf(lab, sizeof(lab), "%.1f", val);
} else {
snprintf(lab, sizeof(lab), "%d", (int)val);
}
out.addText(60 + i, 6, yPix - 6, tickColor, lab, false);
}
int prevY = mapY(st->samples[(st->writeIdx) % gw]); int prevY = mapY(st->samples[(st->writeIdx) % gw]);
for (int x = 1; x < gw; ++x) { for (int x = 1; x < gw; ++x) {
int idx = (st->writeIdx + x) % gw; int idx = (st->writeIdx + x) % gw;
@ -124,6 +392,13 @@ static void buildScene(void* ctx, DisplayList& out) {
out.addLine(100 + x, gx + x - 1, prevY, gx + x, y, GREEN); out.addLine(100 + x, gx + x - 1, prevY, gx + x, y, GREEN);
prevY = y; prevY = y;
} }
// 底部上传状态提示(临时显示)
uint32_t nowMs = millis();
if (st->statusUntilMs > nowMs && st->statusMsg[0] != '\0') {
out.addText(400, SCREEN_W / 2, SCREEN_H - 10, st->statusColor,
st->statusMsg, true);
}
} }
void setup() { void setup() {
@ -144,8 +419,8 @@ void setup() {
// ★ 关键:把 u8g2 挂到 tft 上 // ★ 关键:把 u8g2 挂到 tft 上
u8g2.begin(tft); u8g2.begin(tft);
// 连接到 Adafruit_GFX :contentReference[oaicite:2]{index=2} // 连接到 Adafruit_GFX :contentReference[oaicite:2]{index=2}
u8g2.setFontMode(1); // 透明背景(默认也是 1 u8g2.setFontMode(1); // 透明背景(默认也是 1
u8g2.setFontDirection(0); // 正常从左到右 u8g2.setFontDirection(0); // 正常从左到右
u8g2.setForegroundColor(WHITE); // 文字颜色 u8g2.setForegroundColor(WHITE); // 文字颜色
// 使用更小的中文字体 // 使用更小的中文字体
@ -171,10 +446,31 @@ void setup() {
// 初始化 AD7606与 TFT 共用 SPI // 初始化 AD7606与 TFT 共用 SPI
ad7606::init(); ad7606::init();
// 创建采样队列与任务(将采样放到另一核,降低 UI 阻塞影响)
g_sampleQ = xQueueCreate(2048, sizeof(int16_t));
// 在 Core 0 上跑采样任务,优先级略高于默认
xTaskCreatePinnedToCore(samplerTask, "sampler", 4096, nullptr, 3, nullptr, 0);
// 清空波形缓存并初始化计时 // 清空波形缓存并初始化计时
for (int i = 0; i < AppState::GraphW; ++i) app.samples[i] = 0; for (int i = 0; i < AppState::GraphW; ++i)
app.samples[i] = 0;
app.sampleCount = 0; app.sampleCount = 0;
app.lastSampleMs = millis(); app.lastSampleMs = millis();
// 初始化标定目标
app.target_mN[0] = 0.0f;
app.target_mN[1] = 3.6f;
app.target_mN[2] = 7.2f;
app.target_mN[3] = 10.8f;
for (int i = 0; i < 4; ++i) {
app.captured[i] = false;
app.capturedCode[i] = 0.0f;
}
app.presetIdx = 0;
app.capturedCount = 0;
app.fitReady = false;
app.fitA = 0.0f;
app.fitB = 0.0f;
} }
void loop() { void loop() {
@ -182,14 +478,81 @@ void loop() {
bool rightPressed = digitalRead(BTN_RIGHT) == HIGH; bool rightPressed = digitalRead(BTN_RIGHT) == HIGH;
bool upPressed = digitalRead(BTN_UP) == HIGH; bool upPressed = digitalRead(BTN_UP) == HIGH;
// 读取 AD7606 CH0 原始码并按速率写入缓冲 // 从采样队列取尽样本:
// - 显示:按时间节流到 128 点/3秒pushSample 内已限速)
// - 录制:不节流,全部写入 recBuf
uint32_t now = millis(); uint32_t now = millis();
int16_t ch0 = ad7606::readCH0(); int drained = 0;
pushSample(app, ch0, now); int16_t ch0;
while (g_sampleQ && xQueueReceive(g_sampleQ, &ch0, 0) == pdTRUE) {
drained++;
app.lastRaw = ch0;
// 显示按时间节流
pushSample(app, ch0, now);
now = millis();
// 录制不节流
if (app.recActive && app.recLen < AppState::RecCap) {
app.recBuf[app.recLen++] = ch0;
}
}
// BTN_RIGHT记录当前电压到当前档位并循环到下一档
if (rightPressed && !prevRight) {
float c = (float)app.lastRaw;
int idx = app.presetIdx;
app.capturedCode[idx] = c;
if (!app.captured[idx]) {
app.captured[idx] = true;
app.capturedCount++;
}
// 计算线性拟合(使用已记录点的最小二乘)
if (app.capturedCount >= 2) {
double sx = 0, sy = 0, sxx = 0, sxy = 0;
int n = 0;
for (int i = 0; i < 4; ++i) {
if (!app.captured[i])
continue;
double x = app.capturedCode[i];
double y = app.target_mN[i];
sx += x;
sy += y;
sxx += x * x;
sxy += x * y;
n++;
}
double denom = (n * sxx - sx * sx);
if (n >= 2 && fabs(denom) > 1e-9) {
app.fitA = (float)((n * sxy - sx * sy) / denom);
app.fitB = (float)((sy - app.fitA * sx) / n);
app.fitReady = true;
}
}
// 下一个档位
app.presetIdx = (app.presetIdx + 1) % 4;
}
// 左键:开始/停止录制,停止后上传
if (leftPressed && !prevLeft) {
if (!app.recActive) {
app.recActive = true;
app.recLen = 0;
app.recStartMs = millis();
app.recEndMs = 0;
} else {
app.recActive = false;
app.recEndMs = millis();
postRecording(app);
}
}
// printf("l:%s, r:%s, u:%s\n", leftPressed ? "Y" : "N", rightPressed ? "Y"
// : "N", upPressed ? "Y" : "N"); 驱动 UI 帧刷新
// 驱动 UI 帧刷新
appUI.tick(millis()); appUI.tick(millis());
prevLeft = leftPressed;
delay(16); // 简单节流 ~60FPS prevRight = rightPressed;
prevUp = upPressed;
// delay(16); // 简单节流 ~60FPS
} }