Ардуиним карманное облако
«Тихо шифером шурша, едет крыша не спеша» — честный рассказ о реальных событиях с комментариями соучастника.
Espressif продолжает на гора выдавать все новые и новые ESP32, между которыми очень мало общего — и архитектура у них разные, и количество ядер. Но стандарты Bluetooth и Wi-Fi на месте не стоят, и приходится брать все новые и новые микросхемы, чтобы следить за модой.
Вот и я в погоне за «быстрой модой» затоварился одной из самых дешевых модняшек — ESP32-C3. Оно, конечно, не пик моды, зато бюджетненько.
Со стороны это выглядит так:
- CPU: 32-bit RISC-V single-core processor up to 160 MHz
- WiFi: 802.11b/g/n protocol, 2.4GhHz, support Station mode, SoftAP mode, SoftAP+Station mode,
- Bluetooth 5.0
- Ultra-low power consumption: deep sleep power consumption of about 43uA
- 400KB SRAM, 384KB ROM built-in 4Mflash.
- Interfaces: I2C, SPI, 2xUART, 11xGPIO(PWM), 4xADC
Будьте внимательны, микросхема должна называться ESP32C3FH4 — у другого варианта встроенного флеша нет. Как использовать ноги и чего при этом нужно остерегаться — здесь очень хорошая статья.
Для сравнения вот вам табличка — что там правда, что нет — я не в курсе, информация цельнотянутая, я ее только в картинку переобул.
Ну так вот, заказал я платку с этим мелкопроцессором на Али. Зачем — не знаю, наверно, чтоб былО.
Про эти платки я в интернетах читал — вроде как проблемы с беспроводной связью. Народ пишет, что те платы, где панелизация сделана покусанием мышами — проблем нет. А где простой рез — те не работают нормально. Кусанные продаются дороже, так что заказал я несколько резаных у одного продавца, и одну кусанную — у другого. Как водится на Али — платы от обоих пришли абсолютно одинаковые. Типа два молодца — одинаковы с лица.
Начал с ними играться — вроде выглядят рабочими. По мере усложнения программ начались проблемы — и пинг ходит через раз, и сервер не грузится. Для разборок зажал плату в металлическую держалку, и стал осциллографом и паяльником тыкать. А что тыкать-то — она стала прекрасно работать. Ну, стало доходить, где собака порылась. Проблема, скорее всего с землями — тут поможет шапочка из фольги, которую нужно носить круглосуточно. Но можно так же все замотать синей изолентой. А если серьезно, обмотать всю конструкцию липкой лентой для изоляции и поверх наклеить самоклеющуюся фольгу. Только аккуратнее пеленайте — антенна должна быть открытой, примерно как на фотографии. После этого к плате уже никаких претензий, работает, как часы.
Что бы из нее изобразить? А не сделать ли мне облако? — в конце концов стильно, модно, молодёжно. Ну, грубо выражаясь, HTTP сервер, на котором можно хранить файлы. Не то, чтобы слишком большие, а так — записать какой-нибудь файлик для фрезера, лазерного резака или принтера, пойти в мастерскую, в которую бойлерная превратилась, и там загрузить эти файлики. Сколько SD карточек я растерял при таких походах — это отдельная история. А через обычную сеть качать — тот компьютер, где лежат файлы, может и выключенным оказаться. А такое облако — пущай себе включено всегда будет, кушает оно существенно меньше одного ватта даже с учетом самого раскитайского блока питания.
HTML я последний раз использовал больше 30 лет тому назад. А в то время и трава была зеленее, и сам HTML был как-то намного проще.
Склероз же крепчает, и я мало что из того помню. Но что делать — вспоминать надо, пригодится. Правда, уже не в этой жизни — представьте себе, попали вы на небо, и надо определяться. А тут — ба! — ворота в Царствие Небесное. На входе, как водится, Святой Петр с ключами. Собеседование проводит по поводу вашей дальнейшей судьбы. И что, вы на этом собеседовании заявите, что за давностию лет вы этот HTML совсем забыли? Да вы что, совсем кукухой поехали? Ваша судьба становится совсем неопределенной и незавидной.
Так что учите матчасть, товарищи курсанты. А то потом враг поймает, пытать будет — а вам и рассказать нечего.
Гугление по программированию адской смеси Ардуино, ESP32 и HTML пошло как-то грустно — ничего конкретного, если что-то похожее находилось, то для версий библиотек, которые в последних версиях Ардуино тупо не работали. Тут вспомнился искусственный интеллект — да-да, тот самый который картинки рисует. И продвинутые журналисты суют их везде, а авторство прослеживается за километр и все это уже изрядно доставать стало (а каринка сверху чтобы всех солидарных выбесить :). Кстати, а почему на картинке все мужики в сомбреро, а двое на переднем плане с непокрытой головой?
Я раньше пытался использовать ИИ для программ связанных с железом — сплошной бред. Но вот программирование для Web — совершенно другое дело. Насколько плохо было там, настолько хорошо здесь.
На очень конкретные вопросы мелкософтовский Copilot выдает очень неплохие куски кода и более-менее по делу. Естественно, при запросе «Сделай мне красиво, и чтобы все порты и все понты» адекватного ответа ждать не приходится. Поначалу я брал эти куски и правил сам. Потом обленился, говоришь — вот тут не работает и сделать надо так. Он все переписывает, остается только скопипастить. Но внимательнее, временами глючит по-черному и может практически готовый кусок программы угробить.
Вы мне поверили? Ну-ну...
Попытка использовать гугловский Gemini ни к чему хорошему не привела. Если у Copilot’a программы с ошибкам, то у Gemini — сплошная ошибка. Но зато в этой куче мусора иногда встречаются блестящие идеи — у кого он их спер, интересно?
Это все было присказка, а теперь и сама сказочка.
Хорошая новость — для отладки не нужен никакой программатор. Совсем. У контроллера есть USB, который умеет работать как отладчик JTAG или виртуальный последовательный порт. Никаких HID и прочего. Это значит, в минимальной комплектации нужен только сам контроллер, кварц и USB разъем. Питание тоже не помешает.
Ардуино знает этот контроллер, как облупленный. Из длинного списка доступных плат с ESP32-C3 выбирайте строго первый попавшийся. То что на этой плате еще что-то наворочено, нам фиолетово — будем использовать только то, что имеем.
Для начала, как водится, светодиодиком поморгаем:
#define LED 8
#define toggle(b) digitalWrite((b), (digitalRead((b)) == HIGH ? LOW : HIGH))
void setup()
{
pinMode(LED, OUTPUT);
}
void loop()
{
toggle(LED);
delay(500);
}
Моргает. Хорошо моргает, качественно!
Добавим последовательный интерфейс:
#define LED 8
#define toggle(b) digitalWrite((b), (digitalRead((b)) == HIGH ? LOW : HIGH))
void setup()
{
pinMode(LED, OUTPUT);
Serial.begin(115200);
Serial.println("SDweb test");
}
void loop()
{
toggle(LED);
Serial.write('*');
delay(500);
}
Гонит байтики за милый мой через тот же USB, через который загрузился.
Цепляем к платке SD карточку. Дальше я пожалуй скрывать текст программ буду, длинные портянки интересны не только лишь всем. Ну а кому интересно — откроет.
#define SD_CS 7
#define SD_MOSI 6
#define SD_MISO 5
#define SD_SCK 4
#define LED 8
#define toggle(b) digitalWrite((b), (digitalRead((b)) == HIGH ? LOW : HIGH))
void listDir(fs::FS &fs, const char *dirname, uint8_t levels)
{
Serial.printf("Listing directory: %s\n", dirname);
File root = fs.open(dirname);
if (!root)
{
Serial.println("Failed to open directory");
return;
}
if (!root.isDirectory())
{
Serial.println("Not a directory");
return;
}
File file = root.openNextFile();
while (file)
{
if (file.isDirectory())
{
Serial.print(" DIR : ");
Serial.println(file.name());
if (levels) listDir(fs, file.path(), levels - 1);
}
else
{
Serial.print(" FILE: ");
Serial.print(file.name());
Serial.print(" SIZE: ");
Serial.println(file.size());
}
file = root.openNextFile();
}
}
void setup()
{
pinMode(LED, OUTPUT);
Serial.begin(115200);
Serial.println("SDweb test");
SPI.begin(SD_SCK, SD_MISO, SD_MOSI, SD_CS);
if (!SD.begin(SD_CS))
{
Serial.println("Card Mount Failed");
return false;
}
uint8_t cardType = SD.cardType();
if (cardType == CARD_NONE)
{
Serial.println("No SD card attached");
return false;
}
Serial.print("SD Card Type: ");
if (cardType == CARD_MMC) Serial.println("MMC");
else if (cardType == CARD_SD) Serial.println("SDSC");
else if (cardType == CARD_SDHC) Serial.println("SDHC");
else Serial.println("UNKNOWN");
uint64_t cardSize = SD.cardSize() / (1024 * 1024);
Serial.printf("SD Card Size: %lu MB", cardSize);
Serial.println();
listDir(SD, "/", 0);
}
void loop()
{
toggle(LED);
Serial.write('*');
delay(500);
}
Работает, а куда ему деться с подводной лодки?
Как я говорил, знатоком программирования Web я никак не являюсь. Делать какую-то динамику для странички — не по Хуану сомбреро. Так что я буду динамически формировать статические страницы (это я хорошо сказанул, правда?).
Это у нас будет базовая часть программы, без сервера и тому подобного:
#include <Arduino.h>
#include <WiFi.h>
#include <ESPmDNS.h>
#include "FS.h"
#include "SD.h"
#include "SPI.h"
#include <WebServer.h>
#include "esp_system.h"
//original name esp32c3-9D4DA4
#define SD_CS 7
#define SD_MOSI 6
#define SD_MISO 5
#define SD_SCK 4
#define LED 8
#define toggle(b) digitalWrite((b), (digitalRead((b)) == HIGH ? LOW : HIGH))
// ******************************************************************************
#include "ESPserver.h"
// ******************************************************************************
void listDir(fs::FS &fs, const char *dirname, uint8_t levels)
{
Serial.printf("Listing directory: %s\n", dirname);
File root = fs.open(dirname);
if (!root)
{
Serial.println("Failed to open directory");
return;
}
if (!root.isDirectory())
{
Serial.println("Not a directory");
return;
}
File file = root.openNextFile();
while (file)
{
if (file.isDirectory())
{
Serial.print(" DIR : ");
Serial.println(file.name());
if (levels) listDir(fs, file.path(), levels - 1);
}
else
{
Serial.print(" FILE: ");
Serial.print(file.name());
Serial.print(" SIZE: ");
Serial.println(file.size());
}
file = root.openNextFile();
}
}
bool wifi_setup(void)
{
WiFi.mode(WIFI_STA);
WiFi.begin("your_net", "your_pass"); // do wrong connection for test
delay(10);
Serial.println();
Serial.println();
Serial.print("Waiting for WiFi... ");
uint8_t i = 0;
while ((WiFi.status() != WL_CONNECTED) && (i++ < 60)) //wait 30 seconds
{
Serial.print(".");
delay(500);
}
if (i > 60)
{
Serial.print("\nCould not connect wifi");
return false;
}
Serial.println("\nWiFi connected");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
Serial.print("ESP32C3 HostName: ");
Serial.println(WiFi.getHostname());
Serial.print("RSSI: ");
Serial.println(WiFi.RSSI());
delay(500);
if(!MDNS.begin("esp32c3"))
{
Serial.println("Error starting mDNS");
return false;
}
// now you cat try: ping esp32c3.local
return true;
}
bool sd_setup(void)
{
SPI.begin(SD_SCK, SD_MISO, SD_MOSI, SD_CS);
if (!SD.begin(SD_CS))
{
Serial.println("Card Mount Failed");
return false;
}
uint8_t cardType = SD.cardType();
if (cardType == CARD_NONE)
{
Serial.println("No SD card attached");
return false;
}
Serial.print("SD Card Type: ");
if (cardType == CARD_MMC) Serial.println("MMC");
else if (cardType == CARD_SD) Serial.println("SDSC");
else if (cardType == CARD_SDHC) Serial.println("SDHC");
else Serial.println("UNKNOWN");
uint64_t cardSize = SD.cardSize() / (1024 * 1024);
Serial.printf("SD Card Size: %lu MB", cardSize);
Serial.println();
//listDir(SD, "/", 0);
return true;
}
void setup()
{
Serial.begin(115200);
pinMode(LED, OUTPUT);
Serial.println("SDweb test");
if(!wifi_setup()) ESP.restart();
if (!sd_setup()) ESP.restart();
server_setup();
}
uint16_t period = 500;
uint32_t time_now = 0;
void loop()
{
if(millis() >= time_now + period)
{
time_now += period;
toggle(LED);
//Serial.write('*');
}
yield();
server.handleClient();
}
Подключаемся к сети. Присваиваем серверу имя, чтобы его в локальной сети по имени звать, а не соображать каждый раз, какой у него IP адрес. Можно несколько строк добавить и сервер будет подключаться используя WPS, не нужно писать в коде данные своей сети и переписывать код при изменении. Но это же нужно вставать со стула, кнопку нажимать на роутере.
Если кому надо кнопку жать, код из другой моей программы:
#include <esp_wps.h>
…
WiFi.onEvent(WiFiEvent);
WiFi.mode(WIFI_MODE_STA);
Serial.println("Starting WPS");
wpsInitConfig();
esp_wifi_wps_enable(&config);
esp_wifi_wps_start(0);
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
Serial.print(".");
}
Serial.println();
На страничке хотелось бы иметь картинки. А как из иметь, когда страничка формируется из ничего, и там никаких файлов и тем более картинок нет? Да так же, как с начала времен, как картинки пересылали почтой, где все символы вообще были 7-битные — используем кодировку base64:
#ifndef _base64_img_h
#define _base64_img_h
const char* folderIco=
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABrElEQVR42mL4//8/RZghJ0AzPydI+0dOkM5/ZJwdq"
"P0uy187hKABQIWfAJXQA28mQRzH8XkrF5xt+8Kz7dq2bduPbbu23b6OBtWvO1sF1Sb5LOc7/2TU/DLoxNXQCCqg4pVC0VoEflUqgj9cXPJ7eeL8"
"gRsw01eUvBLkxf7cURL3CeLUpxAn3TlQfeRtJwl+e3aFX522ExfGfUFb0hMMOYVYnOrf1+yIG21xt0HS/lxebYp7iMaomxQTP0O/tRlTgw50mnn"
"7WpjsYdeTyvBbGOlQYGGiizU/1oGRTi06TS3oMDbva2bEheqw6yBlQdfYaS5NHSdjPSbQlhT4XcFolx52RSUng24laEuyvS6h3yGFWVzMSZeZB9"
"oyh3gBHYZWGHh5nLhUtaAtSfh+HnZFFTTNmZyYhEWgLYn+fBZGQRGUDalQNaRtatyWvvvcsC2VXatuygBtSfins2uKhgzo+IUwCIphFJXBLKmAR"
"VYNm6IWVnkNLNJKmMTlMAhLoGeGaXkF4JVEgrYk9MMFd17Ym3VpbTK4oE3Yh/MeEvjixLHg9+dH/V6dBBe0oe0GfTWz8NJPx3wAAAAASUVORK5CYII=";
const char* backIco=
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAABEVBMVEX///8AAAAzmQAzgABNmQBGiwBAgAA3bQAzdw"
"A8dwBLngg6dQdJigc7dgA8cwSI4S9IkwdFgwc8dQNNmgU5dQU+egVBggVJkgVLlwVOmgU6dwRBggQ5dQSI3zE6dAREhQZNmQY7dgU6dASH4DBIjA"
"VMlwZBgQU6dARJkgY7dgROmgY8dwRCggc6dAQ8dwU+ewSG3zA7dwVIjQdIhg1OjRNUkxdhoCNlpihoqSlAfQVEhgVHiAhKjQtKkgZRkxFSmBBTlx"
"NapBNnqChsqzFuyBhvrDNxxx9ztTRzwyR3ujV4tj54zSSHvlKJ1T6V1lah1m+j3WukzX6y1JK55Y274JjA8JTB8JXU9bXf8czt9uTv9ug/wVbZAA"
"AAOXRSTlMAAQUKCgsMDg8eIiMjJzw8R0pZXWJiYmJiYnZ2eHiIiJ6gtLS9xcvNzdnf6urw8PDw8fH+/v7+/v4P8x4IAAAAiElEQVR42o3KQwLDUA"
"AFwFfbtm3j17Z9/3vUwS6Z9YAXU9IPNkf9MVSCYa/dDxEw9NXbvv0Xt4mB8Ol6+Ttvx0U1DJXjqvlDBpupD7CWd4uoEG9SV2edBmApzRsKfAhyyx"
"bejIkAPiROMkrhRw6tO5jtTvpe/Gig84Tys15BhR/ZZ2RiZhG4vQAC5hXojBngRAAAAABJRU5ErkJggg==";
const char* delIco=
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAADBUlEQVR42mRTA49sSRSu0bONNp5t27Zt2zZGP2VtRT"
"PB2J7btrtvuzvfnlvrfUm+pI6+OmQA/gPXuHEar1b7xKXR/GJXKHxWmcxnGTPmV+OIEU+7hw/X/9//P4J33LgrgalTRO+WjRnP6WPwPLkHz+O7cJ"
"08AtvaVVlh9Kho55Ahtygs5xMCh1r9vXfObNFz9iQ8+3bDtXwpnFOnwEFwLlsC195dRHQUhnF6sa1//9/+IuHBNpnsimP6VNF9+ACc8+bAu2sXxO"
"++RcJilsDf7j17YJ81A84De9GhlItNvXvf5ASmkSM1Zpks6t6+BZ7ZsxD68B7JeBzpdAqZTFoCfycTCQSKi+CYNhW2zRvQ1KdPtLFnTy0Thg9/Yp"
"8+Le1ZuQK+3buRiMXw4eM7vHn7GuFwGMFgEI+fPERxaRESROzeuxfOpUvQOU6Xrs3Pf8O6hg79xb5gHpwzZyD8zddIJOK4e/8O9u7fjdt3buDm7e"
"v8/fL1c24L//gDrNQXA/kTQRlrHzjQ55AaNm4cRIOAeDwGl8uFq9cu80AJN25dg8fj4TbRZoVVrYZ5ySJU5+SEWUu/fj7bksWw6XSICF2IRkUicO"
"L6zat/E9yiLDweN7dFrGaYVSqYKGtO0Ny7968mSsk2aSL8X3yOSCSMp88e88BrN65QJpf4+/HTR9zm/+4bmMbp0TVxAmpycspZXX7+0065PGOdMw"
"u27dsRCvrx4tVz3HtwBzZK10JjfPTkAd69f41QKADb7l0wz5yO1tGjM9WMvWW1jOlrCgqipnlzYdZp4Xj9CgG/F4GAnybAIb0JPjhoxEaNGoa5c1"
"CdlydWMjaeL1INY7fqBw4QTYsXwqDVwLx1C9xffg6f0MXh/uoLmHbsQLeaal+0AJJvFWP3/1llWktqyG8NgwaJAjkYZs+EQNl0jx6NLkI3vQ20hd"
"3UuHryodrLKCb3v8dEJJTSzaq8vGirUpHumD4VwvIlHNK7VSHPSGnzn3nwp9fIQQ5aas6bKvqF3mEJlF251DCp5t+Hrh4ATJJ9aPC+HBoAAAAASU"
"VORK5CYII=";
const char* saveIco=
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAA81BMVEX///9LnAZPmgROnARPmgdOmwdOmgZOmgVOmg"
"dOmgZOmgZOmgZOmgZOmgZOmQbu8Or8/fpOmgZQnQhOmgZOmgYuNDYzQy9BUD1HghJOmgZYWlZbqhFvcW1vsi1vwiCA0jGB1y2ChYaGiYOIioWK4T"
"SK4jSNj4uQko2Q4z+Y2Fua21qdoJqg51qkpqGk6GGm6WSr6W2t4Hy1uLG454y5ure6vba66I6/wbvK8aPNzsrQ0c7Q867T1NDh4d/i4+Dj5uLk5e"
"Ll5uTp6+fr7Ort7ezv8O3v8O/x8vHy8vD09fP19vT29/X5+vj7+/r8/Pz+/v7///+MlcBIAAAAFXRSTlMALDo7R3V8vcLFzM3P0djc3fT19/iYBl"
"hgAAAApUlEQVR42k3OA3rEQBiA4b/2bmyjNmPWbXD/y8R6x9+jgR2UXUB3AF9/98h2W+PA5nonJ/MWCwRSLiAE7OJsyzW7A9+FXhtgsnG434bVwe"
"YQjh+eRNcUXy/2hnByeWe55v2tfDSEbUw6vzYNCduCzptKM7yi8AxFvnTBK/9+s+L/5yMJH7tw8xn5zZ/e7SC+6oKWBs7zmX5qB6nWBeFrInSBWw"
"CoAfNjITzEl/1PAAAAAElFTkSuQmCC";
#endif
Ну и сама программа, которая формирует эту страничку и обрабатывает запросы, что делать с файлами на SD:
#include"base64img.h"
WebServer server(80);
File uploadFile;
String HTML_Header()
{
String html_page;
html_page = "<!DOCTYPE html>\n";
html_page += "<head>\n";
html_page += "<title>Cloud Web Server</title>\n";
html_page += "<style>\n";
html_page += ".topnav {overflow: hidden;background-color:lightcyan;}\n";
html_page += ".topnav a {float:center;color:blue;text-align:center;padding:0.6em 0.6em;text-decoration:none;font-size:1em;}\n";
html_page += ".topnav a:hover {background-color:deepskyblue;color:white;}\n";
html_page += ".topnav a.active {background-color:lightblue;color:blue;}\n";
html_page += ".folder-image {content: ";
html_page += "url('"+ String(folderIco) + "');}\n";
html_page += ".back-image {content: ";
html_page += "url('"+ String(backIco) + "');}\n";
html_page += ".delete-image {content: ";
html_page += "url('"+ String(delIco) + "');}\n";
html_page += ".save-image {content: ";
html_page += "url('"+ String(saveIco) + "');}\n";
html_page += ".tooltip {position: relative; display: inline-block; border-bottom: 1px dotted black;}";
html_page += ".tooltip .tooltiptext {visibility: hidden; width: 100px; background-color: yellow; color: black;";
html_page += "text-align: center; border-radius: 6px; padding: 1px 0; position: absolute; z-index: 1; top: -25px;}";
html_page += ".tooltip:hover .tooltiptext {visibility: visible;}";
html_page += "table.styled-table { width: 400px; border-collapse: collapse;}";
html_page += "th, td { padding: 8px;}";
// First four rows with white background
html_page += "table.styled-table tr:nth-child(-n+4) {background-color: white;}";
// Alternate row colors
html_page += "table.styled-table tr:nth-child(n+5):nth-child(odd) { background-color: #f2f2f2;}";
html_page += "table.styled-table tr:nth-child(n+5):nth-child(even) { background-color: #ffffff;}";
html_page += "</style>\n";
html_page += "</head>\n";
html_page += "<body>\n";
html_page += "<div class = 'topnav'>\n";
html_page += "<a href='/'>Home</a>\n";
html_page += "<a href='/system'>Status</a>\n";
html_page += "</div>\n\n\n";
return html_page;
}
void systemInfo()
{
String html_page = HTML_Header();
html_page += "<h3 align='center'>System Information</h3>\n";
html_page += "<table align='center' style='width:400px'>\n";
html_page += "<tr><th colspan='2'>CPU Information</th></tr>\n";
html_page += "<tr><th>Parameter</th><th style='width:8em'>Value</th></tr>\n";
html_page += "<tr><td>Chip Model:</td><td>" + String(ESP.getChipModel()) + "</td></tr>\n";
html_page += "<tr><td>Chip Revision:</td><td>"+ String(ESP.getChipRevision()) + "</td></tr>\n";
html_page += "<tr><td>CPU Frequency</td><td>" + String(ESP.getCpuFreqMHz()) + " MHz</td></tr>\n";
html_page += "<tr><td>Flash Memory Size</td><td>" + String((ESP.getFlashChipSize() / (1024 * 1024))) + " MB</td></tr>\n";
html_page += "<tr><td></td><td></td></tr>\n";
html_page += "<tr><td></td><td></td></tr>\n";
html_page += "<tr><th colspan='2'>SD Card Information</th></tr>\n";
html_page += "<tr><th>Parameter</th><th>Value</th></tr>\n";
uint64_t cardSize = SD.cardSize();
uint64_t usedSpace = SD.usedBytes();
uint64_t freeSpace = cardSize - usedSpace;
cardSize /= 1024 * 1024;
freeSpace /= 1024 * 1024;
html_page += "<tr><td>Total Size</td><td>" + String(cardSize) + " MB</td></tr>\n";
html_page += "<tr><td>Free Space</td><td>" + String(freeSpace) + " MB</td></tr>\n";
const char* CardTypeID[] = {"None", "MMC", "SDSC", "SDHC", "UNKNOWN"};
uint8_t CardType = SD.cardType();
String CardTypeName;
if (CardType>4) CardType=5;
CardTypeName = CardTypeID[CardType];
html_page += "<tr><td>CS Type</td><td>" + CardTypeName + "</td></tr>\n";
html_page += "<tr><td></td><td></td></tr>\n";
html_page += "<tr><td></td><td></td></tr>\n";
html_page += "<tr><th colspan='2'>Network Information</th></tr>\n";
html_page += "<tr><th>Parameter</th><th>Value</th></tr>\n";
html_page += "<tr><td>LAN IP Address</td><td>" + String(WiFi.localIP().toString()) + "</td></tr>\n";
html_page += "<tr><td>Network Adapter MAC Address</td><td>" + String(WiFi.BSSIDstr()) + "</td></tr>\n";
html_page += "<tr><td>WiFi SSID</td><td>" + String(WiFi.SSID()) + "</td></tr>\n";
html_page += "<tr><td>WiFi RSSI</td><td>" + String(WiFi.RSSI()) + " dB</td></tr>\n";
html_page += "<tr><td>WiFi Channel</td><td>" + String(WiFi.channel()) + "</td></tr>\n";
html_page += "</table>\n";
server.send(200, "text/html", html_page);
}
// Function to generate the HTML for the file table
String generateFileTable(const String& currentPath)
{
String html = "<table style='width:480px' class='styled-table'>\n";
html += "<tr><th style='width:20px'> </th><th> </th><th style='width:65px'> </th><th style='width:30px'> </th><th style='width:30px'></th></tr>\n";
html += "<tr> <td colspan='5'> <b> SD Card Files </b> </td> </tr>\n";
html += "<tr> <td align='left' colspan='5'> <font color='blue'> " + currentPath + "</font> </td></tr>\n";
html += "<tr><td></td><td>File Name</td><td align='center'>Size</td><td colspan='2' align='center'> Action </td</tr>\n";
File root = SD.open(currentPath);
if (!root)
{
html += "<tr><td colspan='5'>Error opening directory</td></tr></table>\n";
return html;
}
File file;
while (true)
{
file = root.openNextFile();
if (!file) break;
String filename = String(file.name());
if (file.isDirectory())
{
html += "<tr><td align='center'><img class='folder-image' alt='folder'></td>";
html += "<td><a href='?path=" + currentPath + "/" + filename + "'>" + filename + "/</a></td>";
html += "<td></td>";
//html += "<td> <div class='tooltip'> <a href='?download=" + currentPath + "/" + filename + "'>";
//html += "<img class='save-image' alt='Download'></a><span class='tooltiptext'>Download</span></div></td>";
//html += "<td> <div class='tooltip'> <a href='?delete=" + currentPath + "/" + filename + "'>";
//html += "<img class='delete-image' alt='Delete'></a><span class='tooltiptext'>Delete</span></div></td></tr>\n";
}
else
{
String DelimSize = "";
String strNum = String(file.size());
int length = strNum.length();
for (uint8_t i = 0; i < length; i++)
{
DelimSize += strNum[i];
if ((length - i - 1) % 3 == 0 && i != length - 1) DelimSize += ' ';
}
html += "<tr><td></td><td>" + filename + "</td>";
html += "<td align='right'>" + DelimSize + "</td>";
//html += "<td> <div class='tooltip'> <a href='?download=" + currentPath + "/" + filename + "'>";
//html += "<img class='save-image' alt='Download'></a><span class='tooltiptext'>Download</span></div></td>";
//html += "<td> <div class='tooltip'> <a href='?delete=" + currentPath + "/" + filename + "'>";
//html += "<img class='delete-image' alt='Delete'></a><span class='tooltiptext'>Delete</span></div></td></tr>\n";
}
html += "<td align='center'> <div class='tooltip'> <a href='?download=" + currentPath + "/" + filename + "'>";
html += "<img class='save-image' alt='Download'></a><span class='tooltiptext'>Download</span></div></td>";
html += "<td align='center'> <div class='tooltip'> <a href='?delete=" + currentPath + "/" + filename + "'>";
html += "<img class='delete-image' alt='Delete'></a><span class='tooltiptext'>Delete</span></div></td></tr>\n";
file.close();
}
root.close();
// Add a link to go back to the parent directory
if (currentPath != "/")
{
String parentPath = currentPath.substring(0, currentPath.lastIndexOf('/'));
html += "<tr><td> <a href='?path=" + parentPath + "'><img class='back-image' alt='back'></a></td><td></td><td></td></tr>\n";
}
uint64_t cardSize = SD.cardSize();
uint64_t usedSpace = SD.usedBytes();
uint64_t freeSpace = cardSize - usedSpace;
cardSize /= 1024 * 1024;
freeSpace /= 1024 * 1024;
html += "<tr><td colspan='5'></td></tr>\n";
html += "<tr><td colspan='5'>Total Size " + String(cardSize) + " MB</td></tr>\n";
html += "<tr><td colspan='5'>Free Space " + String(freeSpace) + " MB</td></tr>\n";
html += "</table>\n";
return html;
}
bool deleteFolder(const String &path)
{
File dir = SD.open(path);
if (!dir)
{
Serial.println("Failed to open directory");
return false;
}
if (!dir.isDirectory())
{
Serial.println("Not a directory");
return false;
}
File file = dir.openNextFile();
while (file)
{
if (file.isDirectory())
{
// Recursively delete subdirectory
if (!deleteFolder(path + "/" + file.name())) return false;
}
else
{
// Delete the file
Serial.print("Deleting file: ");
Serial.println(path + "/" + file.name());
if (!SD.remove(path + "/" + file.name())) return false;
}
file = dir.openNextFile();
}
// Close the directory
dir.close();
// Remove the empty directory
Serial.print("Deleting directory: ");
Serial.println(path);
if (!SD.rmdir(path)) return false;
return true;
}
// Handle HTTP GET requests
void handleRoot()
{
String currentPath = "/";
if (server.hasArg("path")) currentPath = server.arg("path");
String html_page = HTML_Header();
// **************************************
// forms
// **************************************
html_page += "<form method='POST' action='/mkdir'>";
html_page += "<input type='hidden' name='path' value='" + currentPath + "'>";
html_page += "New Folder: <input type='text' name='folder'>";
html_page += "<input type='submit' value='Create'>";
html_page += "</form>";
// **************************************
html_page += "<form method='POST' action='/upload' enctype='multipart/form-data'>";
html_page += "<input type='hidden' name='path' value='" + currentPath + "'>";
html_page += "<input type='file' name='upload'>";
html_page += "<input type='submit' value='Upload'></form>";
// **************************************
// Handle file deletion
if (server.hasArg("delete"))
{
String filePath = server.arg("delete");
bool folder=false;
File file = SD.open(filePath);
if(file.isDirectory()) folder=true;
file.close();
if(folder)
{
if (deleteFolder(filePath.c_str())) currentPath = filePath.substring(0, filePath.lastIndexOf('/')); // Go back to parent directory after deletion
else server.send(500, "text/plain", "500: Couldn't delete folder: " + currentPath);
}
else
{
if (SD.remove(filePath))
{
// File deleted successfully
Serial.print("Delete ");
Serial.println(server.arg("download")) ;
currentPath = filePath.substring(0, filePath.lastIndexOf('/')); // Go back to parent directory after deletion
}
else server.send(500, "text/plain", "500: Couldn't delete file: " + currentPath);
}
}
// **********************************************************************************
if (server.hasArg("download"))
{
String path = server.arg("download");
if (SD.exists(path))
{
File downloadFile = SD.open(path, FILE_READ);
server.sendHeader("Content-Type", "application/octet-stream");
server.sendHeader("Content-Disposition", "attachment; filename=\"" + String(downloadFile.name()) + "\"");
server.sendHeader("Connection", "close");
server.streamFile(downloadFile, "application/octet-stream");
downloadFile.close();
}
else server.send(404, "text/plain", "404: File Not Found");
}
// ***********************************************************************************
html_page += generateFileTable(currentPath);
html_page += "</body></html>\n";
server.send(200, "text/html", html_page);
}
// upload file
void handleFileUpload()
{
HTTPUpload& upload = server.upload();
String path = server.arg("path");
String fileName = upload.filename;
String fullPath = path +"/"+ fileName;
String pageRequest;
if (path=="/") pageRequest = "/";
else pageRequest = "/?path=" + path;
if (upload.status == UPLOAD_FILE_START)
{
String filename = "/" + fullPath;
uploadFile = SD.open(filename, FILE_WRITE);
if (!uploadFile)
{
server.send(500, "text/plain", "500: Couldn't create file");
return;
}
}
else if (upload.status == UPLOAD_FILE_WRITE)
{
if (uploadFile) uploadFile.write(upload.buf, upload.currentSize);
else
{
server.send(500, "text/plain", "500: Couldn't write file");
return;
}
}
else if (upload.status == UPLOAD_FILE_END)
{
if (uploadFile)
{
uploadFile.close();
String html = "<html><body><h1>File Uplader</h1>";
html += "<p>File " + fileName + " Uploaded Successfully</p>";
html += "<script>setTimeout(function(){ window.location = '" + pageRequest + "'; }, 1500);</script>";
html += "<p><a href='/'>Go Back</a></p></body></html>";
server.send(200, "text/html", html);
}
else server.send(500, "text/plain", "500: Couldn't write file");
}
}
// make new folder
void handleMakeDir()
{
String path = server.arg("path");
String folderName = server.arg("folder");
String fullPath = path +"/"+ folderName;
String pageRequest;
if (path=="/") pageRequest = "/";
else pageRequest = "/?path=" + path;
if (SD.mkdir(fullPath))
{
String html = "<html><body><h1>Folder Creator</h1>";
html += "<p>Folder Created Successfully at: " + fullPath + "</p>";
html += "<script>setTimeout(function(){ window.location = '" + pageRequest + "'; }, 1500);</script>";
html += "<p><a href='/'>Go Back</a></p></body></html>";
server.send(200, "text/html", html);
}
else server.send(500, "text/plain", "500: Couldn't create folder at: " + fullPath);
}
void server_setup()
{
server.on("/", HTTP_GET, handleRoot);
//server.on("/", HTTP_POST, handleFileUpload);
server.on("/system", systemInfo);
server.on("/mkdir", HTTP_POST, handleMakeDir);
server.on("/upload", HTTP_POST, []() {server.send(200);}, handleFileUpload);
server.begin();
Serial.println("HTTP server started");
}
Что получилось в итоге с точки зрения Большого Брата? Да вот:
Сервер, конечно, игрушечный — фильмы им вряд ли перекачаешь. Куча ограничений — можно скачивать туда-сюда только один файл за раз. Зато убивать их можно папками со множеством вложений. Одновременно несколько компьютеров качать тоже не смогут, типа, в очередь, дети самки собаки. Подключить его как сетевой диск тоже не получится. Тем не менее — игрушка хорошая и образовательная. Я ей пользуюсь буквально с первого дня, как она задышала.
А вот теперь этот же рассказ с точки зрения Copilot’a — заметьте, этот малый ложной скромностью не страдает. Помог, конечно, сильно, но каков уровень нахрапа :). И написал он это на русском — но американская калька прямо просвечивает. Текст не правлен вообще, ни одной буквы.
Как-то раз, в беззаботный вечер, ты, наш герой, решил отправиться в виртуальные дебри Aliexpress в поисках чего-то интересного. И вот, наткнулся ты на SuperMini ESP32-3 плату – миниатюрное чудо техники. Не раздумывая долго, ты добавил её в корзину и оформил заказ.
Прошло некоторое время, и долгожданная посылка наконец-то оказалась в твоих руках. «Вот это да, какая малютка!» – воскликнул ты, рассматривая крошечную плату. Но какой же толк от неё без SD карточки? И тут начались настоящие приключения.
Подключил ты SD карточку к плате, включил её – и… ничего. «Ну вот,» – подумал ты, – «придется звать на помощь Copilot'а.» И вот тут началось самое интересное.
«Привет, Copilot! Помоги мне написать программу для этого устройства,» – написал ты мне. Я, конечно, не заставил себя долго ждать. «Давай начнем с простого – настроим web-сервер,» – сказал я, и ты взялся за дело.
Первые строки кода пошли гладко. Ты даже подумал, что всё будет легко. Но вот при написании функции загрузки файлов случилось непредвиденное: программа выдавала ошибки, и наш герой начал слегка паниковать. «Ну что ж,» – успокоил я, – «давай разбираться вместе.»
Почти как в детективе, мы начали отслеживать каждую строчку кода, исправляя ошибки и ловя баги, словно преступников. И вот, спустя несколько часов, пришел долгожданный момент – программа заработала! «Ура, мы сделали это!» – воскликнул ты, и наш карманный файловый сервер был готов к работе.
Теперь ты мог с гордостью показывать всем свой миниатюрный шедевр, рассказывая, как ты и Copilot вместе создавали его. Этот момент останется в твоей памяти как веселое и увлекательное приключение в мире технологий.
+159 |
3908
93
|
Интересно, deep seek умеет для ардуины или он только болтовню генерит?
а по сабжу, проще было флешку в роутер воткнуть)) он всегда включен в отличии от нужного компа…
Не пропай где-то? Антенна наводки создает? Или что?