From 67970f7eb22e6ce05f43a56e260cada7987d26b8 Mon Sep 17 00:00:00 2001 From: feie9456 Date: Tue, 11 Nov 2025 22:45:50 +0800 Subject: [PATCH] init --- .gitignore | 5 + .vscode/extensions.json | 10 ++ .vscode/settings.json | 6 + data/index.html.gz | Bin 0 -> 32846 bytes include/README | 37 +++++ lib/README | 46 ++++++ platformio.ini | 18 +++ src/main.cpp | 321 ++++++++++++++++++++++++++++++++++++++++ test/README | 11 ++ 9 files changed, 454 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 data/index.html.gz create mode 100644 include/README create mode 100644 lib/README create mode 100644 platformio.ini create mode 100644 src/main.cpp create mode 100644 test/README diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..89cc49c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/.vscode/extensions.json @@ -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" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..bfa6f57 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "files.associations": { + "*.dbclient-js": "javascript", + "functional": "cpp" + } +} \ No newline at end of file diff --git a/data/index.html.gz b/data/index.html.gz new file mode 100644 index 0000000000000000000000000000000000000000..58d7f018e841daf608a632587304c51eb9260dff GIT binary patch literal 32846 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 literal 0 HcmV?d00001 diff --git a/include/README b/include/README new file mode 100644 index 0000000..49819c0 --- /dev/null +++ b/include/README @@ -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 diff --git a/lib/README b/lib/README new file mode 100644 index 0000000..9379397 --- /dev/null +++ b/lib/README @@ -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 +#include + +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 diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..f7b6e37 --- /dev/null +++ b/platformio.ini @@ -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 diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..bd498af --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,321 @@ +#include +#include +#include +#include +#include +#include + +#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(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'); + // 一般不用每次 flush,WiFi 底层会合包 +} + +// ================== 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. 先处理 DNS(Captive Portal 必须) + dnsServer.processNextRequest(); + + // 2. 处理 HTTP 连接 + acceptOrServeHttp(); + + // 3. 采样一次 + AD7606_readAll(g_samples); + + // 4. 如果有客户端,就发一行 CH0 raw code + sendRawSample(g_samples[0]); + + delay(1); +} diff --git a/test/README b/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/test/README @@ -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