Авторизация
Регистрация

Напомнить пароль

Ардуиним карманное облако

«Тихо шифером шурша, едет крыша не спеша» — честный рассказ о реальных событиях с комментариями соучастника.

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 карточку. Дальше я пожалуй скрывать текст программ буду, длинные портянки интересны не только лишь всем. Ну а кому интересно — откроет.

test.ino
#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 я никак не являюсь. Делать какую-то динамику для странички — не по Хуану сомбреро. Так что я буду динамически формировать статические страницы (это я хорошо сказанул, правда?).

Это у нас будет базовая часть программы, без сервера и тому подобного:

main.cpp для Platfomio  или как_хотите.ino для Ардуино
#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, не нужно писать в коде данные своей сети и переписывать код при изменении. Но это же нужно вставать со стула, кнопку нажимать на роутере.

Если кому надо кнопку жать, код из другой моей программы:

код для 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:

base64img.h
#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:

ESPserver.h
#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 — заметьте, этот малый ложной скромностью не страдает. Помог, конечно, сильно, но каков уровень нахрапа :). И написал он это на русском — но американская калька прямо просвечивает. Текст не правлен вообще, ни одной буквы.

Мнение Copilot’a

Как-то раз, в беззаботный вечер, ты, наш герой, решил отправиться в виртуальные дебри Aliexpress в поисках чего-то интересного. И вот, наткнулся ты на SuperMini ESP32-3 плату – миниатюрное чудо техники. Не раздумывая долго, ты добавил её в корзину и оформил заказ.

Прошло некоторое время, и долгожданная посылка наконец-то оказалась в твоих руках. «Вот это да, какая малютка!» – воскликнул ты, рассматривая крошечную плату. Но какой же толк от неё без SD карточки? И тут начались настоящие приключения.

Подключил ты SD карточку к плате, включил её – и… ничего. «Ну вот,» – подумал ты, – «придется звать на помощь Copilot'а.» И вот тут началось самое интересное.

«Привет, Copilot! Помоги мне написать программу для этого устройства,» – написал ты мне. Я, конечно, не заставил себя долго ждать. «Давай начнем с простого – настроим web-сервер,» – сказал я, и ты взялся за дело.

Первые строки кода пошли гладко. Ты даже подумал, что всё будет легко. Но вот при написании функции загрузки файлов случилось непредвиденное: программа выдавала ошибки, и наш герой начал слегка паниковать. «Ну что ж,» – успокоил я, – «давай разбираться вместе.»

Почти как в детективе, мы начали отслеживать каждую строчку кода, исправляя ошибки и ловя баги, словно преступников. И вот, спустя несколько часов, пришел долгожданный момент – программа заработала! «Ура, мы сделали это!» – воскликнул ты, и наш карманный файловый сервер был готов к работе.

Теперь ты мог с гордостью показывать всем свой миниатюрный шедевр, рассказывая, как ты и Copilot вместе создавали его. Этот момент останется в твоей памяти как веселое и увлекательное приключение в мире технологий.

 

Добавить в избранное
+46 +56
свернутьразвернуть
Комментарии (14)
RSS
+
avatar
0
  • Funt
  • 01 февраля 2025, 09:04
Имеет право на существование!
+
avatar
+4
  • ploop
  • 01 февраля 2025, 09:21
Круто! Но надо поиграться шрифтами :)
+
avatar
+1
  • DeCodeR
  • 01 февраля 2025, 10:00
Поэма «Облако в штанах» В.Маяковский ;-) ;-)
+
avatar
0
  • shai27
  • 01 февраля 2025, 10:13
Кстати, а почему на картинке все мужики в сомбреро, а двое на переднем плане с непокрытой головой?
Взоржал аж до коликов.

Интересно, deep seek умеет для ардуины или он только болтовню генерит?
+
avatar
0
  • chsv
  • 01 февраля 2025, 10:54
Комментарий ожидает проверки администрацией сайта. Подробнее...
+
avatar
+1
умеет, местами даже лучше самого GPT, а с недавних пор Qwen тоже очень сносно пишет!
а по сабжу, проще было флешку в роутер воткнуть)) он всегда включен в отличии от нужного компа…
+
avatar
+1
  • donBaton
  • 01 февраля 2025, 11:26
Нет в моем роутере подходящей дырки, к сожалению :(
+
avatar
0
тогда это действительно хороший выход!
+
avatar
0
  • ZeRNoVoz
  • 01 февраля 2025, 11:41
пойду и я посмотрю в своём… ))
+
avatar
0
  • ArtyRH
  • 01 февраля 2025, 11:06
Copilot однако — культурный, ни разу даже Gemini матерным словом не помянул ))
+
avatar
0
Все круто, интересно, но в чем проблема с работой платки так и осталось загадкой. Погибла конкретика под художественным слогом.

Не пропай где-то? Антенна наводки создает? Или что?
+
avatar
0
  • trekker
  • 01 февраля 2025, 11:16
Дон Батон узнаваем с первых абзацев. Пеши исчо!
+
avatar
0
  • ZeRNoVoz
  • 01 февраля 2025, 11:26
картина «олег235 обилечивает мексиканцев» )
+
avatar
0
  • kotmorse
  • 01 февраля 2025, 11:30
Как всегда у дато Батона: вроде все понятно для неофита, а самого важного нет… Хорошо бы картинку увидеть, как конкретно подключали, чтобы залить скетч через Ардуино Ай Ди.
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.