上传数据
This commit is contained in:
parent
042af6036e
commit
003d96fdca
@ -2,5 +2,8 @@
|
|||||||
"wifi": {
|
"wifi": {
|
||||||
"ssid": "Ti",
|
"ssid": "Ti",
|
||||||
"password": "94544549"
|
"password": "94544549"
|
||||||
|
},
|
||||||
|
"backend": {
|
||||||
|
"url": "http://192.168.5.18:3000/api/data"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
13
src/init.cpp
13
src/init.cpp
@ -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
|
||||||
|
|||||||
455
src/main.cpp
455
src/main.cpp
@ -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_t(CH0 原始码)
|
||||||
|
|
||||||
|
// 采样任务:固定频率触发 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
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user