zhensolid 3 mesiacov pred
commit
969d71ba08
8 zmenil súbory, kde vykonal 619 pridanie a 0 odobranie
  1. 5 0
      .gitignore
  2. 10 0
      .vscode/extensions.json
  3. 105 0
      data/index.html
  4. 37 0
      include/README
  5. 46 0
      lib/README
  6. 20 0
      platformio.ini
  7. 385 0
      src/main.cpp
  8. 11 0
      test/README

+ 5 - 0
.gitignore

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

+ 10 - 0
.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"
+    ]
+}

+ 105 - 0
data/index.html

@@ -0,0 +1,105 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+    <title>ESP32音乐播放器</title>
+    <meta charset="UTF-8">
+    <style>
+        body {
+            font-family: Arial, sans-serif;
+            max-width: 800px;
+            margin: 0 auto;
+            padding: 20px;
+        }
+
+        .upload-form {
+            border: 2px dashed #ccc;
+            padding: 20px;
+            margin: 20px 0;
+        }
+
+        .file-list {
+            margin: 20px 0;
+        }
+
+        .file-item {
+            display: flex;
+            justify-content: space-between;
+            padding: 10px;
+            border-bottom: 1px solid #eee;
+        }
+    </style>
+</head>
+
+<body>
+    <h1>ESP32音乐播放器</h1>
+
+    <div class="upload-form">
+        <h2>上传音乐文件</h2>
+        <form id="uploadForm">
+            <input type="file" id="audioFile" accept="audio/*" required>
+            <button type="submit">上传</button>
+        </form>
+    </div>
+
+    <div class="file-list">
+        <h2>已上传的音乐</h2>
+        <div id="fileList">
+            <!-- 文件列表将通过JavaScript动态填充 -->
+        </div>
+    </div>
+
+    <script>
+        document.getElementById('uploadForm').onsubmit = async (e) => {
+            e.preventDefault();
+            const formData = new FormData();
+            const file = document.getElementById('audioFile').files[0];
+            formData.append('file', file);
+
+            try {
+                const response = await fetch('/upload', {
+                    method: 'POST',
+                    body: formData
+                });
+                if (response.ok) {
+                    alert('文件上传成功');
+                    // 刷新文件列表
+                    loadFileList();
+                } else {
+                    alert('上传失败');
+                }
+            } catch (error) {
+                console.error('Error:', error);
+                alert('上传出错');
+            }
+        };
+
+        function playFile(filename) {
+            fetch(`/play?file=${encodeURIComponent(filename)}`)
+                .then(response => response.text())
+                .then(result => alert(result))
+                .catch(error => alert('播放失败'));
+        }
+
+        async function loadFileList() {
+            try {
+                const response = await fetch('/list');
+                const files = await response.json();
+                const fileList = document.getElementById('fileList');
+                fileList.innerHTML = files.map(file => `
+                    <div class="file-item">
+                        <span>${file}</span>
+                        <button onclick="playFile('${file}')">播放</button>
+                    </div>
+                `).join('');
+            } catch (error) {
+                console.error('Error:', error);
+            }
+        }
+
+        // 页面加载时获取文件列表
+        loadFileList();
+    </script>
+</body>
+
+</html>

+ 37 - 0
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

+ 46 - 0
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 <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

+ 20 - 0
platformio.ini

@@ -0,0 +1,20 @@
+[env:esp32-s3-devkitm-1]
+platform = espressif32
+board = esp32-s3-devkitm-1
+framework = arduino
+monitor_speed = 115200
+board_build.flash_mode = qio
+board_build.f_flash = 80000000L
+board_build.partitions = huge_app.csv
+
+; 启用PSRAM
+build_flags = 
+    -DCORE_DEBUG_LEVEL=5
+    -DBOARD_HAS_PSRAM
+    -mfix-esp32-psram-cache-issue
+
+; 项目依赖
+lib_deps =
+    https://github.com/me-no-dev/ESPAsyncWebServer.git
+    https://github.com/me-no-dev/AsyncTCP.git
+    bblanchon/ArduinoJson @ ^6.21.3

+ 385 - 0
src/main.cpp

@@ -0,0 +1,385 @@
+#include <Arduino.h>
+#include <WiFi.h>
+#include <ESPAsyncWebServer.h>
+#include <AsyncTCP.h>
+#include <SPIFFS.h>
+#include <driver/i2s.h>
+
+// WiFi配置
+const char *ssid = "zlsh-office";
+const char *password = "zlsh2018";
+
+// I2S配置
+#define I2S_NUM I2S_NUM_0           // I2S 端口号
+#define I2S_BCK_IO 6                // 位时钟
+#define I2S_WS_IO 4                 // 字选择
+#define I2S_DO_IO 5                 // 数据输出
+#define I2S_DI_IO I2S_PIN_NO_CHANGE // 不使用数据输入
+
+// I2S配置结构体
+i2s_config_t i2s_config = {
+    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
+    .sample_rate = 44100,
+    .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
+    .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
+    .communication_format = I2S_COMM_FORMAT_STAND_I2S,
+    .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
+    .dma_buf_count = 16,
+    .dma_buf_len = 512,
+    .use_apll = true,
+    .tx_desc_auto_clear = true,
+    .fixed_mclk = 0};
+
+// I2S引脚配置
+i2s_pin_config_t pin_config = {
+    .mck_io_num = I2S_PIN_NO_CHANGE,
+    .bck_io_num = I2S_BCK_IO,
+    .ws_io_num = I2S_WS_IO,
+    .data_out_num = I2S_DO_IO,
+    .data_in_num = I2S_DI_IO};
+
+// 创建AsyncWebServer对象在80端口
+AsyncWebServer server(80);
+
+// 当前播放文件状态
+File currentFile;
+bool isPlaying = false;
+
+// 初始化I2S
+void initI2S()
+{
+    esp_err_t err;
+
+    // 安装I2S驱动
+    err = i2s_driver_install(I2S_NUM, &i2s_config, 0, NULL);
+    if (err != ESP_OK)
+    {
+        Serial.printf("Failed to install i2s driver: %d\n", err);
+        return;
+    }
+
+    // 设置I2S引脚
+    err = i2s_set_pin(I2S_NUM, &pin_config);
+    if (err != ESP_OK)
+    {
+        Serial.printf("Failed to set i2s pins: %d\n", err);
+        return;
+    }
+
+    // 清除DMA缓冲区
+    err = i2s_zero_dma_buffer(I2S_NUM);
+    if (err != ESP_OK)
+    {
+        Serial.printf("Failed to clear DMA buffer: %d\n", err);
+        return;
+    }
+}
+
+// 网页HTML
+const char index_html[] PROGMEM = R"rawliteral(
+<!DOCTYPE html>
+<html>
+<head>
+    <title>ESP32音乐播放器</title>
+    <meta charset='UTF-8'>
+    <style>
+        body { font-family: Arial, sans-serif; margin: 20px; max-width: 800px; margin: 0 auto; padding: 20px; }
+        .file-item { margin: 10px 0; padding: 10px; border: 1px solid #ccc; border-radius: 4px; }
+        .controls { margin-top: 10px; }
+        button { padding: 5px 10px; margin-right: 5px; }
+        .upload-form { margin: 20px 0; padding: 10px; border: 1px solid #ccc; border-radius: 4px; }
+    </style>
+</head>
+<body>
+    <h1>ESP32音乐播放器</h1>
+    <div class="upload-form">
+        <h2>上传音频文件</h2>
+        <input type="file" id="audioFile" accept="audio/*">
+        <button onclick="uploadFile()">上传</button>
+        <div id="uploadProgress"></div>
+    </div>
+    <div id='fileList'></div>
+    <script>
+        function uploadFile() {
+            const file = document.getElementById('audioFile').files[0];
+            if (!file) {
+                alert('请选择文件');
+                return;
+            }
+
+            const formData = new FormData();
+            formData.append('file', file);
+
+            const progressDiv = document.getElementById('uploadProgress');
+            progressDiv.innerHTML = '上传中...';
+
+            fetch('/upload', {
+                method: 'POST',
+                body: formData
+            })
+            .then(response => {
+                if (!response.ok) throw new Error('上传失败');
+                progressDiv.innerHTML = '上传成功';
+                loadFiles(); // 重新加载文件列表
+            })
+            .catch(error => {
+                progressDiv.innerHTML = '上传失败: ' + error.message;
+            });
+        }
+
+        function loadFiles() {
+            fetch('/list')
+                .then(response => response.json())
+                .then(files => {
+                    const fileList = document.getElementById('fileList');
+                    fileList.innerHTML = '<h2>文件列表</h2>';
+                    if (files.length === 0) {
+                        fileList.innerHTML += '<p>没有找到音频文件</p>';
+                        return;
+                    }
+                    files.forEach(file => {
+                        const div = document.createElement('div');
+                        div.className = 'file-item';
+                        div.innerHTML = `
+                            <div>${decodeURIComponent(file)}</div>
+                            <div class="controls">
+                                <button onclick="playFile('${file}')">播放</button>
+                                <button onclick="stopPlay()">停止</button>
+                                <button onclick="deleteFile('${file}')">删除</button>
+                            </div>
+                        `;
+                        fileList.appendChild(div);
+                    });
+                })
+                .catch(error => {
+                    console.error('Error:', error);
+                    document.getElementById('fileList').innerHTML = '加载文件列表失败';
+                });
+        }
+
+        function playFile(filename) {
+            fetch('/play?file=' + encodeURIComponent(filename))
+                .then(response => response.text())
+                .then(text => alert(text))
+                .catch(error => alert('播放失败: ' + error));
+        }
+
+        function stopPlay() {
+            fetch('/stop')
+                .then(response => response.text())
+                .then(text => alert(text))
+                .catch(error => alert('停止失败: ' + error));
+        }
+
+        function deleteFile(filename) {
+            if (!confirm('确定要删除文件 ' + filename + ' 吗?')) {
+                return;
+            }
+            fetch('/delete?file=' + encodeURIComponent(filename))
+                .then(response => response.text())
+                .then(text => {
+                    alert(text);
+                    loadFiles(); // 重新加载文件列表
+                })
+                .catch(error => alert('删除失败: ' + error));
+        }
+
+        // 页面加载时获取文件列表
+        window.onload = loadFiles;
+    </script>
+</body>
+</html>
+)rawliteral";
+
+// 发送音频数据到I2S
+void playAudioData(const uint8_t *data, size_t len)
+{
+    size_t bytesWritten;
+    i2s_write(I2S_NUM, data, len, &bytesWritten, portMAX_DELAY);
+}
+
+void setup()
+{
+    Serial.begin(115200);
+    Serial.println("\n启动中...");
+
+    // 初始化SPIFFS
+    if (!SPIFFS.begin(true))
+    {
+        Serial.println("SPIFFS挂载失败");
+        return;
+    }
+    Serial.println("SPIFFS挂载成功");
+
+    // 配置WiFi
+    WiFi.mode(WIFI_STA);
+    WiFi.disconnect(true);
+    delay(1000);
+
+    // 连接WiFi
+    Serial.printf("连接到WiFi: %s\n", ssid);
+    WiFi.begin(ssid, password);
+
+    // 等待WiFi连接
+    int attempt = 0;
+    while (WiFi.status() != WL_CONNECTED && attempt < 20)
+    {
+        delay(500);
+        Serial.print(".");
+        attempt++;
+    }
+    Serial.println();
+
+    if (WiFi.status() == WL_CONNECTED)
+    {
+        Serial.println("WiFi连接成功!");
+        Serial.print("IP地址: ");
+        Serial.println(WiFi.localIP());
+        Serial.print("子网掩码: ");
+        Serial.println(WiFi.subnetMask());
+        Serial.print("网关: ");
+        Serial.println(WiFi.gatewayIP());
+        Serial.print("信号强度(RSSI): ");
+        Serial.print(WiFi.RSSI());
+        Serial.println(" dBm");
+    }
+    else
+    {
+        Serial.println("WiFi连接失败!");
+        Serial.println("重启设备...");
+        ESP.restart();
+    }
+
+    // 设置WiFi事件处理
+    WiFi.onEvent([](WiFiEvent_t event, WiFiEventInfo_t info)
+                 {
+        Serial.print("WiFi断开连接. 原因: ");
+        Serial.println(info.wifi_sta_disconnected.reason);
+        Serial.println("尝试重新连接...");
+        WiFi.begin(ssid, password); }, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED);
+
+    // 初始化I2S
+    initI2S();
+
+    // 设置Web服务器路由
+    // 主页
+    server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
+              { request->send(200, "text/html", index_html); });
+
+    // 文件列表
+    server.on("/list", HTTP_GET, [](AsyncWebServerRequest *request)
+              {
+        File root = SPIFFS.open("/");
+        String json = "[";
+        if(root) {
+            File file = root.openNextFile();
+            bool first = true;
+            while(file) {
+                if(!file.isDirectory()) {
+                    if(!first) json += ",";
+                    json += "\"" + String(file.name()) + "\"";
+                    first = false;
+                }
+                file = root.openNextFile();
+            }
+        }
+        json += "]";
+        request->send(200, "application/json", json); });
+
+    // 文件上传
+    server.on("/upload", HTTP_POST, [](AsyncWebServerRequest *request)
+              { request->send(200); }, [](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final)
+              {
+        if(!index) {
+            // 开始上传新文件
+            request->_tempFile = SPIFFS.open("/" + filename, "w");
+        }
+        if(request->_tempFile) {
+            request->_tempFile.write(data, len);
+        }
+        if(final) {
+            request->_tempFile.close();
+        } });
+
+    // 删除文件
+    server.on("/delete", HTTP_GET, [](AsyncWebServerRequest *request)
+              {
+        if(request->hasParam("file")) {
+            String filename = request->getParam("file")->value();
+            if(SPIFFS.remove("/" + filename)) {
+                request->send(200, "text/plain", "文件已删除");
+            } else {
+                request->send(500, "text/plain", "删除失败");
+            }
+        } else {
+            request->send(400, "text/plain", "缺少文件参数");
+        } });
+
+    // 播放控制
+    server.on("/play", HTTP_GET, [](AsyncWebServerRequest *request)
+              {
+        if(request->hasParam("file")) {
+            String filename = request->getParam("file")->value();
+            if(currentFile) currentFile.close();
+            
+            currentFile = SPIFFS.open("/" + filename, "r");
+            if(!currentFile) {
+                request->send(404, "text/plain", "文件不存在");
+                return;
+            }
+            
+            isPlaying = true;
+            request->send(200, "text/plain", "开始播放");
+        } else {
+            request->send(400, "text/plain", "缺少文件参数");
+        } });
+
+    // 停止播放
+    server.on("/stop", HTTP_GET, [](AsyncWebServerRequest *request)
+              {
+        if(currentFile) currentFile.close();
+        isPlaying = false;
+        request->send(200, "text/plain", "停止播放"); });
+
+    // 处理404错误
+    server.onNotFound([](AsyncWebServerRequest *request)
+                      { request->send(404, "text/plain", "找不到页面"); });
+
+    // 启动服务器
+    server.begin();
+    Serial.println("Web服务器已启动");
+}
+
+void loop()
+{
+    if (isPlaying && currentFile)
+    {
+        static uint8_t buffer[2048];
+        size_t bytesRead = currentFile.read(buffer, sizeof(buffer));
+
+        if (bytesRead > 0)
+        {
+            size_t bytesWritten = 0;
+            esp_err_t result = i2s_write(I2S_NUM, buffer, bytesRead, &bytesWritten, portMAX_DELAY);
+
+            if (result != ESP_OK)
+            {
+                Serial.printf("I2S写入错误: %d\n", result);
+                currentFile.close();
+                isPlaying = false;
+                i2s_zero_dma_buffer(I2S_NUM);
+            }
+        }
+        else
+        {
+            // 文件播放完毕
+            currentFile.close();
+            isPlaying = false;
+            i2s_zero_dma_buffer(I2S_NUM);
+            Serial.println("播放完成");
+        }
+    }
+
+    // 让出时间给其他任务
+    delay(1);
+}

+ 11 - 0
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