This commit is contained in:
feie9456 2025-11-11 22:45:50 +08:00
commit 67970f7eb2
9 changed files with 454 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch

10
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,10 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"platformio.platformio-ide"
],
"unwantedRecommendations": [
"ms-vscode.cpptools-extension-pack"
]
}

6
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"files.associations": {
"*.dbclient-js": "javascript",
"functional": "cpp"
}
}

BIN
data/index.html.gz Normal file

Binary file not shown.

37
include/README Normal file
View File

@ -0,0 +1,37 @@
This directory is intended for project header files.
A header file is a file containing C declarations and macro definitions
to be shared between several project source files. You request the use of a
header file in your project source file (C, C++, etc) located in `src` folder
by including it, with the C preprocessing directive `#include'.
```src/main.c
#include "header.h"
int main (void)
{
...
}
```
Including a header file produces the same results as copying the header file
into each source file that needs it. Such copying would be time-consuming
and error-prone. With a header file, the related declarations appear
in only one place. If they need to be changed, they can be changed in one
place, and programs that include the header file will automatically use the
new version when next recompiled. The header file eliminates the labor of
finding and changing all the copies as well as the risk that a failure to
find one copy will result in inconsistencies within a program.
In C, the convention is to give header files names that end with `.h'.
Read more about using header files in official GCC documentation:
* Include Syntax
* Include Operation
* Once-Only Headers
* Computed Includes
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html

46
lib/README Normal file
View File

@ -0,0 +1,46 @@
This directory is intended for project specific (private) libraries.
PlatformIO will compile them to static libraries and link into the executable file.
The source code of each library should be placed in a separate directory
("lib/your_library_name/[Code]").
For example, see the structure of the following example libraries `Foo` and `Bar`:
|--lib
| |
| |--Bar
| | |--docs
| | |--examples
| | |--src
| | |- Bar.c
| | |- Bar.h
| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
| |
| |--Foo
| | |- Foo.c
| | |- Foo.h
| |
| |- README --> THIS FILE
|
|- platformio.ini
|--src
|- main.c
Example contents of `src/main.c` using Foo and Bar:
```
#include <Foo.h>
#include <Bar.h>
int main (void)
{
...
}
```
The PlatformIO Library Dependency Finder will find automatically dependent
libraries by scanning project source files.
More information about PlatformIO Library Dependency Finder
- https://docs.platformio.org/page/librarymanager/ldf.html

18
platformio.ini Normal file
View File

@ -0,0 +1,18 @@
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[env:rymcu-esp32-s3-devkitc-1]
platform = espressif32
board = rymcu-esp32-s3-devkitc-1
framework = arduino
upload_speed = 921600
monitor_speed = 115200
build_flags = -std=gnu++2a
board_build.filesystem = littlefs

321
src/main.cpp Normal file
View File

@ -0,0 +1,321 @@
#include <Arduino.h>
#include <FS.h>
#include <LittleFS.h>
#include <SPI.h>
#include <WiFi.h>
#include <DNSServer.h>
#define FS LittleFS
// ================== WiFi AP 配置 ==================
const char* AP_SSID = "AD7606_AP";
const char* AP_PASS = "12345678";
WiFiServer httpServer(80);
WiFiClient streamClient;
bool streamClientActive = false;
// Captive Portal 用 DNS 服务器
const byte DNS_PORT = 53;
DNSServer dnsServer;
// ================== AD7606 引脚配置(按实际改) ==================
static const int PIN_AD_MISO = 8; // DB7 / DOUTA
static const int PIN_AD_SCK = 11; // RD/SC
static const int PIN_AD_CS = 12; // CS_N
static const int PIN_AD_CONVSTA = 13; // CO-A
static const int PIN_AD_CONVSTB = 13; // CO-B与 CO-A 共用)
static const int PIN_AD_RESET = 5; // REST
static const int PIN_AD_BUSY = 14; // BUSY
static const int PIN_AD_OS0 = 6; // OSI0
static const int PIN_AD_OS1 = 7; // OSI1
static const int PIN_AD_OS2 = 15; // OSI2
// 如果 SER / STBY 直接焊到 3.3V,这两个 PIN 物理上可以不接
static const int PIN_AD_SER = 3; // SER
static const int PIN_AD_STBY = 4; // STBY
SPIClass AD7606_SPI(FSPI);
int16_t g_samples[8];
// ================== AD7606 相关函数 ==================
void AD7606_reset() {
digitalWrite(PIN_AD_RESET, LOW);
delayMicroseconds(2);
digitalWrite(PIN_AD_RESET, HIGH);
delayMicroseconds(2);
digitalWrite(PIN_AD_RESET, LOW);
}
void AD7606_startConvst() {
digitalWrite(PIN_AD_CONVSTA, LOW);
digitalWrite(PIN_AD_CONVSTB, LOW);
delayMicroseconds(1);
digitalWrite(PIN_AD_CONVSTA, HIGH);
digitalWrite(PIN_AD_CONVSTB, HIGH);
delayMicroseconds(1);
}
void AD7606_readAll(int16_t* data) {
// 1. 触发转换
AD7606_startConvst();
// 2. 等 BUSY 变低BUSY 高 = 正在转换)
while (digitalRead(PIN_AD_BUSY) == HIGH) {
// 可选的小延时
// delayMicroseconds(1);
}
// 3. SPI 连续读取 8 个 16-bit
AD7606_SPI.beginTransaction(SPISettings(8000000, // 8MHz
MSBFIRST, SPI_MODE0));
digitalWrite(PIN_AD_CS, LOW);
for (int i = 0; i < 8; ++i) {
uint16_t raw = AD7606_SPI.transfer16(0x0000);
data[i] = static_cast<int16_t>(raw);
}
digitalWrite(PIN_AD_CS, HIGH);
AD7606_SPI.endTransaction();
}
float AD7606_codeToVolt(int16_t code, bool range10V) {
float fullScale = range10V ? 10.0f : 5.0f;
return (float)code * (fullScale / 32768.0f);
}
void AD7606_init() {
pinMode(PIN_AD_CS, OUTPUT);
pinMode(PIN_AD_SCK, OUTPUT);
pinMode(PIN_AD_MISO, INPUT);
pinMode(PIN_AD_CONVSTA, OUTPUT);
pinMode(PIN_AD_CONVSTB, OUTPUT);
pinMode(PIN_AD_RESET, OUTPUT);
pinMode(PIN_AD_BUSY, INPUT);
pinMode(PIN_AD_OS0, OUTPUT);
pinMode(PIN_AD_OS1, OUTPUT);
pinMode(PIN_AD_OS2, OUTPUT);
pinMode(PIN_AD_SER, OUTPUT);
pinMode(PIN_AD_STBY, OUTPUT);
digitalWrite(PIN_AD_CS, HIGH);
digitalWrite(PIN_AD_CONVSTA, HIGH);
digitalWrite(PIN_AD_CONVSTB, HIGH);
// 串行模式 & 正常工作(如果硬件已拉 3.3V,这两句只是保险)
digitalWrite(PIN_AD_SER, HIGH); // SER=1 -> 串行
digitalWrite(PIN_AD_STBY, HIGH); // STBY=1 -> 正常工作
// 不过采样 OS[2:0] = 000
digitalWrite(PIN_AD_OS0, LOW);
digitalWrite(PIN_AD_OS1, LOW);
digitalWrite(PIN_AD_OS2, LOW);
AD7606_reset();
delay(1);
const int dummyMosi = 9; // MOSI 随便给个没用的脚
AD7606_SPI.begin(PIN_AD_SCK, PIN_AD_MISO, dummyMosi, PIN_AD_CS);
AD7606_startConvst();
}
// ================== HTTP + Captive Portal 部分 ==================
// 返回 gzip 压缩的 index.html.gz
void serveIndexHtml(WiFiClient& client) {
File f = FS.open("/index.html.gz", "r");
if (!f || f.isDirectory()) {
client.print(
"HTTP/1.1 500 Internal Server Error\r\n"
"Content-Type: text/plain\r\n"
"Connection: close\r\n"
"\r\n"
"index.html not found\r\n");
client.stop();
return;
}
client.print(
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"Content-Encoding: gzip\r\n"
"Connection: close\r\n"
"Cache-Control: no-cache\r\n"
"Access-Control-Allow-Origin: *\r\n"
"\r\n");
uint8_t buf[1024];
while (true) {
size_t n = f.read(buf, sizeof(buf));
if (n == 0) break;
client.write(buf, n);
}
f.close();
client.stop();
}
// 发送 302 重定向到 /index.html可选用
void redirectToPortal(WiFiClient& client) {
client.print(
"HTTP/1.1 302 Found\r\n"
"Location: /index.html\r\n"
"Connection: close\r\n"
"Cache-Control: no-cache\r\n"
"Access-Control-Allow-Origin: *\r\n"
"\r\n");
client.stop();
}
// 简易 HTTP 服务器 + Captive Portal 路由
void acceptOrServeHttp() {
// 1) 如果已有 stream 客户端,先检测是否掉线
if (streamClientActive && !streamClient.connected()) {
Serial.println("[HTTP] stream client disconnected");
streamClient.stop();
streamClientActive = false;
}
// 2) 接受新连接
WiFiClient client = httpServer.available();
if (!client) {
return; // 没有新客户端
}
client.setTimeout(200); // 避免 readStringUntil 长时间阻塞 (ms)
// 3) 读取首行
String reqLine = client.readStringUntil('\r');
client.readStringUntil('\n'); // 丢弃 \n
if (reqLine.length() == 0) {
client.stop();
return;
}
// 4) 解析 path
String path;
int firstSpace = reqLine.indexOf(' ');
int secondSpace = reqLine.indexOf(' ', firstSpace + 1);
if (firstSpace >= 0 && secondSpace > firstSpace) {
path = reqLine.substring(firstSpace + 1, secondSpace);
}
// 5) 丢弃其余 header最多读取一定行数防御
for (int i = 0; i < 40 && client.connected(); ++i) {
String line = client.readStringUntil('\n');
if (line == "\r" || line.length() == 0) break;
}
// 6) 路由:
// - /stream: 长连接输出采样
// - 其他任意路径 -> Captive Portal 页面 (index.html)
if (path == "/stream") {
if (streamClientActive) {
// 已经有一个 streaming占用中
client.print(
"HTTP/1.1 409 Conflict\r\n"
"Content-Type: text/plain\r\n"
"Connection: close\r\n"
"Cache-Control: no-cache\r\n"
"Access-Control-Allow-Origin: *\r\n"
"\r\n"
"stream already active\r\n");
client.stop();
} else {
client.print(
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/plain\r\n"
"Cache-Control: no-cache\r\n"
"Connection: keep-alive\r\n"
"Access-Control-Allow-Origin: *\r\n"
"\r\n");
streamClient = client; // 复制句柄,保持长连接
streamClientActive = true;
Serial.println("[HTTP] stream client connected /stream");
// 不调用 stop()
}
} else {
// 这里可以选择 serveIndexHtml 或 redirectToPortal
// 为了简单,直接返回 index.html 内容
serveIndexHtml(client);
// 或者改成:
// redirectToPortal(client);
}
}
// 发送一帧 CH0 原始码值(只有一个数字 + '\n'
void sendRawSample(int16_t ch0_code) {
if (!streamClientActive || !streamClient.connected()) {
return;
}
streamClient.print(ch0_code);
streamClient.print('\n');
// 一般不用每次 flushWiFi 底层会合包
}
// ================== Arduino 入口 ==================
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("Mounting LittleFS...");
if (!FS.begin(true)) { // true = 如果挂载失败就尝试格式化
Serial.println("LittleFS mount failed");
} else {
Serial.println("LittleFS mounted OK");
}
Serial.println("Init AD7606 (ESP32-S3 + SPI)...");
AD7606_init();
Serial.println("AD7606 init done.");
// 启动 AP
WiFi.mode(WIFI_AP);
bool apOk = WiFi.softAP(AP_SSID, AP_PASS);
if (apOk) {
Serial.print("AP started, SSID: ");
Serial.println(AP_SSID);
Serial.print("AP IP: ");
Serial.println(WiFi.softAPIP());
} else {
Serial.println("AP start FAILED");
}
// 启动 DNS Server所有域名都解析到 AP IP
IPAddress apIP = WiFi.softAPIP();
dnsServer.start(DNS_PORT, "*", apIP);
Serial.print("DNS server started on ");
Serial.print(apIP);
Serial.print(":");
Serial.println(DNS_PORT);
// 启动 HTTP Server
httpServer.begin();
Serial.println("HTTP server started on port 80");
}
void loop() {
// 1. 先处理 DNSCaptive Portal 必须)
dnsServer.processNextRequest();
// 2. 处理 HTTP 连接
acceptOrServeHttp();
// 3. 采样一次
AD7606_readAll(g_samples);
// 4. 如果有客户端,就发一行 CH0 raw code
sendRawSample(g_samples[0]);
delay(1);
}

11
test/README Normal file
View File

@ -0,0 +1,11 @@
This directory is intended for PlatformIO Test Runner and project tests.
Unit Testing is a software testing method by which individual units of
source code, sets of one or more MCU program modules together with associated
control data, usage procedures, and operating procedures, are tested to
determine whether they are fit for use. Unit testing finds problems early
in the development cycle.
More information about PlatformIO Unit Testing:
- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html