Jan 12, 2022 - version A1
- update for esp32-arduino 2.02 - replace ftp file transfer with http file transfer directly from browser - obscure features deletedmaster
rodzic
7fd2544bfc
commit
1d928bb214
|
@ -0,0 +1,59 @@
|
|||
// mods by James Zahary Dec 28, 2021 https://github.com/jameszah/ESPxWebFlMgr
|
||||
// based on https://github.com/holgerlembke/ESPxWebFlMgr
|
||||
|
||||
//
|
||||
// Copyright (c) 2013 Christopher Baker <https://christopherbaker.net>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
|
||||
#include "CRC32.h"
|
||||
|
||||
// Conditionally use pgm memory if it is available.
|
||||
|
||||
#if defined(PROGMEM)
|
||||
#define FLASH_PROGMEM PROGMEM
|
||||
#define FLASH_READ_DWORD(x) (pgm_read_dword_near(x))
|
||||
#else
|
||||
#define FLASH_PROGMEM
|
||||
#define FLASH_READ_DWORD(x) (*(uint32_t*)(x))
|
||||
#endif
|
||||
|
||||
|
||||
static const uint32_t crc32_table[] FLASH_PROGMEM = {
|
||||
0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac,
|
||||
0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c,
|
||||
0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c,
|
||||
0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c
|
||||
};
|
||||
|
||||
|
||||
CRC32::CRC32()
|
||||
{
|
||||
reset();
|
||||
}
|
||||
|
||||
|
||||
void CRC32::reset()
|
||||
{
|
||||
_state = ~0L;
|
||||
}
|
||||
|
||||
|
||||
void CRC32::update(const uint8_t& data)
|
||||
{
|
||||
// via http://forum.arduino.cc/index.php?topic=91179.0
|
||||
uint8_t tbl_idx = 0;
|
||||
|
||||
tbl_idx = _state ^ (data >> (0 * 4));
|
||||
_state = FLASH_READ_DWORD(crc32_table + (tbl_idx & 0x0f)) ^ (_state >> 4);
|
||||
tbl_idx = _state ^ (data >> (1 * 4));
|
||||
_state = FLASH_READ_DWORD(crc32_table + (tbl_idx & 0x0f)) ^ (_state >> 4);
|
||||
}
|
||||
|
||||
|
||||
uint32_t CRC32::finalize() const
|
||||
{
|
||||
return ~_state;
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
// mods by James Zahary Dec 28, 2021 https://github.com/jameszah/ESPxWebFlMgr
|
||||
// based on https://github.com/holgerlembke/ESPxWebFlMgr
|
||||
|
||||
//
|
||||
// Copyright (c) 2013 Christopher Baker <https://christopherbaker.net>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
#include "Arduino.h"
|
||||
|
||||
|
||||
/// \brief A class for calculating the CRC32 checksum from arbitrary data.
|
||||
/// \sa http://forum.arduino.cc/index.php?topic=91179.0
|
||||
class CRC32
|
||||
{
|
||||
public:
|
||||
/// \brief Initialize an empty CRC32 checksum.
|
||||
CRC32();
|
||||
|
||||
/// \brief Reset the checksum claculation.
|
||||
void reset();
|
||||
|
||||
/// \brief Update the current checksum caclulation with the given data.
|
||||
/// \param data The data to add to the checksum.
|
||||
void update(const uint8_t& data);
|
||||
|
||||
/// \brief Update the current checksum caclulation with the given data.
|
||||
/// \tparam Type The data type to read.
|
||||
/// \param data The data to add to the checksum.
|
||||
template <typename Type>
|
||||
void update(const Type& data)
|
||||
{
|
||||
update(&data, 1);
|
||||
}
|
||||
|
||||
/// \brief Update the current checksum caclulation with the given data.
|
||||
/// \tparam Type The data type to read.
|
||||
/// \param data The array to add to the checksum.
|
||||
/// \param size Size of the array to add.
|
||||
template <typename Type>
|
||||
void update(const Type* data, size_t size)
|
||||
{
|
||||
size_t nBytes = size * sizeof(Type);
|
||||
const uint8_t* pData = (const uint8_t*)data;
|
||||
|
||||
for (size_t i = 0; i < nBytes; i++)
|
||||
{
|
||||
update(pData[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/// \returns the caclulated checksum.
|
||||
uint32_t finalize() const;
|
||||
|
||||
/// \brief Calculate the checksum of an arbitrary data array.
|
||||
/// \tparam Type The data type to read.
|
||||
/// \param data A pointer to the data to add to the checksum.
|
||||
/// \param size The size of the data to add to the checksum.
|
||||
/// \returns the calculated checksum.
|
||||
template <typename Type>
|
||||
static uint32_t calculate(const Type* data, size_t size)
|
||||
{
|
||||
CRC32 crc;
|
||||
crc.update(data, size);
|
||||
return crc.finalize();
|
||||
}
|
||||
|
||||
private:
|
||||
/// \brief The internal checksum state.
|
||||
uint32_t _state = ~0L;
|
||||
|
||||
};
|
|
@ -0,0 +1,925 @@
|
|||
// mods by James Zahary Dec 28, 2021 https://github.com/jameszah/ESPxWebFlMgr
|
||||
// Jan 12, 2022 - adds dates/times to display
|
||||
// based on https://github.com/holgerlembke/ESPxWebFlMgr
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <inttypes.h>
|
||||
#include "ESPxWebFlMgr.h"
|
||||
#include "ESPxWebFlMgrWp.h"
|
||||
#include "ESPxWebFlMgrWpF.h"
|
||||
|
||||
#include "crc32.h"
|
||||
|
||||
#include <time.h> //jz
|
||||
|
||||
#ifdef ESP8266
|
||||
#include <ESP8266WebServer.h>
|
||||
#include <FS.h>
|
||||
#endif
|
||||
|
||||
#ifdef ESP32
|
||||
#include <WebServer.h>
|
||||
#include <FS.h>
|
||||
#include <SD_MMC.h> //jz #include <SPIFFS.h>
|
||||
#include <detail/RequestHandlersImpl.h>
|
||||
#endif
|
||||
|
||||
|
||||
String getContentType(const String& path) {
|
||||
#ifdef ESP8266
|
||||
return mime::getContentType(path);
|
||||
#endif
|
||||
#ifdef ESP32
|
||||
return StaticRequestHandler::getContentType(path);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
//*****************************************************************************************************
|
||||
ESPxWebFlMgr::ESPxWebFlMgr(word port) {
|
||||
_Port = port;
|
||||
}
|
||||
|
||||
//*****************************************************************************************************
|
||||
ESPxWebFlMgr::~ESPxWebFlMgr() {
|
||||
end();
|
||||
}
|
||||
|
||||
//*****************************************************************************************************
|
||||
void ESPxWebFlMgr::begin() {
|
||||
#ifdef ESP8266
|
||||
fileManager = new ESP8266WebServer(_Port);
|
||||
#endif
|
||||
#ifdef ESP32
|
||||
fileManager = new WebServer(_Port);
|
||||
#endif
|
||||
|
||||
#ifdef fileManagerServerStaticsInternally
|
||||
fileManager->on("/", HTTP_GET, std::bind(&ESPxWebFlMgr::fileManagerIndexpage, this));
|
||||
fileManager->on("/fm.css", HTTP_GET, std::bind(&ESPxWebFlMgr::fileManagerCSS, this));
|
||||
fileManager->on("/fm.js", HTTP_GET, std::bind(&ESPxWebFlMgr::fileManagerJS, this));
|
||||
#endif
|
||||
fileManager->on("/bg.css", HTTP_GET, std::bind(&ESPxWebFlMgr::fileManagerGetBackGround, this));
|
||||
|
||||
fileManager->on("/i", HTTP_GET, std::bind(&ESPxWebFlMgr::fileManagerFileListInsert, this));
|
||||
fileManager->on("/c", HTTP_GET, std::bind(&ESPxWebFlMgr::fileManagerCommandExecutor, this));
|
||||
fileManager->on("/e", HTTP_GET, std::bind(&ESPxWebFlMgr::fileManagerFileEditorInsert, this));
|
||||
// file receiver with attached file to form
|
||||
fileManager->on("/r", HTTP_POST, std::bind(&ESPxWebFlMgr::fileManagerReceiverOK, this),
|
||||
std::bind(&ESPxWebFlMgr::fileManagerReceiver, this));
|
||||
|
||||
fileManager->onNotFound(std::bind(&ESPxWebFlMgr::fileManagerNotFound, this));
|
||||
|
||||
fileManager->begin();
|
||||
}
|
||||
|
||||
//*****************************************************************************************************
|
||||
void ESPxWebFlMgr::end() {
|
||||
if (fileManager) {
|
||||
delete fileManager;
|
||||
fileManager = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
//*****************************************************************************************************
|
||||
void ESPxWebFlMgr::handleClient() {
|
||||
if (fileManager) {
|
||||
fileManager->handleClient();
|
||||
}
|
||||
}
|
||||
|
||||
//*****************************************************************************************************
|
||||
void ESPxWebFlMgr::setViewSysFiles(bool vsf) {
|
||||
_ViewSysFiles = vsf;
|
||||
}
|
||||
|
||||
//*****************************************************************************************************
|
||||
bool ESPxWebFlMgr::getViewSysFiles(void) {
|
||||
return _ViewSysFiles;
|
||||
}
|
||||
|
||||
//*****************************************************************************************************
|
||||
void ESPxWebFlMgr::setSysFileStartPattern(String sfsp) {
|
||||
_SysFileStartPattern = sfsp;
|
||||
}
|
||||
|
||||
//*****************************************************************************************************
|
||||
String ESPxWebFlMgr::getSysFileStartPattern(void) {
|
||||
return _SysFileStartPattern;
|
||||
}
|
||||
|
||||
|
||||
//*****************************************************************************************************
|
||||
// privates start here
|
||||
//*****************************************************************************************************
|
||||
//*****************************************************************************************************
|
||||
//*****************************************************************************************************
|
||||
void ESPxWebFlMgr::fileManagerGetBackGround(void) {
|
||||
fileManager->send(200, F("text/css"), ".background {background-color: " + _backgroundColor + ";}");
|
||||
}
|
||||
|
||||
//*****************************************************************************************************
|
||||
void ESPxWebFlMgr::setBackGroundColor(const String backgroundColor) {
|
||||
_backgroundColor = backgroundColor;
|
||||
}
|
||||
|
||||
//*****************************************************************************************************
|
||||
void ESPxWebFlMgr::fileManagerNotFound(void) {
|
||||
String uri = fileManager->uri();
|
||||
|
||||
if (uri == "/") {
|
||||
uri = "/fm.html";
|
||||
}
|
||||
|
||||
String contentTyp = getContentType(uri);
|
||||
|
||||
if (ESPxWebFlMgr_FileSystem.exists(uri)) {
|
||||
File f = ESPxWebFlMgr_FileSystem.open(uri, "r");
|
||||
if (f) {
|
||||
if (fileManager->streamFile(f, contentTyp) != f.size()) {
|
||||
// Serial.println(F("Sent less data than expected!"));
|
||||
// We should panic a little bit.
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
} else
|
||||
{
|
||||
fileManager->send(404, F("text/plain"), F("URI not found."));
|
||||
}
|
||||
}
|
||||
|
||||
//*****************************************************************************************************
|
||||
String ESPxWebFlMgr::dispIntDotted(size_t i) {
|
||||
String res = "";
|
||||
while (i != 0) {
|
||||
int r = i % 1000;
|
||||
res = String(i % 1000) + res;
|
||||
i /= 1000;
|
||||
if ( (r < 100) && (i > 0) ) {
|
||||
res = "0" + res;
|
||||
if (r < 10) {
|
||||
res = "0" + res;
|
||||
}
|
||||
}
|
||||
if (i != 0) {
|
||||
res = "," + res; //jz dot to comma ;-)
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
//*****************************************************************************************************
|
||||
size_t ESPxWebFlMgr::totalBytes(void) {
|
||||
#ifdef ESP8266
|
||||
FSInfo info;
|
||||
ESPxWebFlMgr_FileSystem.info(info);
|
||||
return info.totalBytes;
|
||||
#endif
|
||||
|
||||
#ifdef ESP32
|
||||
return (ESPxWebFlMgr_FileSystem.totalBytes() / 1024);
|
||||
#endif
|
||||
}
|
||||
|
||||
//*****************************************************************************************************
|
||||
size_t ESPxWebFlMgr::usedBytes(void) {
|
||||
#ifdef ESP8266
|
||||
FSInfo info;
|
||||
ESPxWebFlMgr_FileSystem.info(info);
|
||||
return info.usedBytes;
|
||||
#endif
|
||||
|
||||
#ifdef ESP32
|
||||
return (ESPxWebFlMgr_FileSystem.usedBytes() / 1024);
|
||||
#endif
|
||||
}
|
||||
|
||||
//*****************************************************************************************************
|
||||
String ESPxWebFlMgr::dispFileString(size_t fs) {
|
||||
if (fs < 0) {
|
||||
return "-0";
|
||||
}
|
||||
|
||||
if (fs == 0) {
|
||||
return "0 kB";
|
||||
}
|
||||
|
||||
if (fs < 1000) {
|
||||
return String(fs) + " kB";
|
||||
}
|
||||
// switch from bytes to kilobytes due to 4gb+ sd cards //jz
|
||||
//String units[] = { "B", "kB", "MB", "GB", "TB" };
|
||||
String units[] = { "kB", "MB", "GB", "TB" };
|
||||
int digitGroups = (int) (log10(fs) / log10(1024));
|
||||
|
||||
//return String(fs / pow(1024, digitGroups)) + " " + units[digitGroups] + " <small>(" + dispIntDotted(fs) + " kB)</small>";
|
||||
return String(fs / pow(1024, digitGroups)) + " " + units[digitGroups] ;
|
||||
}
|
||||
|
||||
//*****************************************************************************************************
|
||||
void ESPxWebFlMgr::fileManagerIndexpage(void) {
|
||||
fileManager->send(200, F("text/html"), FPSTR(ESPxWebFlMgrWpindexpage));
|
||||
delay(1);
|
||||
}
|
||||
|
||||
//*****************************************************************************************************
|
||||
void ESPxWebFlMgr::fileManagerJS(void) {
|
||||
fileManager->send(200, F("text/javascript"), FPSTR(ESPxWebFlMgrWpjavascript));
|
||||
delay(1);
|
||||
}
|
||||
|
||||
//*****************************************************************************************************
|
||||
void ESPxWebFlMgr::fileManagerCSS(void) {
|
||||
fileManager->send(200, F("text/css"), FPSTR(ESPxWebFlMgrWpcss));
|
||||
delay(1);
|
||||
}
|
||||
|
||||
//*****************************************************************************************************
|
||||
String ESPxWebFlMgr::CheckFileNameLengthLimit(String fn) {
|
||||
// SPIFFS file name limit. Is there a way to get the max length from SPIFFS/LittleFS?
|
||||
// SPIFFS_OBJ_NAME_LEN is spiffs.... but not very clean.
|
||||
if (fn.length() > 32) {
|
||||
int len = fn.length();
|
||||
fn.remove(29);
|
||||
fn += String(len);
|
||||
}
|
||||
|
||||
return fn;
|
||||
}
|
||||
|
||||
//*****************************************************************************************************
|
||||
String ESPxWebFlMgr::colorline(int i) {
|
||||
if (i % 2 == 0) {
|
||||
return "ccu";
|
||||
} else {
|
||||
return "ccg";
|
||||
}
|
||||
}
|
||||
|
||||
//*****************************************************************************************************
|
||||
boolean ESPxWebFlMgr::allowAccessToThisFile(const String filename) {
|
||||
return ! filename.startsWith(_SysFileStartPattern);
|
||||
}
|
||||
|
||||
|
||||
//*****************************************************************************************************
|
||||
// jz kludge for switching folders on the sd card
|
||||
String subdir = "/";
|
||||
|
||||
//*****************************************************************************************************
|
||||
//[make FS from esp8266 and esp32 compatible]**********************************************************
|
||||
// this is the way MS DOS 3.x (?) did it with Int21 findfirst/findnext/findclose
|
||||
#ifdef ESP8266
|
||||
File ESPxWebFlMgr::nextFile(Dir &dir) {
|
||||
dir.next();
|
||||
return dir.openFile("r");
|
||||
}
|
||||
File ESPxWebFlMgr::firstFile(Dir &dir) {
|
||||
dir = ESPxWebFlMgr_FileSystem.openDir("/");
|
||||
return nextFile(dir);
|
||||
}
|
||||
#endif
|
||||
#ifdef ESP32
|
||||
#define Dir File
|
||||
File ESPxWebFlMgr::nextFile(Dir &dir) {
|
||||
return dir.openNextFile();
|
||||
}
|
||||
File ESPxWebFlMgr::firstFile(Dir &dir) {
|
||||
dir = ESPxWebFlMgr_FileSystem.open(subdir, "r"); //jz dir = ESPxWebFlMgr_FileSystem.open("/", "r");
|
||||
return nextFile(dir);
|
||||
}
|
||||
#endif
|
||||
|
||||
//*****************************************************************************************************
|
||||
void ESPxWebFlMgr::fileManagerFileListInsert(void) { // must get arg with /i to list that folder
|
||||
fileManager->setContentLength(CONTENT_LENGTH_UNKNOWN);
|
||||
fileManager->send(200, F("text/html"), String());
|
||||
|
||||
fileManager->sendContent(F("<div class=\"cc\"><div class=\"gc\">"));
|
||||
|
||||
bool gzipperexists = ( (ESPxWebFlMgr_FileSystem.exists("/gzipper.js.gz")) ||
|
||||
(ESPxWebFlMgr_FileSystem.exists("/gzipper.js")) );
|
||||
|
||||
//Serial.println(fileManager->args());
|
||||
//Serial.println(fileManager->argName(0));
|
||||
//Serial.println(fileManager->arg(0));
|
||||
|
||||
if ( (fileManager->args() == 1) && (fileManager->argName(0) == "subdir") ) {
|
||||
subdir = fileManager->arg(0);
|
||||
//Serial.print("Arg: "); Serial.println(fileManager->arg(0));
|
||||
|
||||
/*
|
||||
if (fileManager->arg(0) == "/"){
|
||||
subdir = "/";
|
||||
} else if (subdir == "/") {
|
||||
subdir = fileManager->arg(0);
|
||||
} else {
|
||||
subdir = fileManager->arg(0);
|
||||
}
|
||||
Serial.print("New subdir: "); Serial.println(subdir);
|
||||
*/
|
||||
} else {
|
||||
subdir = "/";
|
||||
}
|
||||
//Serial.print("Final subdir: "); Serial.println(subdir);
|
||||
|
||||
// first file is "go to root"
|
||||
|
||||
String fcd;
|
||||
String direct = "ccd"; //jz bland color for directory
|
||||
String fn = "/";
|
||||
fcd = "<div "
|
||||
"class=\"ccl " + direct + "\""
|
||||
"onclick=\"opendirectory('" + fn + "')\""
|
||||
"> " + fn + " - GOTO ROOT DIR -" + "</div>";
|
||||
fcd += "<div class=\"ccz " + direct + "\"> " + " " + " </div>";
|
||||
fcd += "<div class=\"cct " + direct + "\"> " + dispIntDotted(0) + " </div>";
|
||||
fcd += "<div class=\"ccr " + direct + "\"> ";
|
||||
fcd += " </div>";
|
||||
|
||||
fileManager->sendContent(fcd);
|
||||
|
||||
|
||||
// List files
|
||||
int i = 0;
|
||||
Dir dir;
|
||||
File file = firstFile(dir);
|
||||
while (file) {
|
||||
String fn = file.name();
|
||||
/*
|
||||
Serial.print("FN: >");
|
||||
Serial.print(fn);
|
||||
Serial.print("<");
|
||||
Serial.println();
|
||||
*/
|
||||
if ( (_ViewSysFiles) || (allowAccessToThisFile(fn)) ) {
|
||||
/*
|
||||
String fc = "<div id=\"file"+String(i)+"\" "
|
||||
"data-filename=\""+fn+"\""
|
||||
"class=\"ccl " + colorline(i) + "\""
|
||||
"onclick=\"downloadfile('" + fn + "')\""
|
||||
"> " + fn + "</div>";
|
||||
*/
|
||||
|
||||
String fc;
|
||||
String nsd;
|
||||
if (subdir == "/") {
|
||||
nsd = "/";
|
||||
} else {
|
||||
nsd = subdir + "/";
|
||||
}
|
||||
if (file.isDirectory()) {
|
||||
String direct = "ccd"; //jz bland color for directory
|
||||
fc = "<div "
|
||||
"class=\"ccl " + direct + "\""
|
||||
"onclick=\"opendirectory('" + nsd + fn + "')\""
|
||||
"> " + fn + " - DIR -" + "</div>";
|
||||
|
||||
} else {
|
||||
//Serial.println(subdir);
|
||||
//Serial.println(fn);
|
||||
fc = "<div "
|
||||
"class=\"ccl " + colorline(i) + "\""
|
||||
"onclick=\"downloadfile('" + nsd + fn + "')\""
|
||||
"> " + fn + "</div>";
|
||||
}
|
||||
|
||||
time_t t= file.getLastWrite();
|
||||
struct tm * tmstruct = localtime(&t);
|
||||
//Serial.printf(" LAST WRITE: %d-%02d-%02d %02d:%02d:%02d\n",(tmstruct->tm_year)+1900,( tmstruct->tm_mon)+1, tmstruct->tm_mday,tmstruct->tm_hour , tmstruct->tm_min, tmstruct->tm_sec);
|
||||
char ccz[100];
|
||||
sprintf(ccz, " %d-%02d-%02d %02d:%02d:%02d\n",(tmstruct->tm_year)+1900,( tmstruct->tm_mon)+1, tmstruct->tm_mday,tmstruct->tm_hour , tmstruct->tm_min, tmstruct->tm_sec);
|
||||
|
||||
|
||||
fc += "<div class=\"ccz " + colorline(i) + "\"> " + String(ccz) + " </div>"; //jz
|
||||
|
||||
// File f = dir.openFile("r");
|
||||
fc += "<div class=\"cct " + colorline(i) + "\"> " + dispIntDotted(file.size()) + " </div>";
|
||||
|
||||
fc += "<div class=\"ccr " + colorline(i) + "\"> "
|
||||
"<button title=\"Delete\" onclick=\"deletefile('" + fn + "')\" class=\"b\">D</button> "
|
||||
"<button title=\"Rename\" onclick=\"renamefile('" + fn + "')\" class=\"b\">R</button> ";
|
||||
|
||||
// no gziped version and (zipper or gziped zipper) exists
|
||||
if ( (! (fn.endsWith(".gz")) ) && gzipperexists) {
|
||||
fc += "<button title=\"Compress\" onclick=\"compressurlfile('" + fn + "')\" class=\"b\">C</button> ";
|
||||
}
|
||||
// for editor
|
||||
#ifndef fileManagerEditEverything
|
||||
String contentTyp = getContentType(fn);
|
||||
if ( (contentTyp.startsWith("text/")) ||
|
||||
(contentTyp.startsWith("application/j")) ) // boldly assume: json, javascript and everything else is edible....
|
||||
#endif
|
||||
{
|
||||
fc += "<button title=\"Edit\" onclick=\"editfile('" + fn + "')\" class=\"b\">E</button> ";
|
||||
}
|
||||
|
||||
fc += " </div>";
|
||||
|
||||
fileManager->sendContent(fc);
|
||||
i++;
|
||||
}
|
||||
file = nextFile(dir);
|
||||
}
|
||||
|
||||
// fileManager->sendContent("<span id=\"filecount\" data-count=\""+String(i)+"\"></span>");
|
||||
|
||||
String sinfo = " Size: " +
|
||||
dispFileString(totalBytes()) +
|
||||
", used: " +
|
||||
dispFileString(usedBytes());
|
||||
/*
|
||||
fileManager->sendContent(F(" FS blocksize: "));
|
||||
fileManager->sendContent(String(info.blockSize));
|
||||
fileManager->sendContent(F(", pageSize: "));
|
||||
fileManager->sendContent(String(info.pageSize));
|
||||
*/
|
||||
fileManager->sendContent(F("</div></div>"));
|
||||
|
||||
fileManager->sendContent(F("##"));
|
||||
fileManager->sendContent(sinfo);
|
||||
|
||||
fileManager->sendContent("");
|
||||
delay(1);
|
||||
}
|
||||
|
||||
//*****************************************************************************************************
|
||||
String ESPxWebFlMgr::escapeHTMLcontent(String html) {
|
||||
//html.replace("<","<");
|
||||
//html.replace(">",">");
|
||||
html.replace("&", "&");
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
// in place editor
|
||||
//*****************************************************************************************************
|
||||
void ESPxWebFlMgr::fileManagerFileEditorInsert(void) {
|
||||
//Serial.println("Edit");
|
||||
|
||||
if ( (fileManager->args() == 1) && (fileManager->argName(0) == "edit") ) {
|
||||
|
||||
String fn = "/" + fileManager->arg(0);
|
||||
if ( (! _ViewSysFiles) && (!allowAccessToThisFile(fn)) ) {
|
||||
fileManager->send(404, F("text/plain"), F("Illegal."));
|
||||
return;
|
||||
}
|
||||
|
||||
fileManager->setContentLength(CONTENT_LENGTH_UNKNOWN);
|
||||
fileManager->send(200, F("text/html"), String());
|
||||
|
||||
fileManager->sendContent(ESPxWebFlMgrWpFormIntro);
|
||||
|
||||
if (ESPxWebFlMgr_FileSystem.exists(subdir + "/" + fn)) {
|
||||
File f = ESPxWebFlMgr_FileSystem.open(subdir + "/" + fn, "r");
|
||||
if (f) {
|
||||
do {
|
||||
String l = f.readStringUntil('\n') + '\n';
|
||||
l = escapeHTMLcontent(l);
|
||||
fileManager->sendContent(l);
|
||||
} while (f.available());
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
|
||||
fileManager->sendContent(ESPxWebFlMgrWpFormExtro1);
|
||||
fileManager->sendContent(fn);
|
||||
fileManager->sendContent(ESPxWebFlMgrWpFormExtro2);
|
||||
|
||||
fileManager->sendContent("");
|
||||
} else {
|
||||
fileManager->send(404, F("text/plain"), F("Illegal."));
|
||||
}
|
||||
|
||||
delay(1);
|
||||
}
|
||||
|
||||
// Drag and Drop
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/File_drag_and_drop
|
||||
// https://www.ab-heute-programmieren.de/drag-and-drop-upload-mit-html5/#Schritt_3_Eine_Datei_hochladen
|
||||
//*****************************************************************************************************
|
||||
void ESPxWebFlMgr::fileManagerReceiverOK(void) {
|
||||
// Serial.println("fileManagerReceiverOK");
|
||||
fileManager->send(200);
|
||||
delay(1);
|
||||
}
|
||||
|
||||
//*****************************************************************************************************
|
||||
void ESPxWebFlMgr::fileManagerReceiver(void) {
|
||||
// Serial.println("fileManagerReceiver");
|
||||
|
||||
HTTPUpload& upload = fileManager->upload();
|
||||
// Serial.println("Server upload Status: " + String(upload.status));
|
||||
|
||||
if (upload.status == UPLOAD_FILE_START) {
|
||||
String filename = upload.filename;
|
||||
if (!filename.startsWith("/")) {
|
||||
filename = "/" + filename;
|
||||
}
|
||||
// Serial.print("handleFileUpload Name: ");
|
||||
// Serial.println(filename);
|
||||
|
||||
if (! ( (_ViewSysFiles) || (allowAccessToThisFile(filename)) ) ) {
|
||||
filename = "/illegalfilename";
|
||||
}
|
||||
|
||||
// cut length
|
||||
filename = CheckFileNameLengthLimit(filename);
|
||||
|
||||
fsUploadFile = ESPxWebFlMgr_FileSystem.open(subdir + filename, "w");
|
||||
} else if (upload.status == UPLOAD_FILE_WRITE) {
|
||||
// Serial.print("handleFileUpload Data: ");
|
||||
// Serial.println(upload.currentSize);
|
||||
if (fsUploadFile)
|
||||
fsUploadFile.write(upload.buf, upload.currentSize);
|
||||
} else if (upload.status == UPLOAD_FILE_END) {
|
||||
if (fsUploadFile) {
|
||||
fsUploadFile.close();
|
||||
// fsUploadFile = NULL;
|
||||
}
|
||||
// Serial.print("handleFileUpload Size: ");
|
||||
// Serial.println(upload.totalSize);
|
||||
}
|
||||
delay(1);
|
||||
}
|
||||
|
||||
struct __attribute__ ((__packed__)) zipFileHeader {
|
||||
uint32_t signature; // 0x04034b50;
|
||||
uint16_t versionneeded;
|
||||
uint16_t bitflags;
|
||||
uint16_t comp_method;
|
||||
uint16_t lastModFileTime;
|
||||
uint16_t lastModFileDate;
|
||||
uint32_t crc_32;
|
||||
uint32_t comp_size;
|
||||
uint32_t uncompr_size;
|
||||
uint16_t fname_len;
|
||||
uint16_t extra_field_len;
|
||||
};
|
||||
|
||||
struct __attribute__ ((__packed__)) zipDataDescriptor {
|
||||
uint32_t signature; // 0x08074b50
|
||||
uint32_t crc32;
|
||||
uint32_t comp_size;
|
||||
uint32_t uncompr_size;
|
||||
};
|
||||
|
||||
struct __attribute__ ((__packed__)) zipEndOfDirectory {
|
||||
uint32_t signature; // 0x06054b50;
|
||||
uint16_t nrofdisks;
|
||||
uint16_t diskwherecentraldirectorystarts;
|
||||
uint16_t nrofcentraldirectoriesonthisdisk;
|
||||
uint16_t totalnrofcentraldirectories;
|
||||
uint32_t sizeofcentraldirectory;
|
||||
uint32_t ofsetofcentraldirectoryrelativetostartofarchiv;
|
||||
uint16_t commentlength;
|
||||
};
|
||||
|
||||
struct __attribute__ ((__packed__)) zipCentralDirectoryFileHeader {
|
||||
uint32_t signature; // 0x02014b50
|
||||
uint16_t versionmadeby;
|
||||
uint16_t versionneededtoextract;
|
||||
uint16_t flag;
|
||||
uint16_t compressionmethode;
|
||||
uint16_t lastModFileTime;
|
||||
uint16_t lastModFileDate;
|
||||
uint32_t crc_32;
|
||||
uint32_t comp_size;
|
||||
uint32_t uncompr_size;
|
||||
uint16_t fname_len;
|
||||
uint16_t extra_len;
|
||||
uint16_t comment_len;
|
||||
uint16_t diskstart;
|
||||
uint16_t internalfileattr;
|
||||
uint32_t externalfileattr;
|
||||
uint32_t relofsoflocalfileheader;
|
||||
// nun filename, extra field, comment
|
||||
};
|
||||
|
||||
//*****************************************************************************************************
|
||||
int ESPxWebFlMgr::WriteChunk(const char* b, size_t l) {
|
||||
// Serial.print(" Chunk: " + String(l) + " ");
|
||||
|
||||
const char * footer = "\r\n";
|
||||
char chunkSize[11];
|
||||
sprintf(chunkSize, "%zx\r\n", l);
|
||||
fileManager->client().write(chunkSize, strlen(chunkSize));
|
||||
fileManager->client().write(b, l);
|
||||
fileManager->client().write(footer, 2);
|
||||
|
||||
return strlen(chunkSize) + l + 2;
|
||||
}
|
||||
|
||||
//*****************************************************************************************************
|
||||
/* https://en.wikipedia.org/wiki/Zip_(file_format)
|
||||
https://www.fileformat.info/tool/hexdump.htm
|
||||
https://hexed.it/?hl=de
|
||||
HxD https://mh-nexus.de/de/
|
||||
|
||||
This code needs some memory:
|
||||
4 * <nr. of files> + copybuffersize
|
||||
|
||||
Uses no compression, because, well, code size. Should be good for 4mb.
|
||||
*/
|
||||
void ESPxWebFlMgr::getAllFilesInOneZIP(void) {
|
||||
const byte copybuffersize = 100;
|
||||
|
||||
fileManager->setContentLength(CONTENT_LENGTH_UNKNOWN);
|
||||
// fileManager->sendHeader(F("Content-Type"), F("text/text"));
|
||||
// fileManager->sendHeader(F("Transfer-Encoding"), F("chunked"));
|
||||
// fileManager->sendHeader(F("Connection"), F("close"));
|
||||
fileManager->sendHeader(F("Content-Disposition"), F("attachment; filename=alles.zip"));
|
||||
fileManager->sendHeader(F("Content-Transfer-Encoding"), F("binary"));
|
||||
fileManager->send(200, F("application/octet-stream"), "");
|
||||
|
||||
// Pass 0: count files
|
||||
int files = 0;
|
||||
{
|
||||
Dir dir;
|
||||
File file = firstFile(dir);
|
||||
while (file) {
|
||||
String fn = file.name();
|
||||
if (!file.isDirectory() && (file.size() != 0)) { //jz
|
||||
if ( (_ViewSysFiles) || (allowAccessToThisFile(fn)) ) {
|
||||
files++;
|
||||
}
|
||||
}
|
||||
file = nextFile(dir);
|
||||
}
|
||||
//Serial.println("Files: " + String(files));
|
||||
}
|
||||
// Store the crcs
|
||||
uint32_t crc_32s[files];
|
||||
|
||||
// Pass 1: local headers + file
|
||||
{
|
||||
zipFileHeader zip;
|
||||
zip.signature = 0x04034b50;
|
||||
zip.versionneeded = 0;
|
||||
zip.bitflags = 1 << 3;
|
||||
zip.comp_method = 0; // stored
|
||||
zip.lastModFileTime = 0x4fa5;
|
||||
zip.lastModFileDate = 0xe44e;
|
||||
zip.extra_field_len = 0;
|
||||
|
||||
int i = 0;
|
||||
Dir dir;
|
||||
File file = firstFile(dir);
|
||||
while (file) {
|
||||
String fn = file.name();
|
||||
if (!file.isDirectory() && (file.size() != 0) ) { //jz
|
||||
if ( (_ViewSysFiles) || (allowAccessToThisFile(fn)) ) {
|
||||
if (fn.indexOf("/") == 0) {
|
||||
fn.remove(0, 1); // "/" filenames beginning with "/" dont work for Windows....
|
||||
}
|
||||
|
||||
zip.comp_size = 0;
|
||||
zip.uncompr_size = 0;
|
||||
zip.crc_32 = 0;
|
||||
zip.fname_len = fn.length();
|
||||
WriteChunk((char*)&zip, sizeof(zip));
|
||||
WriteChunk(fn.c_str(), zip.fname_len);
|
||||
|
||||
//Serial.print("Send: " + fn);
|
||||
// File f = dir.open("r",FILE_READ);
|
||||
int len = file.size();
|
||||
//Serial.print("\nsending "); Serial.print(fn);
|
||||
//Serial.print(" len is "); Serial.println(len);
|
||||
|
||||
// send crc+len later...
|
||||
zipDataDescriptor datadiscr;
|
||||
datadiscr.signature = 0x08074b50;
|
||||
datadiscr.comp_size = len;
|
||||
datadiscr.uncompr_size = len;
|
||||
|
||||
const char * footer = "\r\n";
|
||||
char chunkSize[11];
|
||||
sprintf(chunkSize, "%zx\r\n", len);
|
||||
fileManager->client().write(chunkSize, strlen(chunkSize));
|
||||
|
||||
{ // pff.
|
||||
CRC32 crc;
|
||||
byte b[copybuffersize];
|
||||
int lenr = len;
|
||||
while (lenr > 0) {
|
||||
byte r = (lenr > copybuffersize) ? copybuffersize : lenr;
|
||||
file.read(b, r);
|
||||
crc.update(b, r);
|
||||
fileManager->client().write(b, r);
|
||||
lenr -= r;
|
||||
// Serial.print(lenr);Serial.print(","); //jz
|
||||
}
|
||||
//Serial.println(" done");
|
||||
datadiscr.crc32 = crc.finalize();
|
||||
crc_32s[i] = datadiscr.crc32;
|
||||
}
|
||||
|
||||
fileManager->client().write(footer, 2);
|
||||
|
||||
WriteChunk((char*)&datadiscr, sizeof(datadiscr));
|
||||
|
||||
// f.close();
|
||||
i++;
|
||||
/** /
|
||||
Serial.print(" ");
|
||||
Serial.print(l);
|
||||
Serial.println();
|
||||
/**/
|
||||
}
|
||||
}
|
||||
file = nextFile(dir);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Pass 2: Central directory Structur
|
||||
{
|
||||
zipEndOfDirectory eod;
|
||||
eod.signature = 0x06054b50;
|
||||
eod.nrofdisks = 0;
|
||||
eod.diskwherecentraldirectorystarts = 0;
|
||||
eod.nrofcentraldirectoriesonthisdisk = 0;
|
||||
eod.totalnrofcentraldirectories = 0;
|
||||
eod.sizeofcentraldirectory = 0;
|
||||
eod.ofsetofcentraldirectoryrelativetostartofarchiv = 0;
|
||||
eod.commentlength = 0;
|
||||
|
||||
zipCentralDirectoryFileHeader CDFH;
|
||||
|
||||
CDFH.signature = 0x02014b50;
|
||||
CDFH.versionmadeby = 0;
|
||||
CDFH.versionneededtoextract = 0;
|
||||
CDFH.flag = 0;
|
||||
CDFH.compressionmethode = 0; // Stored
|
||||
CDFH.lastModFileTime = 0x4fa5;
|
||||
CDFH.lastModFileDate = 0xe44e;
|
||||
CDFH.extra_len = 0;
|
||||
CDFH.comment_len = 0;
|
||||
CDFH.diskstart = 0;
|
||||
CDFH.internalfileattr = 0x01;
|
||||
CDFH.externalfileattr = 0x20;
|
||||
CDFH.relofsoflocalfileheader = 0;
|
||||
|
||||
int i = 0;
|
||||
|
||||
Dir dir;
|
||||
File file = firstFile(dir);
|
||||
while (file) {
|
||||
String fn = file.name();
|
||||
if (!file.isDirectory() && (file.size() != 0)) { //jz
|
||||
if ( (_ViewSysFiles) || (allowAccessToThisFile(fn)) ) {
|
||||
if (fn.indexOf("/") == 0) {
|
||||
fn.remove(0, 1); // "/" filenames beginning with "/" dont work for Windows....
|
||||
}
|
||||
// Serial.print("CDFH: " + fn);
|
||||
// File f = dir.open("r",FILE_READ);
|
||||
int len = file.size();
|
||||
|
||||
//Serial.print("\nsending "); Serial.print(fn); //jz
|
||||
//Serial.print(" len is "); Serial.println(len);
|
||||
|
||||
CDFH.comp_size = len;
|
||||
CDFH.uncompr_size = len;
|
||||
CDFH.fname_len = fn.length();
|
||||
CDFH.crc_32 = crc_32s[i];
|
||||
|
||||
// f.close();
|
||||
|
||||
WriteChunk((char*)&CDFH, sizeof(CDFH));
|
||||
WriteChunk(fn.c_str(), CDFH.fname_len);
|
||||
|
||||
int ofs = sizeof(zipFileHeader) + len + CDFH.fname_len + sizeof(zipDataDescriptor);
|
||||
|
||||
// next position
|
||||
CDFH.relofsoflocalfileheader += ofs;
|
||||
|
||||
// book keeping
|
||||
eod.nrofcentraldirectoriesonthisdisk++;
|
||||
eod.totalnrofcentraldirectories++;
|
||||
eod.ofsetofcentraldirectoryrelativetostartofarchiv += ofs;
|
||||
eod.sizeofcentraldirectory += sizeof(CDFH) + CDFH.fname_len;
|
||||
|
||||
i++;
|
||||
}
|
||||
}
|
||||
file = nextFile(dir);
|
||||
}
|
||||
|
||||
// Serial.print("EOD: ");
|
||||
WriteChunk((char*)&eod, sizeof(eod));
|
||||
// Serial.println();
|
||||
}
|
||||
|
||||
const char * endchunk = "0\r\n\r\n";
|
||||
fileManager->client().write(endchunk, 5);
|
||||
|
||||
fileManager->sendContent("");
|
||||
delay(1);
|
||||
}
|
||||
|
||||
//*****************************************************************************************************
|
||||
void ESPxWebFlMgr::fileManagerCommandExecutor(void) {
|
||||
// https://www.youtube.com/watch?v=KSxTxynXiBs
|
||||
/*
|
||||
for (uint8_t i = 0; i < fileManager->args(); i++) {
|
||||
Serial.print(i);
|
||||
Serial.print(" ");
|
||||
Serial.print(fileManager->argName(i));
|
||||
Serial.print(": ");
|
||||
Serial.print(fileManager->arg(i));
|
||||
Serial.println();
|
||||
}
|
||||
*/
|
||||
|
||||
// no Args: DIE!
|
||||
if (fileManager->args() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// +--++--++--++--++--++--++--++--++--++--++--++--++--++--++--+
|
||||
// one arg, "za", zip all and download
|
||||
if ( (fileManager->args() == 1) && (fileManager->argName(0) == "za") ) {
|
||||
getAllFilesInOneZIP();
|
||||
// does it all
|
||||
return;
|
||||
}
|
||||
|
||||
// +--++--++--++--++--++--++--++--++--++--++--++--++--++--++--+
|
||||
// one arg, "dwn", download
|
||||
// must happen in the context of the webpage... thus via "window.location.href="/c?dwn="+filename;"
|
||||
if ( (fileManager->args() == 1) && (fileManager->argName(0) == "dwn") ) {
|
||||
String fn = fileManager->arg(0);
|
||||
Serial.println(fn);
|
||||
if ( (_ViewSysFiles) || (allowAccessToThisFile(fn)) ) {
|
||||
//Serial.println("allowed");
|
||||
File f = ESPxWebFlMgr_FileSystem.open("/" + fn, "r"); // add slash for esp32_arduino 2.0
|
||||
if (f) {
|
||||
//Serial.println("got it open");
|
||||
fileManager->sendHeader(F("Content-Type"), F("text/text"));
|
||||
fileManager->sendHeader(F("Connection"), F("close"));
|
||||
//Serial.print(">");Serial.print(fn);Serial.println("<");
|
||||
//Serial.println(fn.indexOf("/"));
|
||||
if (fn.indexOf("/") == 0) {
|
||||
fileManager->sendHeader(F("Content-Disposition"), "attachment; filename=" + fn.substring(1));
|
||||
} else {
|
||||
fileManager->sendHeader(F("Content-Disposition"), "attachment; filename=" + fn);
|
||||
}
|
||||
fileManager->sendHeader(F("Content-Transfer-Encoding"), F("binary"));
|
||||
if (fileManager->streamFile(f, "application/octet-stream") != f.size()) {
|
||||
Serial.println(F("Sent less data than expected!"));
|
||||
}
|
||||
f.close();
|
||||
return;
|
||||
} else {
|
||||
Serial.print("Could not open file "); Serial.println(fn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// +--++--++--++--++--++--++--++--++--++--++--++--++--++--++--+
|
||||
// one arg, "opd", opendirectory
|
||||
if ( (fileManager->args() == 1) && (fileManager->argName(0) == "opd") ) {
|
||||
String fn = fileManager->arg(0);
|
||||
if ( (_ViewSysFiles) || (allowAccessToThisFile(fn)) ) {
|
||||
delay(1);
|
||||
}
|
||||
}
|
||||
|
||||
// +--++--++--++--++--++--++--++--++--++--++--++--++--++--++--+
|
||||
// one arg, "del", delete
|
||||
if ( (fileManager->args() == 1) && (fileManager->argName(0) == "del") ) {
|
||||
String fn = fileManager->arg(0);
|
||||
if ( (_ViewSysFiles) || (allowAccessToThisFile(fn)) ) {
|
||||
int x = ESPxWebFlMgr_FileSystem.remove( subdir + "/" + fn); // Add slash
|
||||
//delay(1000);
|
||||
if (!x) {
|
||||
Serial.print("remove failed, try rmdir ");
|
||||
Serial.print( subdir + "/" + fn );
|
||||
int y = ESPxWebFlMgr_FileSystem.rmdir( "/" + fn); // Add slash
|
||||
//delay(1000);
|
||||
if (!y) {
|
||||
Serial.print("rmdir failed, directory must be empty! ");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// +--++--++--++--++--++--++--++--++--++--++--++--++--++--++--+
|
||||
// one arg, "ren", rename
|
||||
if ( (fileManager->args() == 2) && (fileManager->argName(0) == "ren") ) {
|
||||
String fn = fileManager->arg(0);
|
||||
if ( (_ViewSysFiles) || (allowAccessToThisFile(fn)) ) {
|
||||
String fn2 = CheckFileNameLengthLimit(fileManager->arg(1));
|
||||
if ( (_ViewSysFiles) || (allowAccessToThisFile(fn2)) ) {
|
||||
Serial.println(subdir);
|
||||
Serial.println(fn);
|
||||
Serial.println(fn2);
|
||||
ESPxWebFlMgr_FileSystem.rename( subdir + "/" + fn, subdir + "/" + fn2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dummy answer
|
||||
fileManager->send(200, "text/plain", "");
|
||||
delay(1);
|
||||
}
|
|
@ -0,0 +1,162 @@
|
|||
// mods by James Zahary Dec 28, 2021 https://github.com/jameszah/ESPxWebFlMgr
|
||||
// Jan 12, 2022 - adds dates/times to display
|
||||
// based on https://github.com/holgerlembke/ESPxWebFlMgr
|
||||
|
||||
// inline guard. Did I mention that c/c++ is broken by design?
|
||||
#ifndef ESPxWebFlMgr_h
|
||||
#define ESPxWebFlMgr_h
|
||||
|
||||
/*
|
||||
Changes
|
||||
V1.03
|
||||
x removed all SPIFFS from ESP32 version, switched fully to LittleFS
|
||||
x fixed rename+delete for ESP32+LittleFS (added "/")
|
||||
|
||||
V1.02
|
||||
x fixed the way to select the file system by conditional defines
|
||||
|
||||
V1.01
|
||||
+ added file name progress while uploading
|
||||
x fixed error in ZIP file structure (zip.bitflags needs a flag)
|
||||
|
||||
V1.00
|
||||
+ out of V0.9998...
|
||||
+ ESP8266: LittleFS is default
|
||||
+ javascript: added "msgline();"
|
||||
+ javascript: added "Loading..." as a not-working-hint to show that Javascript is disabled
|
||||
+ cleaning up the "/"-stuff (from SPIFF with leading "/" to LittleFS without...)
|
||||
+ Warning: esp8266 2.7.4 has an error in mime::getContentType(path) for .TXT. Fix line 65 is { kTxtSuffix, kTxt },
|
||||
+ review of "edit file", moved some stuff to ESPxWebFlMgrWpF.h
|
||||
*/
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <inttypes.h>
|
||||
|
||||
// file system default for esp8266 is LittleFS, for ESP32 it is SPIFFS (no time to check...)
|
||||
|
||||
#ifdef ESP8266
|
||||
#include <ESP8266WiFi.h>
|
||||
#include <ESP8266WebServer.h>
|
||||
#include <FS.h>
|
||||
//
|
||||
#include <LittleFS.h>
|
||||
#define ESPxWebFlMgr_FileSystem LittleFS
|
||||
/*
|
||||
#include <SPIFFS.h>
|
||||
#define ESPxWebFlMgr_FileSystem SPIFFS
|
||||
*/
|
||||
#endif
|
||||
|
||||
#ifdef ESP32
|
||||
#include <WiFi.h>
|
||||
#include <WebServer.h>
|
||||
#include <FS.h>
|
||||
#include <SD_MMC.h> //jz #include <LittleFS.h>
|
||||
#define ESPxWebFlMgr_FileSystem SD_MMC //jz #define ESPxWebFlMgr_FileSystem LittleFS
|
||||
#endif
|
||||
|
||||
|
||||
#ifndef ESPxWebFlMgr_FileSystem
|
||||
#pragma message ("ESPxWebFlMgr_FileSystem not defined.")
|
||||
#endif
|
||||
|
||||
/* undefine this to save about 10k code space.
|
||||
it requires to put the files from "<library>/filemanager" into the FS. No free lunch.
|
||||
*/
|
||||
#define fileManagerServerStaticsInternally
|
||||
|
||||
// will show the Edit-Button for every file type, even binary and such.
|
||||
//#define fileManagerEditEverything
|
||||
|
||||
class ESPxWebFlMgr {
|
||||
private:
|
||||
word _Port ;
|
||||
#ifdef ESP8266
|
||||
ESP8266WebServer * fileManager = NULL;
|
||||
#endif
|
||||
#ifdef ESP32
|
||||
WebServer * fileManager = NULL;
|
||||
#endif
|
||||
bool _ViewSysFiles = false;
|
||||
String _SysFileStartPattern = "/.";
|
||||
File fsUploadFile;
|
||||
String _backgroundColor = "black";
|
||||
|
||||
void fileManagerNotFound(void);
|
||||
String dispIntDotted(size_t i);
|
||||
String dispFileString(size_t fs);
|
||||
String CheckFileNameLengthLimit(String fn);
|
||||
|
||||
// the webpage
|
||||
void fileManagerIndexpage(void);
|
||||
void fileManagerJS(void);
|
||||
void fileManagerCSS(void);
|
||||
void fileManagerGetBackGround(void);
|
||||
|
||||
// javascript xmlhttp includes
|
||||
String colorline(int i);
|
||||
String escapeHTMLcontent(String html);
|
||||
void fileManagerFileListInsert(void);
|
||||
void fileManagerFileEditorInsert(void);
|
||||
boolean allowAccessToThisFile(const String filename);
|
||||
void fileManagerCommandExecutor(void);
|
||||
void fileManagerReceiverOK(void);
|
||||
void fileManagerReceiver(void);
|
||||
|
||||
// Zip-File uncompressed/stored
|
||||
void getAllFilesInOneZIP(void);
|
||||
int WriteChunk(const char* b, size_t l);
|
||||
|
||||
// helper: fs.h from esp32 and esp8266 don't have a compatible solution
|
||||
// for getting a file list from a directory
|
||||
#ifdef ESP32
|
||||
#define Dir File
|
||||
#endif
|
||||
File nextFile(Dir &dir);
|
||||
File firstFile(Dir &dir);
|
||||
// and not to get this data about usage...
|
||||
size_t totalBytes(void);
|
||||
size_t usedBytes(void);
|
||||
|
||||
public:
|
||||
ESPxWebFlMgr(word port);
|
||||
virtual ~ESPxWebFlMgr();
|
||||
|
||||
void begin();
|
||||
void end();
|
||||
virtual void handleClient();
|
||||
|
||||
// This must be called before the webpage is loaded in the browser...
|
||||
// must be a valid css color name, see https://en.wikipedia.org/wiki/Web_colors
|
||||
void setBackGroundColor(const String backgroundColor);
|
||||
|
||||
void setViewSysFiles(bool vsf);
|
||||
bool getViewSysFiles(void);
|
||||
|
||||
void setSysFileStartPattern(String sfsp);
|
||||
String getSysFileStartPattern(void);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
/*
|
||||
History
|
||||
|
||||
-- 2019-07-07
|
||||
+ Renamed to ESPxWebFlMgr and made it work with esp32 and esp8266
|
||||
+ separated file manager web page, "build script" to generate it
|
||||
|
||||
-- 2019-07-06
|
||||
+ "Download all files" creates a zip file from all files and downloads it
|
||||
+ option to set background color
|
||||
- html5 fixes
|
||||
|
||||
-- 2019-07-03
|
||||
+ Public Release on https://github.com/holgerlembke/ESP8266WebFlMgr
|
||||
|
||||
|
||||
Things to do
|
||||
|
||||
?? unify file system access for SPIFFS, LittleFS and SDFS
|
||||
|
||||
*/
|
|
@ -0,0 +1,541 @@
|
|||
// mods by James Zahary Dec 28, 2021 https://github.com/jameszah/ESPxWebFlMgr
|
||||
// Jan 12, 2022 - adds dates/times to display
|
||||
// based on https://github.com/holgerlembke/ESPxWebFlMgr
|
||||
|
||||
// inline guard. Did I mention that c/c++ is broken by design?
|
||||
#ifndef ESPxWebFlMgrWp_h
|
||||
#define ESPxWebFlMgrWp_h
|
||||
|
||||
// this file has been created by makeESPxWebFlMgrWp\do.cmd
|
||||
|
||||
//*****************************************************************************************************
|
||||
static const char ESPxWebFlMgrWpindexpage[] PROGMEM = R"==x==(
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>FileManager</title>
|
||||
<meta charset="utf-8"/>
|
||||
<link rel="stylesheet" type="text/css" href="/bg.css">
|
||||
<link rel="stylesheet" type="text/css" href="/fm.css">
|
||||
<script src="/fm.js"></script>
|
||||
<script src="/gzipper.js"></script>
|
||||
</head>
|
||||
<body class="background">
|
||||
<div id="gc">
|
||||
<div class="o1"> </div>
|
||||
<div class="o2"> </div>
|
||||
<div class="o3" id="o3"> </div>
|
||||
<div class="o4"> </div>
|
||||
|
||||
<div class="m1">
|
||||
<div class="s11"> </div>
|
||||
<div class="s12">
|
||||
<div class="s13 background"> </div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="m2" ondrop="dropHandler(event);" ondragover="dragOverHandler(event);">
|
||||
File<br />
|
||||
Drop<br />
|
||||
Zone<br />
|
||||
</div>
|
||||
<div class="m3">
|
||||
<div class="s31"> </div>
|
||||
<div class="s32">
|
||||
<div class="s33 background"> </div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="u1"> </div>
|
||||
<div class="u2" onclick="downloadall();">Download all files</div>
|
||||
<div class="u3" id="msg">Loading...</div>
|
||||
<div class="u4"> </div>
|
||||
<div class="c" id="fi">
|
||||
File list should appear here.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
)==x==";
|
||||
|
||||
static const char ESPxWebFlMgrWpjavascript[] PROGMEM = R"==x==(
|
||||
|
||||
function compressurlfile(source) {
|
||||
msgline("Fetching file...");
|
||||
var request = new XMLHttpRequest();
|
||||
request.onreadystatechange = function () {
|
||||
var DONE = this.DONE || 4;
|
||||
if (this.readyState === DONE) {
|
||||
var data = this.responseText;
|
||||
var gzip = require('gzip-js'), options = { level: 9, name: source, timestamp: parseInt(Date.now() / 1000, 10) };
|
||||
var out = gzip.zip(data, options);
|
||||
var bout = new Uint8Array(out); // out is 16 bits...
|
||||
|
||||
msgline("Sending compressed file...");
|
||||
var sendback = new XMLHttpRequest();
|
||||
sendback.onreadystatechange = function () {
|
||||
var DONE = this.DONE || 4;
|
||||
if (this.readyState === DONE) {
|
||||
getfileinsert();
|
||||
}
|
||||
};
|
||||
sendback.open('POST', '/r');
|
||||
var formdata = new FormData();
|
||||
var blob = new Blob([bout], { type: "application/octet-binary" });
|
||||
formdata.append(source + '.gz', blob, source + '.gz');
|
||||
sendback.send(formdata);
|
||||
}
|
||||
};
|
||||
request.open('GET', source, true);
|
||||
request.send(null);
|
||||
}
|
||||
|
||||
var subdir;
|
||||
|
||||
function getfileinsert() {
|
||||
msgline("Fetching files infos...");
|
||||
subdir = '/';
|
||||
var request = new XMLHttpRequest();
|
||||
request.onreadystatechange = function () {
|
||||
var DONE = this.DONE || 4;
|
||||
if (this.readyState === DONE) {
|
||||
var res = this.responseText.split("##");
|
||||
document.getElementById('fi').innerHTML = res[0];
|
||||
document.getElementById("o3").innerHTML = res[1];
|
||||
msgline("");
|
||||
}
|
||||
};
|
||||
request.open('GET', '/i', true);
|
||||
request.send(null);
|
||||
}
|
||||
|
||||
function getfileinsert2(strddd) {
|
||||
msgline("Fetching files infos...");
|
||||
subdir = strddd;
|
||||
var request = new XMLHttpRequest();
|
||||
request.onreadystatechange = function () {
|
||||
var DONE = this.DONE || 4;
|
||||
if (this.readyState === DONE) {
|
||||
var res = this.responseText.split("##");
|
||||
document.getElementById('fi').innerHTML = res[0];
|
||||
document.getElementById("o3").innerHTML = res[1];
|
||||
msgline("");
|
||||
}
|
||||
};
|
||||
request.open('GET', '/i?subdir=' + strddd, true); // must send the subdir variable to get that folder //jz
|
||||
request.send(null);
|
||||
}
|
||||
function executecommand(command) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = function () {
|
||||
var DONE = this.DONE || 4;
|
||||
if (this.readyState === DONE) {
|
||||
getfileinsert2(subdir);
|
||||
}
|
||||
};
|
||||
xhr.open('GET', '/c?' + command, true);
|
||||
xhr.send(null);
|
||||
}
|
||||
|
||||
function downloadfile(filename) {
|
||||
window.location.href = "/c?dwn=" + filename;
|
||||
}
|
||||
|
||||
function opendirectory(filename) {
|
||||
|
||||
getfileinsert2(filename);
|
||||
}
|
||||
|
||||
function deletefile(filename) {
|
||||
if (confirm("Really delete " + filename)) {
|
||||
msgline("Refresh when done deleting..."); //jz msgline("Please wait. Delete in progress...");
|
||||
executecommand("del=" + filename);
|
||||
}
|
||||
}
|
||||
|
||||
function renamefile(filename) {
|
||||
var newname = prompt("new name for " + filename, filename);
|
||||
if (newname != null) {
|
||||
msgline("Refresh when done renaming ..."); //jz msgline("Please wait. Rename in progress...");
|
||||
executecommand("ren=" + filename + "&new=" + newname);
|
||||
}
|
||||
}
|
||||
|
||||
var editxhr;
|
||||
|
||||
function editfile(filename) {
|
||||
msgline("Please wait. Creating editor...");
|
||||
|
||||
editxhr = new XMLHttpRequest();
|
||||
editxhr.onreadystatechange = function () {
|
||||
var DONE = this.DONE || 4;
|
||||
if (this.readyState === DONE) {
|
||||
document.getElementById('fi').innerHTML = editxhr.responseText;
|
||||
document.getElementById("o3").innerHTML = "Edit " + filename;
|
||||
msgline("");
|
||||
}
|
||||
};
|
||||
editxhr.open('GET', '/e?edit=' + filename, true);
|
||||
editxhr.send(null);
|
||||
}
|
||||
|
||||
function sved(filename) {
|
||||
var content = document.getElementById('tect').value;
|
||||
// utf-8
|
||||
content = unescape(encodeURIComponent(content));
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.open("POST", "/r", true);
|
||||
|
||||
var boundary = '-----whatever';
|
||||
xhr.setRequestHeader("Content-Type", "multipart/form-data; boundary=" + boundary);
|
||||
|
||||
var body = "" +
|
||||
'--' + boundary + '\r\n' +
|
||||
'Content-Disposition: form-data; name="uploadfile"; filename="' + filename + '"' + '\r\n' +
|
||||
'Content-Type: text/plain' + '\r\n' +
|
||||
'' + '\r\n' +
|
||||
content + '\r\n' +
|
||||
'--' + boundary + '--\r\n' + // \r\n fixes upload delay in ESP8266WebServer
|
||||
'';
|
||||
|
||||
// ajax does not do xhr.setRequestHeader("Content-length", body.length);
|
||||
|
||||
xhr.onreadystatechange = function () {
|
||||
var DONE = this.DONE || 4;
|
||||
if (this.readyState === DONE) {
|
||||
getfileinsert();
|
||||
}
|
||||
}
|
||||
|
||||
xhr.send(body);
|
||||
}
|
||||
|
||||
function abed() {
|
||||
getfileinsert();
|
||||
}
|
||||
|
||||
var uploaddone = true; // hlpr for multiple file uploads
|
||||
|
||||
function uploadFile(file, islast) {
|
||||
uploaddone = false;
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = function () {
|
||||
// console.log(xhr.status);
|
||||
var DONE = this.DONE || 4;
|
||||
if (this.readyState === DONE) {
|
||||
if (islast) {
|
||||
getfileinsert2(subdir);
|
||||
console.log('last file');
|
||||
}
|
||||
uploaddone = true;
|
||||
}
|
||||
};
|
||||
xhr.open('POST', '/r');
|
||||
var formdata = new FormData();
|
||||
//var newname = subdir + '/' + file.name; //jz didnt work, so do it in c++
|
||||
//file.name = newname;
|
||||
formdata.append('uploadfile', file);
|
||||
// not sure why, but with that the upload to esp32 is stable.
|
||||
formdata.append('dummy', 'dummy');
|
||||
xhr.send(formdata);
|
||||
}
|
||||
|
||||
var globaldropfilelisthlpr = null; // read-only-list, no shift()
|
||||
var transferitem = 0;
|
||||
var uploadFileProzessorhndlr = null;
|
||||
|
||||
function uploadFileProzessor() {
|
||||
if (uploaddone) {
|
||||
if (transferitem==globaldropfilelisthlpr.length) {
|
||||
clearInterval(uploadFileProzessorhndlr);
|
||||
} else {
|
||||
var file = globaldropfilelisthlpr[transferitem];
|
||||
msgline("Please wait. Transferring file "+file.name+"...");
|
||||
console.log('process file ' + file.name);
|
||||
transferitem++;
|
||||
uploadFile(file,transferitem==globaldropfilelisthlpr.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
function dropHandlerALT(ev) {
|
||||
console.log('File(s) dropped');
|
||||
|
||||
document.getElementById('msg').innerHTML = "Please wait. Transferring file...";
|
||||
|
||||
// Prevent default behavior (Prevent file from being opened)
|
||||
ev.preventDefault();
|
||||
|
||||
if (ev.dataTransfer.items) {
|
||||
// Use DataTransferItemList interface to access the file(s)
|
||||
for (var i = 0; i < ev.dataTransfer.items.length; i++) {
|
||||
// If dropped items aren't files, reject them
|
||||
if (ev.dataTransfer.items[i].kind === 'file') {
|
||||
var file = ev.dataTransfer.items[i].getAsFile();
|
||||
uploadFile(file);
|
||||
console.log('.1. file[' + i + '].name = ' + file.name);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Use DataTransfer interface to access the file(s)
|
||||
for (var i = 0; i < ev.dataTransfer.files.length; i++) {
|
||||
console.log('.2. file[' + i + '].name = ' + ev.dataTransfer.files[i].name);
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
function dropHandler(ev) {
|
||||
console.log('File(s) dropped');
|
||||
|
||||
globaldropfilelisthlpr = ev.dataTransfer;
|
||||
transferitem = 0;
|
||||
|
||||
msgline("Please wait. Transferring file...");
|
||||
|
||||
// Prevent default behavior (Prevent file from being opened)
|
||||
ev.preventDefault();
|
||||
|
||||
if (ev.dataTransfer.items) {
|
||||
var data = ev.dataTransfer;
|
||||
globaldropfilelisthlpr = data.files;
|
||||
uploadFileProzessorhndlr = setInterval(uploadFileProzessor,1000);
|
||||
console.log('Init upload list.');
|
||||
} else {
|
||||
// Use DataTransfer interface to access the file(s)
|
||||
for (var i = 0; i < ev.dataTransfer.files.length; i++) {
|
||||
console.log('.2. file[' + i + '].name = ' + ev.dataTransfer.files[i].name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function dragOverHandler(ev) {
|
||||
console.log('File(s) in drop zone');
|
||||
|
||||
// Prevent default behavior (Prevent file from being opened)
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
function msgline(msg) {
|
||||
document.getElementById('msg').innerHTML = msg;
|
||||
}
|
||||
|
||||
function downloadall() {
|
||||
msgline("Sending all files in one zip.");
|
||||
window.location.href = "/c?za=all";
|
||||
msgline("");
|
||||
}
|
||||
|
||||
//->
|
||||
window.onload = getfileinsert;
|
||||
|
||||
)==x==";
|
||||
|
||||
|
||||
//*****************************************************************************************************
|
||||
static const char ESPxWebFlMgrWpcss[] PROGMEM = R"==g==(
|
||||
|
||||
div {
|
||||
margin: 1px;
|
||||
padding: 0px;
|
||||
font-family: 'Segoe UI', Verdana, sans-serif;
|
||||
}
|
||||
|
||||
#gc {
|
||||
display: grid;
|
||||
grid-template-columns: 80px 25% auto 30px;
|
||||
grid-template-rows: 20px 30px auto 30px 20px;
|
||||
grid-template-areas: "o1 o2 o3 o4" "m1 c c c" "m2 c c c" "m3 c c c" "u1 u2 u3 u4";
|
||||
}
|
||||
|
||||
.o1 {
|
||||
grid-area: o1;
|
||||
background-color: #9999CC;
|
||||
border-top-left-radius: 20px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.o2 {
|
||||
grid-area: o2;
|
||||
background-color: #9999FF;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.o3 {
|
||||
grid-area: o3;
|
||||
background-color: #CC99CC;
|
||||
margin-bottom: 0px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.o4 {
|
||||
grid-area: o4;
|
||||
background-color: #CC6699;
|
||||
border-radius: 0 10px 10px 0;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.m1 {
|
||||
grid-area: m1;
|
||||
margin-top: 0px;
|
||||
background-color: #9999CC;
|
||||
display: grid;
|
||||
grid-template-columns: 60px 20px;
|
||||
grid-template-rows: 20px;
|
||||
grid-template-areas: "s11 s12";
|
||||
}
|
||||
|
||||
.s12 {
|
||||
margin: 0px;
|
||||
background-color: #9999CC;
|
||||
}
|
||||
|
||||
.s13 {
|
||||
margin: 0px;
|
||||
border-top-left-radius: 20px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.m2 {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
grid-area: m2;
|
||||
background-color: #CC6699;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.m3 {
|
||||
grid-area: m3;
|
||||
margin-bottom: 0px;
|
||||
background-color: #9999CC;
|
||||
display: grid;
|
||||
grid-template-columns: 60px 20px;
|
||||
grid-template-rows: 20px;
|
||||
grid-template-areas: "s31 s32";
|
||||
}
|
||||
|
||||
.s32 {
|
||||
margin: 0px;
|
||||
background-color: #9999CC;
|
||||
}
|
||||
|
||||
.s33 {
|
||||
margin: 0px;
|
||||
border-bottom-left-radius: 20px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.u1 {
|
||||
grid-area: u1;
|
||||
background-color: #9999CC;
|
||||
border-bottom-left-radius: 20px;
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.u2 {
|
||||
grid-area: u2;
|
||||
cursor: pointer;
|
||||
background-color: #CC6666;
|
||||
margin-top: 0px;
|
||||
padding-left: 10px;
|
||||
vertical-align: middle;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.u2:hover {
|
||||
background-color: #9999FF;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.u3 {
|
||||
grid-area: u3;
|
||||
padding-left: 10px;
|
||||
background-color: #FF9966;
|
||||
font-size: 80%;
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.u4 {
|
||||
grid-area: u4;
|
||||
background-color: #FF9900;
|
||||
border-radius: 0 10px 10px 0;
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.c {
|
||||
grid-area: c;
|
||||
}
|
||||
|
||||
#fi .b {
|
||||
background-color: Transparent;
|
||||
border: 1px solid #9999FF;
|
||||
border-radius: 1px;
|
||||
padding: 0px;
|
||||
width: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#fi .b:hover {
|
||||
background-color: #9999FF;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cc {
|
||||
width: min-content;
|
||||
margin: 10px 0px;
|
||||
}
|
||||
|
||||
.gc div {
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.ccg {
|
||||
height: 1.5em;
|
||||
background-color: #A5A5FF;
|
||||
}
|
||||
|
||||
.ccu {
|
||||
height: 1.5em;
|
||||
background-color: #FE9A00;
|
||||
}
|
||||
|
||||
.ccd {
|
||||
height: 1.5em;
|
||||
background-color: #e8e2d8;
|
||||
}
|
||||
|
||||
.ccl {
|
||||
border-radius: 5px 0 0 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ccl:hover {
|
||||
border-radius: 5px 0 0 5px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ccr {
|
||||
border-radius: 0 5px 5px 0;
|
||||
}
|
||||
|
||||
.cct {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.ccz {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.gc {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, max-content);
|
||||
}
|
||||
)==g==";
|
||||
|
||||
|
||||
#endif
|
|
@ -0,0 +1,23 @@
|
|||
// mods by James Zahary Dec 28, 2021 https://github.com/jameszah/ESPxWebFlMgr
|
||||
// based on https://github.com/holgerlembke/ESPxWebFlMgr
|
||||
|
||||
// inline guard. Still broken by design?
|
||||
#ifndef ESPxWebFlMgrWpF_h
|
||||
#define ESPxWebFlMgrWpF_h
|
||||
|
||||
static const char ESPxWebFlMgrWpFormIntro[] PROGMEM =
|
||||
R"==x==(<form><textarea id="tect" rows="25" cols="80">)==x==";
|
||||
|
||||
|
||||
static const char ESPxWebFlMgrWpFormExtro1[] PROGMEM =
|
||||
R"==x==(</textarea></form><button title="Save file" onclick="sved(')==x==";
|
||||
|
||||
// noot sure what the <script> part is for.
|
||||
static const char ESPxWebFlMgrWpFormExtro2[] PROGMEM =
|
||||
R"==x==(');" >Save</button> <button title="Abort editing" onclick="abed();">Abort editing</button>
|
||||
|
||||
<script id="info">document.getElementById('o3').innerHTML = "File:";</script>)==x==";
|
||||
|
||||
|
||||
|
||||
#endif
|
Plik diff jest za duży
Load Diff
|
@ -0,0 +1,968 @@
|
|||
/*
|
||||
Copyright (c) 2018 Brian Lough. All right reserved.
|
||||
|
||||
UniversalTelegramBot - Library to create your own Telegram Bot using
|
||||
ESP8266 or ESP32 on Arduino IDE.
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
/*
|
||||
**** Note Regarding Client Connection Keeping ****
|
||||
Client connection is established in functions that directly involve use of
|
||||
client, i.e sendGetToTelegram, sendPostToTelegram, and
|
||||
sendMultipartFormDataToTelegram. It is closed at the end of
|
||||
sendMultipartFormDataToTelegram, but not at the end of sendGetToTelegram and
|
||||
sendPostToTelegram as these may need to keep the connection alive for respose
|
||||
/ response checking. Re-establishing a connection then wastes time which is
|
||||
noticeable in user experience. Due to this, it is important that connection
|
||||
be closed manually after calling sendGetToTelegram or sendPostToTelegram by
|
||||
calling closeClient(); Failure to close connection causes memory leakage and
|
||||
SSL errors
|
||||
*/
|
||||
|
||||
// James Zahary June 30, 2020
|
||||
// - small mods to add caption to photos, and slow down transmit to telegram
|
||||
// James Zahary Jan 2, 2022
|
||||
// - Client to WiFiClientSecure due to esp32-arduino 2.02
|
||||
|
||||
#include "UniversalTelegramBot.h"
|
||||
|
||||
UniversalTelegramBot::UniversalTelegramBot(String token, WiFiClientSecure &client) { // UniversalTelegramBot::UniversalTelegramBot(String token, Client &client) {
|
||||
|
||||
_token = token;
|
||||
#ifdef ARDUINO_ESP8266_RELEASE_2_5_0
|
||||
//client->setInsecure();
|
||||
#endif
|
||||
this->client = &client;
|
||||
}
|
||||
|
||||
String UniversalTelegramBot::sendGetToTelegram(String command) {
|
||||
String mess = "";
|
||||
long now;
|
||||
bool avail;
|
||||
|
||||
// Connect with api.telegram.org if not already connected
|
||||
if (!client->connected()) {
|
||||
#ifdef _debug
|
||||
Serial.println(F("[BOT]Connecting to server"));
|
||||
#endif
|
||||
if (!client->connect(HOST, SSL_PORT)) {
|
||||
#ifdef _debug
|
||||
Serial.println(F("[BOT]Conection error"));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
if (client->connected()) {
|
||||
|
||||
#ifdef _debug
|
||||
Serial.println(F(".... connected to server"));
|
||||
#endif
|
||||
|
||||
String a = "";
|
||||
char c;
|
||||
int ch_count = 0;
|
||||
client->println("GET /" + command);
|
||||
now = millis();
|
||||
avail = false;
|
||||
while (millis() - now < longPoll * 1000 + waitForResponse) {
|
||||
while (client->available()) {
|
||||
char c = client->read();
|
||||
if (ch_count < maxMessageLength) {
|
||||
mess = mess + c;
|
||||
ch_count++;
|
||||
}
|
||||
avail = true;
|
||||
}
|
||||
if (avail) {
|
||||
#ifdef _debug
|
||||
Serial.println();
|
||||
Serial.println(mess);
|
||||
Serial.println();
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mess;
|
||||
}
|
||||
|
||||
String UniversalTelegramBot::sendPostToTelegram(String command, JsonObject payload) {
|
||||
|
||||
String body = "";
|
||||
String headers = "";
|
||||
long now;
|
||||
bool responseReceived = false;
|
||||
|
||||
// Connect with api.telegram.org if not already connected
|
||||
if (!client->connected()) {
|
||||
#ifdef _debug
|
||||
Serial.println(F("[BOT Client]Connecting to server"));
|
||||
#endif
|
||||
if (!client->connect(HOST, SSL_PORT)) {
|
||||
#ifdef _debug
|
||||
Serial.println(F("[BOT Client]Conection error"));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
if (client->connected()) {
|
||||
// POST URI
|
||||
client->print("POST /" + command);
|
||||
client->println(F(" HTTP/1.1"));
|
||||
delay(jzdelay);
|
||||
// Host header
|
||||
client->print(F("Host:"));
|
||||
client->println(HOST);
|
||||
delay(jzdelay);
|
||||
// JSON content type
|
||||
client->println(F("Content-Type: application/json"));
|
||||
delay(jzdelay);
|
||||
|
||||
// Content length
|
||||
int length = measureJson(payload);
|
||||
client->print(F("Content-Length:"));
|
||||
client->println(length);
|
||||
delay(jzdelay);
|
||||
// End of headers
|
||||
client->println();
|
||||
// POST message body
|
||||
String out;
|
||||
serializeJson(payload, out);
|
||||
|
||||
client->println(out);
|
||||
delay(jzdelay);
|
||||
|
||||
int ch_count = 0;
|
||||
now = millis();
|
||||
bool finishedHeaders = false;
|
||||
bool currentLineIsBlank = true;
|
||||
while (millis() - now < waitForResponse) {
|
||||
while (client->available()) {
|
||||
char c = client->read();
|
||||
responseReceived = true;
|
||||
|
||||
if (!finishedHeaders) {
|
||||
if (currentLineIsBlank && c == '\n') {
|
||||
finishedHeaders = true;
|
||||
} else {
|
||||
headers = headers + c;
|
||||
}
|
||||
} else {
|
||||
if (ch_count < maxMessageLength) {
|
||||
body = body + c;
|
||||
ch_count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (c == '\n') currentLineIsBlank = true;
|
||||
else if (c != '\r') currentLineIsBlank = false;
|
||||
|
||||
}
|
||||
|
||||
if (responseReceived && ch_count > 5) { //jz
|
||||
#ifdef _debug
|
||||
Serial.println();
|
||||
Serial.println(body);
|
||||
Serial.println();
|
||||
#endif
|
||||
//Serial.print(millis() - now); Serial.println(" sendPostToTelegram - breaking");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
String UniversalTelegramBot::sendMultipartFormDataToTelegram(
|
||||
String command, String binaryProperyName, String fileName,
|
||||
String contentType, String chat_id, int fileSize,
|
||||
MoreDataAvailable moreDataAvailableCallback,
|
||||
GetNextByte getNextByteCallback,
|
||||
GetNextBuffer getNextBufferCallback,
|
||||
GetNextBufferLen getNextBufferLenCallback) {
|
||||
|
||||
String body = "";
|
||||
String headers = "";
|
||||
long now;
|
||||
bool responseReceived = false;
|
||||
bool finishedHeaders = false;
|
||||
bool currentLineIsBlank = true;
|
||||
|
||||
String boundry = F("------------------------b8f610217e83e29b");
|
||||
|
||||
// Connect with api.telegram.org if not already connected
|
||||
if (!client->connected()) {
|
||||
#ifdef _debug
|
||||
Serial.println(F("[BOT Client]Connecting to server"));
|
||||
#endif
|
||||
if (!client->connect(HOST, SSL_PORT)) {
|
||||
#ifdef _debug
|
||||
Serial.println(F("[BOT Client]Conection error"));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
if (client->connected()) {
|
||||
|
||||
String start_request = "";
|
||||
String end_request = "";
|
||||
|
||||
start_request = start_request + "--" + boundry + "\r\n";
|
||||
start_request = start_request + "content-disposition: form-data; name=\"chat_id\"" + "\r\n";
|
||||
start_request = start_request + "\r\n";
|
||||
start_request = start_request + chat_id + "\r\n";
|
||||
|
||||
start_request = start_request + "--" + boundry + "\r\n";
|
||||
start_request = start_request + "content-disposition: form-data; name=\"caption\"" + "\r\n";
|
||||
start_request = start_request + "\r\n";
|
||||
start_request = start_request + "caption here!" + "\r\n";
|
||||
|
||||
start_request = start_request + "--" + boundry + "\r\n";
|
||||
start_request = start_request + "content-disposition: form-data; name=\"" + binaryProperyName + "\"; filename=\"" + fileName + "\"" + "\r\n";
|
||||
|
||||
start_request = start_request + "Content-Type: " + contentType + "\r\n";
|
||||
start_request = start_request + "\r\n";
|
||||
|
||||
end_request = end_request + "\r\n";
|
||||
end_request = end_request + "--" + boundry + "--" + "\r\n";
|
||||
|
||||
client->print("POST /bot" + _token + "/" + command);
|
||||
client->println(F(" HTTP/1.1"));
|
||||
// Host header
|
||||
client->print(F("Host: "));
|
||||
client->println(HOST);
|
||||
client->println(F("User-Agent: arduino/1.0"));
|
||||
Serial.print("*") ; delay(jzdelay); //jz
|
||||
client->println(F("Accept: */*"));
|
||||
Serial.print("*") ; delay(jzdelay); //jz
|
||||
|
||||
int contentLength = fileSize + start_request.length() + end_request.length();
|
||||
#ifdef _debug
|
||||
Serial.println("Content-Length: " + String(contentLength));
|
||||
#endif
|
||||
client->print("Content-Length: ");
|
||||
client->println(String(contentLength));
|
||||
client->println("Content-Type: multipart/form-data; boundary=" + boundry);
|
||||
Serial.print("*") ; delay(jzdelay); //jz
|
||||
client->println(); //v99 - ssl not happy
|
||||
Serial.print("*") ; delay(jzdelay); //jz
|
||||
client->print(start_request);
|
||||
Serial.print("Start request: " + start_request);
|
||||
#ifdef _debug
|
||||
Serial.print("Start request: " + start_request);
|
||||
#endif
|
||||
|
||||
if (getNextByteCallback == nullptr) {
|
||||
while (moreDataAvailableCallback()) {
|
||||
client->write((const uint8_t *)getNextBufferCallback(), getNextBufferLenCallback());
|
||||
#ifdef _debug
|
||||
Serial.println(F("Sending photo from buffer"));
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
#ifdef _debug
|
||||
Serial.println(F("Sending photo by binary"));
|
||||
#endif
|
||||
byte buffer[jzblocksize]; //jz 512
|
||||
int count = 0;
|
||||
char ch;
|
||||
while (moreDataAvailableCallback()) {
|
||||
buffer[count] = getNextByteCallback();
|
||||
count++;
|
||||
if (count == jzblocksize) { //jz 512
|
||||
// yield();
|
||||
#ifdef _debug
|
||||
//Serial.println(F("Sending binary photo full buffer"));
|
||||
#endif
|
||||
client->write((const uint8_t *)buffer, jzblocksize); //jz 512
|
||||
Serial.print("*") ; delay(jzdelay); //jz
|
||||
count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (count > 0) {
|
||||
#ifdef _debug
|
||||
Serial.println(F("Sending binary photo remaining buffer"));
|
||||
#endif
|
||||
client->write((const uint8_t *)buffer, count);
|
||||
Serial.print("*") ; delay(jzdelay); //jz
|
||||
}
|
||||
}
|
||||
|
||||
client->print(end_request);
|
||||
//#ifdef _debug
|
||||
Serial.print("End request: " + end_request);
|
||||
//#endif
|
||||
|
||||
Serial.print("... Done Sending. Client.Available = "); Serial.println(client->available());
|
||||
delay(2000);
|
||||
Serial.print("... 2 secs later. Client.Available = "); Serial.println(client->available());
|
||||
|
||||
int ch_count = 0;
|
||||
now = millis();
|
||||
|
||||
while (millis() - now < waitForResponse) {
|
||||
while (client->available()) {
|
||||
char c = client->read();
|
||||
responseReceived = true;
|
||||
|
||||
if (!finishedHeaders) {
|
||||
if (currentLineIsBlank && c == '\n') {
|
||||
finishedHeaders = true;
|
||||
} else {
|
||||
headers = headers + c;
|
||||
}
|
||||
} else {
|
||||
if (ch_count < maxMessageLength) {
|
||||
body = body + c;
|
||||
ch_count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (c == '\n') currentLineIsBlank = true;
|
||||
else if (c != '\r') currentLineIsBlank = false;
|
||||
}
|
||||
|
||||
if (responseReceived && ch_count > 5) { //jz && ch_count > 5
|
||||
#ifdef _debug
|
||||
Serial.println();
|
||||
Serial.println(body);
|
||||
Serial.println();
|
||||
#endif
|
||||
//Serial.print(millis() - now); Serial.println(" sendMultipartFormDataToTelegram - breaking");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
closeClient();
|
||||
return body;
|
||||
}
|
||||
|
||||
String UniversalTelegramBot::sendMultipartFormDataToTelegramWithCaption(
|
||||
String command, String binaryProperyName, String fileName,
|
||||
String contentType, String caption, String chat_id, int fileSize,
|
||||
MoreDataAvailable moreDataAvailableCallback,
|
||||
GetNextByte getNextByteCallback,
|
||||
GetNextBuffer getNextBufferCallback,
|
||||
GetNextBufferLen getNextBufferLenCallback) {
|
||||
|
||||
String body = "";
|
||||
String headers = "";
|
||||
long now;
|
||||
bool responseReceived = false;
|
||||
bool finishedHeaders = false;
|
||||
bool currentLineIsBlank = true;
|
||||
|
||||
String boundry = F("------------------------b8f610217e83e29b");
|
||||
|
||||
// Connect with api.telegram.org if not already connected
|
||||
if (!client->connected()) {
|
||||
#ifdef _debug
|
||||
Serial.println(F("[BOT Client]Connecting to server"));
|
||||
#endif
|
||||
//client->stop();
|
||||
//client -> setInsecure(); //a1
|
||||
//client -> setTimeout(2);
|
||||
//client -> setHandshakeTimeout(120000); //120000
|
||||
int x = client->connect(HOST, SSL_PORT);
|
||||
if (x == 0) {
|
||||
#ifdef _debug
|
||||
Serial.println(F("[BOT Client]Conection error"));
|
||||
delay(2000);
|
||||
int x = client->connect(HOST, SSL_PORT);
|
||||
delay(2000);
|
||||
if (!client->connected()) {
|
||||
Serial.println(F("[BOT Client]Another Conection error"));
|
||||
delay(2000);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
if (client->connected()) {
|
||||
|
||||
String start_request = "";
|
||||
String end_request = "";
|
||||
|
||||
|
||||
//Serial.print("Start: "); Serial.println(ESP.getFreeHeap());
|
||||
|
||||
start_request = start_request + "--" + boundry + "\r\n";
|
||||
start_request = start_request + "content-disposition: form-data; name=\"chat_id\"" + "\r\n";
|
||||
start_request = start_request + "\r\n";
|
||||
start_request = start_request + chat_id + "\r\n";
|
||||
|
||||
start_request = start_request + "--" + boundry + "\r\n"; //jz caption stuff
|
||||
start_request = start_request + "content-disposition: form-data; name=\"caption\"" + "\r\n";
|
||||
start_request = start_request + "\r\n";
|
||||
start_request = start_request + caption + "\r\n";
|
||||
|
||||
start_request = start_request + "--" + boundry + "\r\n";
|
||||
start_request = start_request + "content-disposition: form-data; name=\"" + binaryProperyName + "\"; filename=\"" + fileName + "\"" + "\r\n";
|
||||
|
||||
start_request = start_request + "Content-Type: " + contentType + "\r\n";
|
||||
start_request = start_request + "\r\n";
|
||||
|
||||
end_request = end_request + "\r\n";
|
||||
end_request = end_request + "--" + boundry + "--" + "\r\n";
|
||||
|
||||
client->print("POST /bot" + _token + "/" + command);
|
||||
client->println(F(" HTTP/1.1"));
|
||||
// Host header
|
||||
client->print(F("Host: "));
|
||||
client->println(HOST);
|
||||
client->println(F("User-Agent: arduino/1.0"));
|
||||
Serial.print("*") ; delay(jzdelay); //jz
|
||||
client->println(F("Accept: */*"));
|
||||
Serial.print("*") ; delay(jzdelay); //jz
|
||||
|
||||
int contentLength = fileSize + start_request.length() + end_request.length();
|
||||
#ifdef _debug
|
||||
Serial.println("Content-Length: " + String(contentLength));
|
||||
#endif
|
||||
client->print("Content-Length: ");
|
||||
client->println(String(contentLength));
|
||||
client->println("Content-Type: multipart/form-data; boundary=" + boundry);
|
||||
Serial.print("*") ; delay(jzdelay); //jz
|
||||
client->println(); //v99
|
||||
Serial.print("*") ; delay(jzdelay); //jz
|
||||
client->print(start_request);
|
||||
|
||||
#ifdef _debug
|
||||
Serial.print("Start request: " + start_request);
|
||||
#endif
|
||||
|
||||
//Serial.print("End: "); Serial.println(ESP.getFreeHeap());
|
||||
|
||||
if (getNextByteCallback == nullptr) {
|
||||
while (moreDataAvailableCallback()) {
|
||||
client->write((const uint8_t *)getNextBufferCallback(), getNextBufferLenCallback());
|
||||
#ifdef _debug
|
||||
Serial.println(F("Sending photo from buffer"));
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
#ifdef _debug
|
||||
Serial.println(F("Sending photo by binary"));
|
||||
#endif
|
||||
byte buffer[jzblocksize];
|
||||
int count = 0;
|
||||
char ch;
|
||||
while (moreDataAvailableCallback()) {
|
||||
buffer[count] = getNextByteCallback();
|
||||
count++;
|
||||
if (count == jzblocksize) {
|
||||
// yield();
|
||||
#ifdef _debug
|
||||
//Serial.println(F("Sending binary photo full buffer"));
|
||||
#endif
|
||||
client->write((const uint8_t *)buffer, jzblocksize);
|
||||
Serial.print("*") ; delay(jzdelay); //jz
|
||||
count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (count > 0) {
|
||||
#ifdef _debug
|
||||
Serial.println(F("Sending binary photo remaining buffer"));
|
||||
#endif
|
||||
client->write((const uint8_t *)buffer, count);
|
||||
Serial.print("*") ; delay(jzdelay); //jz
|
||||
}
|
||||
}
|
||||
|
||||
client->print(end_request);
|
||||
#ifdef _debug
|
||||
Serial.print("End request: " + end_request);
|
||||
|
||||
Serial.print("... Done Sending. Client.Available = "); Serial.println(client->available());
|
||||
delay(2000);
|
||||
Serial.print("... 2 secs later. Client.Available = "); Serial.println(client->available());
|
||||
#endif
|
||||
|
||||
int ch_count = 0;
|
||||
now = millis();
|
||||
|
||||
while (millis() - now < waitForResponse) {
|
||||
while (client->available()) {
|
||||
char c = client->read();
|
||||
responseReceived = true;
|
||||
|
||||
if (!finishedHeaders) {
|
||||
if (currentLineIsBlank && c == '\n') {
|
||||
finishedHeaders = true;
|
||||
} else {
|
||||
headers = headers + c;
|
||||
}
|
||||
} else {
|
||||
if (ch_count < maxMessageLength) {
|
||||
body = body + c;
|
||||
ch_count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (c == '\n') currentLineIsBlank = true;
|
||||
else if (c != '\r') currentLineIsBlank = false;
|
||||
}
|
||||
|
||||
if (responseReceived && ch_count > 5) { //jz && ch_count > 5
|
||||
#ifdef _debug
|
||||
Serial.println();
|
||||
Serial.println(body);
|
||||
Serial.println();
|
||||
#endif
|
||||
//Serial.print(millis() - now); Serial.println(" sendMultipartFormDataToTelegram - breaking");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
closeClient();
|
||||
return body;
|
||||
}
|
||||
|
||||
bool UniversalTelegramBot::getMe() {
|
||||
String command = "bot" + _token + "/getMe";
|
||||
String response = sendGetToTelegram(command); // receive reply from telegram.org
|
||||
DynamicJsonDocument doc(maxMessageLength);
|
||||
DeserializationError error = deserializeJson(doc, response);
|
||||
JsonObject obj = doc.as<JsonObject>(); //there is nothing better right now to use obj.containsKey("result")
|
||||
closeClient();
|
||||
|
||||
if (!error) {
|
||||
if (obj.containsKey("result")) {
|
||||
String _name = doc["result"]["first_name"];
|
||||
String _username = doc["result"]["username"];
|
||||
name = _name;
|
||||
userName = _username;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/***************************************************************
|
||||
GetUpdates - function to receive messages from telegram
|
||||
(Argument to pass: the last+1 message to read)
|
||||
Returns the number of new messages
|
||||
***************************************************************/
|
||||
int UniversalTelegramBot::getUpdates(long offset) {
|
||||
|
||||
#ifdef _debug
|
||||
Serial.println(F("GET Update Messages"));
|
||||
#endif
|
||||
String command = "bot" + _token + "/getUpdates?offset=" + String(offset) + "&limit=" + String(HANDLE_MESSAGES);
|
||||
if (longPoll > 0) {
|
||||
command = command + "&timeout=" + String(longPoll);
|
||||
}
|
||||
String response = sendGetToTelegram(command); // receive reply from telegram.org
|
||||
|
||||
if (response == "") {
|
||||
#ifdef _debug
|
||||
Serial.println(F("Received empty string in response!"));
|
||||
#endif
|
||||
// close the client as there's nothing to do with an empty string
|
||||
closeClient();
|
||||
return 0;
|
||||
} else {
|
||||
#ifdef _debug
|
||||
Serial.print(F("incoming message length "));
|
||||
Serial.println(response.length());
|
||||
Serial.println(F("Creating DynamicJsonBuffer"));
|
||||
#endif
|
||||
|
||||
// Parse response into Json object
|
||||
DynamicJsonDocument doc(maxMessageLength);
|
||||
DeserializationError error = deserializeJson(doc, response);
|
||||
#ifdef _debug
|
||||
Serial.print(F("GetUpdates parsed jsonDoc: "));
|
||||
serializeJson(doc, Serial);
|
||||
Serial.println();
|
||||
#endif
|
||||
|
||||
JsonObject obj = doc.as<JsonObject>(); //there is nothing better right now
|
||||
if (!error) {
|
||||
#ifdef _debug
|
||||
Serial.print(F("GetUpdates parsed jsonObj: "));
|
||||
serializeJson(obj, Serial);
|
||||
Serial.println();
|
||||
#endif
|
||||
if (obj.containsKey("result")) {
|
||||
int resultArrayLength = doc["result"].size();
|
||||
if (resultArrayLength > 0) {
|
||||
int newMessageIndex = 0;
|
||||
// Step through all results
|
||||
for (int i = 0; i < resultArrayLength; i++) {
|
||||
JsonObject result = doc["result"][i];
|
||||
if (processResult(result, newMessageIndex)) newMessageIndex++;
|
||||
}
|
||||
// We will keep the client open because there may be a response to be
|
||||
// given
|
||||
return newMessageIndex;
|
||||
} else {
|
||||
#ifdef _debug
|
||||
Serial.println(F("no new messages"));
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
#ifdef _debug
|
||||
Serial.println(F("Response contained no 'result'"));
|
||||
#endif
|
||||
}
|
||||
} else { // Parsing failed
|
||||
if (response.length() < 2) { // Too short a message. Maybe a connection issue
|
||||
#ifdef _debug
|
||||
Serial.println(F("Parsing error: Message too short"));
|
||||
#endif
|
||||
} else {
|
||||
// Buffer may not be big enough, increase buffer or reduce max number of
|
||||
// messages
|
||||
#ifdef _debug
|
||||
Serial.print(F("Failed to parse update, the message could be too "
|
||||
"big for the buffer. Error code: "));
|
||||
Serial.println(error.c_str()); // debug print of parsing error
|
||||
#endif
|
||||
}
|
||||
}
|
||||
// Close the client as no response is to be given
|
||||
closeClient();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool UniversalTelegramBot::processResult(JsonObject result, int messageIndex) {
|
||||
int update_id = result["update_id"];
|
||||
// Check have we already dealt with this message (this shouldn't happen!)
|
||||
if (last_message_received != update_id) {
|
||||
last_message_received = update_id;
|
||||
messages[messageIndex].update_id = update_id;
|
||||
messages[messageIndex].text = F("");
|
||||
messages[messageIndex].from_id = F("");
|
||||
messages[messageIndex].from_name = F("");
|
||||
messages[messageIndex].longitude = 0;
|
||||
messages[messageIndex].latitude = 0;
|
||||
|
||||
if (result.containsKey("message")) {
|
||||
JsonObject message = result["message"];
|
||||
messages[messageIndex].type = F("message");
|
||||
messages[messageIndex].from_id = message["from"]["id"].as<String>();
|
||||
messages[messageIndex].from_name = message["from"]["first_name"].as<String>();
|
||||
messages[messageIndex].date = message["date"].as<String>();
|
||||
messages[messageIndex].chat_id = message["chat"]["id"].as<String>();
|
||||
messages[messageIndex].chat_title = message["chat"]["title"].as<String>();
|
||||
|
||||
if (message.containsKey("text")) {
|
||||
messages[messageIndex].text = message["text"].as<String>();
|
||||
|
||||
} else if (message.containsKey("location")) {
|
||||
messages[messageIndex].longitude = message["location"]["longitude"].as<float>();
|
||||
messages[messageIndex].latitude = message["location"]["latitude"].as<float>();
|
||||
}
|
||||
} else if (result.containsKey("channel_post")) {
|
||||
JsonObject message = result["channel_post"];
|
||||
messages[messageIndex].type = F("channel_post");
|
||||
messages[messageIndex].text = message["text"].as<String>();
|
||||
messages[messageIndex].date = message["date"].as<String>();
|
||||
messages[messageIndex].chat_id = message["chat"]["id"].as<String>();
|
||||
messages[messageIndex].chat_title = message["chat"]["title"].as<String>();
|
||||
|
||||
} else if (result.containsKey("callback_query")) {
|
||||
JsonObject message = result["callback_query"];
|
||||
messages[messageIndex].type = F("callback_query");
|
||||
messages[messageIndex].from_id = message["from"]["id"].as<String>();
|
||||
messages[messageIndex].from_name = message["from"]["first_name"].as<String>();
|
||||
messages[messageIndex].text = message["data"].as<String>();
|
||||
messages[messageIndex].date = message["date"].as<String>();
|
||||
messages[messageIndex].chat_id = message["message"]["chat"]["id"].as<String>();
|
||||
messages[messageIndex].chat_title = F("");
|
||||
|
||||
} else if (result.containsKey("edited_message")) {
|
||||
JsonObject message = result["edited_message"];
|
||||
messages[messageIndex].type = F("edited_message");
|
||||
messages[messageIndex].from_id = message["from"]["id"].as<String>();
|
||||
messages[messageIndex].from_name = message["from"]["first_name"].as<String>();
|
||||
messages[messageIndex].date = message["date"].as<String>();
|
||||
messages[messageIndex].chat_id = message["chat"]["id"].as<String>();
|
||||
messages[messageIndex].chat_title = message["chat"]["title"].as<String>();
|
||||
|
||||
if (message.containsKey("text")) {
|
||||
messages[messageIndex].text = message["text"].as<String>();
|
||||
|
||||
} else if (message.containsKey("location")) {
|
||||
messages[messageIndex].longitude = message["location"]["longitude"].as<float>();
|
||||
messages[messageIndex].latitude = message["location"]["latitude"].as<float>();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/***********************************************************************
|
||||
SendMessage - function to send message to telegram
|
||||
(Arguments to pass: chat_id, text to transmit and markup(optional))
|
||||
***********************************************************************/
|
||||
bool UniversalTelegramBot::sendSimpleMessage(String chat_id, String text,
|
||||
String parse_mode) {
|
||||
|
||||
bool sent = false;
|
||||
#ifdef _debug
|
||||
Serial.println(F("sendSimpleMessage: SEND Simple Message"));
|
||||
#endif
|
||||
long sttime = millis();
|
||||
|
||||
if (text != "") {
|
||||
while (millis() < sttime + 8000) { // loop for a while to send the message
|
||||
String command = "bot" + _token + "/sendMessage?chat_id=" + chat_id +
|
||||
"&text=" + text + "&parse_mode=" + parse_mode;
|
||||
String response = sendGetToTelegram(command);
|
||||
#ifdef _debug
|
||||
Serial.println(response);
|
||||
#endif
|
||||
sent = checkForOkResponse(response);
|
||||
if (sent) break;
|
||||
}
|
||||
}
|
||||
closeClient();
|
||||
return sent;
|
||||
}
|
||||
|
||||
bool UniversalTelegramBot::sendMessage(String chat_id, String text,
|
||||
String parse_mode) {
|
||||
|
||||
DynamicJsonDocument payload(maxMessageLength);
|
||||
payload["chat_id"] = chat_id;
|
||||
payload["text"] = text;
|
||||
|
||||
if (parse_mode != "")
|
||||
payload["parse_mode"] = parse_mode;
|
||||
|
||||
return sendPostMessage(payload.as<JsonObject>());
|
||||
}
|
||||
|
||||
bool UniversalTelegramBot::sendMessageWithReplyKeyboard(
|
||||
String chat_id, String text, String parse_mode, String keyboard,
|
||||
bool resize, bool oneTime, bool selective) {
|
||||
|
||||
DynamicJsonDocument payload(maxMessageLength);
|
||||
payload["chat_id"] = chat_id;
|
||||
payload["text"] = text;
|
||||
|
||||
if (parse_mode != "")
|
||||
payload["parse_mode"] = parse_mode;
|
||||
|
||||
JsonObject replyMarkup = payload.createNestedObject("reply_markup");
|
||||
|
||||
// Reply keyboard is an array of arrays.
|
||||
// Outer array represents rows
|
||||
// Inner arrays represents columns
|
||||
// This example "ledon" and "ledoff" are two buttons on the top row
|
||||
// and "status is a single button on the next row"
|
||||
DynamicJsonDocument keyboardBuffer(maxMessageLength); // creating a buffer enough to keep keyboard string
|
||||
deserializeJson(keyboardBuffer, keyboard);
|
||||
replyMarkup["keyboard"] = keyboardBuffer.as<JsonArray>();
|
||||
|
||||
// Telegram defaults these values to false, so to decrease the size of the
|
||||
// payload we will only send them if needed
|
||||
if (resize)
|
||||
replyMarkup["resize_keyboard"] = resize;
|
||||
|
||||
if (oneTime)
|
||||
replyMarkup["one_time_keyboard"] = oneTime;
|
||||
|
||||
if (selective)
|
||||
replyMarkup["selective"] = selective;
|
||||
|
||||
return sendPostMessage(payload.as<JsonObject>());
|
||||
}
|
||||
|
||||
bool UniversalTelegramBot::sendMessageWithInlineKeyboard(String chat_id,
|
||||
String text,
|
||||
String parse_mode,
|
||||
String keyboard) {
|
||||
|
||||
DynamicJsonDocument payload(maxMessageLength);
|
||||
payload["chat_id"] = chat_id;
|
||||
payload["text"] = text;
|
||||
|
||||
if (parse_mode != "")
|
||||
payload["parse_mode"] = parse_mode;
|
||||
|
||||
JsonObject replyMarkup = payload.createNestedObject("reply_markup");
|
||||
DynamicJsonDocument keyboardBuffer(maxMessageLength); // assuming keyboard buffer will alwas be limited to 1024 bytes
|
||||
deserializeJson(keyboardBuffer, keyboard);
|
||||
replyMarkup["inline_keyboard"] = keyboardBuffer.as<JsonArray>();
|
||||
return sendPostMessage(payload.as<JsonObject>());
|
||||
}
|
||||
|
||||
/***********************************************************************
|
||||
SendPostMessage - function to send message to telegram
|
||||
(Arguments to pass: chat_id, text to transmit and markup(optional))
|
||||
***********************************************************************/
|
||||
bool UniversalTelegramBot::sendPostMessage(JsonObject payload) {
|
||||
|
||||
bool sent = false;
|
||||
#ifdef _debug
|
||||
Serial.print(F("sendPostMessage: SEND Post Message: "));
|
||||
serializeJson(payload, Serial);
|
||||
Serial.println();
|
||||
#endif
|
||||
long sttime = millis();
|
||||
|
||||
if (payload.containsKey("text")) {
|
||||
while (millis() < sttime + 8000) { // loop for a while to send the message
|
||||
String command = "bot" + _token + "/sendMessage";
|
||||
String response = sendPostToTelegram(command, payload);
|
||||
#ifdef _debug
|
||||
Serial.println(response);
|
||||
#endif
|
||||
sent = checkForOkResponse(response);
|
||||
if (sent) break;
|
||||
}
|
||||
}
|
||||
|
||||
closeClient();
|
||||
return sent;
|
||||
}
|
||||
|
||||
String UniversalTelegramBot::sendPostPhoto(JsonObject payload) {
|
||||
|
||||
bool sent = false;
|
||||
String response = "";
|
||||
#ifdef _debug
|
||||
Serial.println(F("sendPostPhoto: SEND Post Photo"));
|
||||
#endif
|
||||
long sttime = millis();
|
||||
|
||||
if (payload.containsKey("photo")) {
|
||||
while (millis() < sttime + 8000) { // loop for a while to send the message
|
||||
String command = "bot" + _token + "/sendPhoto";
|
||||
response = sendPostToTelegram(command, payload);
|
||||
#ifdef _debug
|
||||
Serial.println(response);
|
||||
#endif
|
||||
sent = checkForOkResponse(response);
|
||||
if (sent) break;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
closeClient();
|
||||
return response;
|
||||
}
|
||||
|
||||
String UniversalTelegramBot::sendPhotoByBinary(
|
||||
String chat_id, String contentType, int fileSize,
|
||||
MoreDataAvailable moreDataAvailableCallback,
|
||||
GetNextByte getNextByteCallback, GetNextBuffer getNextBufferCallback, GetNextBufferLen getNextBufferLenCallback) {
|
||||
|
||||
#ifdef _debug
|
||||
Serial.println(F("sendPhotoByBinary: SEND Photo"));
|
||||
#endif
|
||||
|
||||
String response = sendMultipartFormDataToTelegram("sendPhoto", "photo", "img.jpg",
|
||||
contentType, chat_id, fileSize,
|
||||
moreDataAvailableCallback, getNextByteCallback, getNextBufferCallback, getNextBufferLenCallback);
|
||||
|
||||
#ifdef _debug
|
||||
Serial.println(response);
|
||||
#endif
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
|
||||
String UniversalTelegramBot::sendPhoto(String chat_id, String photo,
|
||||
String caption,
|
||||
bool disable_notification,
|
||||
int reply_to_message_id,
|
||||
String keyboard) {
|
||||
|
||||
DynamicJsonDocument payload(maxMessageLength);
|
||||
payload["chat_id"] = chat_id;
|
||||
payload["photo"] = photo;
|
||||
|
||||
if (caption)
|
||||
payload["caption"] = caption;
|
||||
|
||||
if (disable_notification)
|
||||
payload["disable_notification"] = disable_notification;
|
||||
|
||||
if (reply_to_message_id && reply_to_message_id != 0)
|
||||
payload["reply_to_message_id"] = reply_to_message_id;
|
||||
|
||||
if (keyboard) {
|
||||
JsonObject replyMarkup = payload.createNestedObject("reply_markup");
|
||||
DynamicJsonDocument keyboardBuffer(maxMessageLength); // assuming keyboard buffer will alwas be limited to 1024 bytes
|
||||
deserializeJson(keyboardBuffer, keyboard);
|
||||
replyMarkup["keyboard"] = keyboardBuffer.as<JsonArray>();
|
||||
}
|
||||
|
||||
return sendPostPhoto(payload.as<JsonObject>());
|
||||
}
|
||||
|
||||
bool UniversalTelegramBot::checkForOkResponse(String response) {
|
||||
int responseLength = response.length();
|
||||
|
||||
for (int m = 5; m < responseLength + 1; m++) {
|
||||
if (response.substring(m - 10, m) ==
|
||||
"{\"ok\":true") { // Chek if message has been properly sent
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UniversalTelegramBot::sendChatAction(String chat_id, String text) {
|
||||
|
||||
bool sent = false;
|
||||
#ifdef _debug
|
||||
Serial.println(F("SEND Chat Action Message"));
|
||||
#endif
|
||||
long sttime = millis();
|
||||
|
||||
if (text != "") {
|
||||
while (millis() < sttime + 8000) { // loop for a while to send the message
|
||||
String command = "bot" + _token + "/sendChatAction?chat_id=" + chat_id +
|
||||
"&action=" + text;
|
||||
String response = sendGetToTelegram(command);
|
||||
|
||||
#ifdef _debug
|
||||
Serial.println(response);
|
||||
#endif
|
||||
sent = checkForOkResponse(response);
|
||||
|
||||
if (sent) break;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
closeClient();
|
||||
return sent;
|
||||
}
|
||||
|
||||
void UniversalTelegramBot::closeClient() {
|
||||
if (client->connected()) {
|
||||
#ifdef _debug
|
||||
Serial.println(F("Closing client"));
|
||||
#endif
|
||||
client->stop();
|
||||
client -> setHandshakeTimeout(120000); // bug in esp32-arduino 2.0.2
|
||||
}
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
Copyright (c) 2018 Brian Lough. All right reserved.
|
||||
|
||||
UniversalTelegramBot - Library to create your own Telegram Bot using
|
||||
ESP8266 or ESP32 on Arduino IDE.
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef UniversalTelegramBot_h
|
||||
#define UniversalTelegramBot_h
|
||||
|
||||
#define ARDUINOJSON_DECODE_UNICODE 1
|
||||
#define ARDUINOJSON_USE_LONG_LONG 1
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <Client.h>
|
||||
#include <core_version.h>
|
||||
|
||||
// a1
|
||||
#include <WiFiClientSecure.h>
|
||||
|
||||
#define HOST "api.telegram.org"
|
||||
#define SSL_PORT 443
|
||||
#define HANDLE_MESSAGES 1
|
||||
|
||||
//unmark following line to enable debug mode
|
||||
//#define _debug
|
||||
|
||||
typedef bool (*MoreDataAvailable)();
|
||||
typedef byte (*GetNextByte)();
|
||||
typedef byte* (*GetNextBuffer)();
|
||||
typedef int (GetNextBufferLen)();
|
||||
|
||||
struct telegramMessage {
|
||||
String text;
|
||||
String chat_id;
|
||||
String chat_title;
|
||||
String from_id;
|
||||
String from_name;
|
||||
String date;
|
||||
String type;
|
||||
float longitude;
|
||||
float latitude;
|
||||
int update_id;
|
||||
};
|
||||
|
||||
class UniversalTelegramBot {
|
||||
public:
|
||||
UniversalTelegramBot(String token, WiFiClientSecure &client); //UniversalTelegramBot(String token, Client &client);
|
||||
|
||||
String sendGetToTelegram(String command);
|
||||
String sendPostToTelegram(String command, JsonObject payload);
|
||||
String
|
||||
sendMultipartFormDataToTelegram(String command, String binaryProperyName,
|
||||
String fileName, String contentType,
|
||||
String chat_id, int fileSize,
|
||||
MoreDataAvailable moreDataAvailableCallback,
|
||||
GetNextByte getNextByteCallback,
|
||||
GetNextBuffer getNextBufferCallback,
|
||||
GetNextBufferLen getNextBufferLenCallback);
|
||||
|
||||
String
|
||||
sendMultipartFormDataToTelegramWithCaption(String command, String binaryProperyName,
|
||||
String fileName, String contentType,
|
||||
String caption, String chat_id, int fileSize,
|
||||
MoreDataAvailable moreDataAvailableCallback,
|
||||
GetNextByte getNextByteCallback,
|
||||
GetNextBuffer getNextBufferCallback,
|
||||
GetNextBufferLen getNextBufferLenCallback);
|
||||
|
||||
|
||||
bool getMe();
|
||||
|
||||
bool sendSimpleMessage(String chat_id, String text, String parse_mode);
|
||||
bool sendMessage(String chat_id, String text, String parse_mode = "");
|
||||
bool sendMessageWithReplyKeyboard(String chat_id, String text,
|
||||
String parse_mode, String keyboard,
|
||||
bool resize = false, bool oneTime = false,
|
||||
bool selective = false);
|
||||
bool sendMessageWithInlineKeyboard(String chat_id, String text,
|
||||
String parse_mode, String keyboard);
|
||||
|
||||
bool sendChatAction(String chat_id, String text);
|
||||
|
||||
bool sendPostMessage(JsonObject payload);
|
||||
String sendPostPhoto(JsonObject payload);
|
||||
String sendPhotoByBinary(String chat_id, String contentType, int fileSize,
|
||||
MoreDataAvailable moreDataAvailableCallback,
|
||||
GetNextByte getNextByteCallback,
|
||||
GetNextBuffer getNextBufferCallback,
|
||||
GetNextBufferLen getNextBufferLenCallback);
|
||||
String sendPhoto(String chat_id, String photo, String caption = "",
|
||||
bool disable_notification = false,
|
||||
int reply_to_message_id = 0, String keyboard = "");
|
||||
|
||||
int getUpdates(long offset);
|
||||
bool checkForOkResponse(String response);
|
||||
telegramMessage messages[HANDLE_MESSAGES];
|
||||
long last_message_received;
|
||||
String name;
|
||||
String userName;
|
||||
int longPoll = 0;
|
||||
int waitForResponse = 5000; //jz = 1500;
|
||||
int jzdelay = 10; //60; // delay between multipart blocks
|
||||
int jzblocksize = 2 * 1024; // multipart block size
|
||||
|
||||
private:
|
||||
// JsonObject * parseUpdates(String response);
|
||||
String _token;
|
||||
WiFiClientSecure *client; //Client *client; //a1 2.02 esp32-arduino
|
||||
void closeClient();
|
||||
const int maxMessageLength = 1500; //was 1500
|
||||
bool processResult(JsonObject result, int messageIndex);
|
||||
};
|
||||
|
||||
#endif
|
|
@ -0,0 +1,65 @@
|
|||
static const char devname[] = "desklens"; // name of your camera for mDNS, Router, and filenames
|
||||
|
||||
#define include_telegram
|
||||
//#define include_pir_and_touch
|
||||
#define include_ftp
|
||||
#define include_streaming
|
||||
#define get_rid_of_touch
|
||||
|
||||
int delete_old_files = 1; // set to 1 and it will delete your oldest day of files so you SD is always 10% empty
|
||||
|
||||
// https://sites.google.com/a/usapiens.com/opnode/time-zones -- find your timezone here
|
||||
#define TIMEZONE "GMT0BST,M3.5.0/01,M10.5.0/02" // your timezone - this is GMT
|
||||
|
||||
// 1 for blink red led with every sd card write, at your frame rate
|
||||
// 0 for blink only for skipping frames and SOS if camera or sd is broken
|
||||
#define BlinkWithWrite 1
|
||||
|
||||
// EDIT ssid and password **** with Version 98x-WiFiMan, you are using WiFiManager to set ssid and password, so these are redundant
|
||||
const char* ssid = "jzjzjzjz";
|
||||
const char* password = "mrpeanut";
|
||||
|
||||
// reboot startup parameters here
|
||||
|
||||
int Internet_Enabled = 1; // set to 0 to shut off all internet activities - wifi, time, http, ftp, telegram
|
||||
int DeepSleepPir = 0; // set to 1 to deepsleep between pir videos
|
||||
int record_on_reboot = 1; // set to 1 to record, or 0 to NOT record on reboot
|
||||
int PIRpin = 13; // for active high pir or microwave etc
|
||||
int PIRenabled = 0; // 1 is PIR is enable on reboot, will only work if you are not recording
|
||||
|
||||
int MagicNumber = 011; // change this if you are re-compiling and you dont want to use the ESPROM settings
|
||||
int stream_interval = 333; // milliseconds between frames delivered during the live stream - 333 is 3 fps
|
||||
|
||||
// here are 2 sets of startup parameters -- more down in the stop and restart webpage
|
||||
|
||||
// VGA 10 fps for 30 minutes, and repeat, play at real time
|
||||
|
||||
int framesize = 8; // 13 UXGA, 11 HD, 9 SVGA, 8 VGA, 6 CIF
|
||||
int repeat_config = 100; // repaeat same movie this many times
|
||||
int xspeed = 1; // playback speed - realtime is 1, or 300 means playpack 30 fps of frames at 10 second per frame ( 30 fps / 0.1 fps )
|
||||
int gray = 0; // not gray
|
||||
int quality = 12; // quality on the 10..50 subscale - 10 is good, 20 is grainy and smaller files, 12 is better in bright sunshine due to clipping
|
||||
int capture_interval = 100; // milli-seconds between frames
|
||||
volatile int total_frames_config = 18000; // how many frames - length of movie in ms is total_frames x capture_interval
|
||||
|
||||
|
||||
// UXGA 1 frame every 10 seconds for 60 minutes, and repeat, play at 30 fps or 300 times speed
|
||||
/*
|
||||
int framesize = 13; // 13 UXGA, 11 HD, 9 SVGA, 8 VGA, 6 CIF
|
||||
int repeat_config = 300; // repaeat same movie this many times
|
||||
int xspeed = 300; // playback speed - realtime is 1, or 300 means playpack 30 fps of frames at 10 second per frames ( 30 fps / 0.1 fps )
|
||||
int gray = 0; // not gray
|
||||
int quality = 6; // quality on the 10..50 subscale - 10 is good, 20 is grainy and smaller files, 12 is better in bright sunshine due to clipping
|
||||
int capture_interval = 10000; // milli-seconds between frames
|
||||
volatile int total_frames_config = 360; // how many frames - length of movie is total_frames x capture_interval
|
||||
*/
|
||||
|
||||
// enable the www.telegram.org BOT - it sends a text and and snapshot to you every time it starts a video
|
||||
// https://github.com/witnessmenow/Universal-Arduino-Telegram-Bot
|
||||
// I'm using the branch v1.2 from June 2020 - new master introduced late june, but not working for picture and captions, so my v1.2 mods included here
|
||||
// You need to create a bot, and get its number BOTtoken, and then get your telegram number -- all free at telegram.org
|
||||
// detailed instructions here https://randomnerdtutorials.com/telegram-control-esp32-esp8266-nodemcu-outputs/
|
||||
|
||||
RTC_DATA_ATTR int EnableBOT = 0;
|
||||
#define BOTtoken "9876543210:qwertyuiopasdfghjklzxcvbnmqwertyuio" // get your own bot and id at telegram.org
|
||||
#define BOTme "1234567890"
|
Ładowanie…
Reference in New Issue