Upload files to "/"

This commit is contained in:
avery.babka 2025-07-25 14:05:36 -05:00
parent 94f2c99da4
commit 7ed3e02fc5
5 changed files with 1062 additions and 0 deletions

368
FileLib.ino Normal file
View File

@ -0,0 +1,368 @@
#include "FileLib.h"
FileLibClass::FileLibClass(StorageType type) : _type(type) {}
FileLibClass::~FileLibClass()
{
_printMessage("Unmounting file system\n");
if (_type == StorageType::FL_SPIFFS)
{
LITTLEFS.end();
}
else if (_type == StorageType::FL_MMC)
{
SD_MMC.end();
}
else if (_type == StorageType::FL_SD)
{
SD.end();
}
}
bool FileLibClass::begin(uint8_t csPin, uint8_t misoPin, uint8_t mosiPin, uint8_t sckPin)
{
_printMessage("Initializing file system\n");
if (_type == StorageType::FL_SPIFFS)
{
_filesystem = &LITTLEFS;
return LITTLEFS.begin(true);
}
if (_type == StorageType::FL_MMC)
{
_filesystem = &SD_MMC;
return SD_MMC.begin();
}
if (_type == StorageType::FL_SD)
{
_spi.begin(sckPin, misoPin, mosiPin, csPin);
_filesystem = &SD;
return SD.begin(5, _spi);
}
return false;
}
File FileLibClass::openForWriting(const char *fileName)
{
_printMessage("Opening file '%s' for writing\n", fileName);
return _filesystem->open(fileName, FILE_WRITE);
}
File FileLibClass::open(const char *fileName, const char *mode)
{
if (!_filesystem)
{
_printMessage("File system not initialized");
return File(); // Return an empty File object if filesystem is not initialized
}
return _filesystem->open(fileName, mode);
}
bool FileLibClass::readPartial(const char *fileName, uint32_t startPos, uint8_t *buffer, uint16_t bufferSize)
{
if (!_filesystem)
{
_printMessage("File system not initialized");
return false;
}
File file = _filesystem->open(fileName, FILE_READ);
if (!file)
{
_printMessage("Failed to open file");
return false;
}
if (!file.seek(startPos))
{
_printMessage("Failed to seek to position %u in file", startPos);
file.close();
return false;
}
size_t bytesRead = file.read(buffer, bufferSize);
file.close();
if (bytesRead == 0)
{
_printMessage("No data read from file");
return false;
}
return true;
}
bool FileLibClass::write(const char *fileName, const uint8_t *data, size_t dataSize, bool append)
{
_printMessage("Writing %d bytes to file '%s'\n", dataSize, fileName);
File file = _filesystem->open(fileName, append ? FILE_APPEND : FILE_WRITE);
// Sanity check
if (!file || file.isDirectory())
{
_printMessage("Failed to open file '%s' for writing\n", fileName);
return false;
}
// size_t written = file.write(reinterpret_cast<const uint8_t *>(data), dataSize);
size_t written = 0;
for (size_t i = 0; i < dataSize; i++)
{
written += file.write(data[i]);
}
// Confirm that the data was written
if (written == dataSize)
{
file.close();
return true;
}
if (written > 0)
{
_printMessage("Only wrote %d bytes of %d\n", written, dataSize);
file.close();
return false;
}
_printMessage("Failed to write to file '%s'\n", fileName);
file.close();
return false;
}
bool FileLibClass::writeString(const char *fileName, const uint8_t *data, bool append)
{
_printMessage("Writing string to file '%s'\n", fileName);
// use strlen so we don't write the null terminator
int strLen = strlen((char *)&data);
return write(fileName, reinterpret_cast<const uint8_t *>(data), strLen, append);
}
void FileLibClass::listDir(const char *dirName, uint8_t levels)
{
if (!_verbose)
{
return;
}
_printMessage("Listing directory: %s\r\n", dirName);
File root = _filesystem->open(dirName);
if (!root)
{
_printMessage(" failed to open directory");
return;
}
if (!root.isDirectory())
{
_printMessage(" not a directory");
return;
}
File file = root.openNextFile();
while (file)
{
if (file.isDirectory())
{
Serial.printf(" DIR : %s\n", file.name());
if (levels)
{
listDir(file.name(), levels - 1);
}
}
else
{
Serial.printf(" FILE: %s", file.name());
Serial.printf("\tSIZE: %d\n", file.size());
}
file = root.openNextFile();
}
}
bool FileLibClass::read(const char *fileName, uint8_t *buffer, uint16_t bufferSize, bool clearBuffer)
{
_printMessage("Reading file '%s'\n", fileName);
if (clearBuffer)
{
memset(buffer, 0, bufferSize);
}
File file = _filesystem->open(fileName, FILE_READ);
uint16_t fileSize = file.size();
// Sanity check
if (!file || file.isDirectory())
{
_printMessage("Failed to open file '%s'.\n", fileName);
return false;
}
// check if the buffer is big enough
if (bufferSize < fileSize)
{
_printMessage("Buffer size is too small for file '%s'.\n", fileName);
file.close();
return false;
}
if (!file.available())
{
_printMessage("File '%s' is empty.\n", fileName);
}
// size_t bytesRead = file.readBytes(buffer, fileSize);
// read the data byte by byte
for (size_t i = 0; i < fileSize; i++)
{
buffer[i] = file.read();
}
file.close();
return true;
}
bool FileLibClass::remove(const char *fileName)
{
_printMessage("Removing file '%s'\n", fileName);
if (!_filesystem->exists(fileName))
{
_printMessage("File '%s' does not exist.\n", fileName);
return false;
}
return _filesystem->remove(fileName);
}
bool FileLibClass::rename(const char *fileName, const char *newName, bool overwrite)
{
_printMessage("Renaming file '%s' to '%s'\n", fileName, newName);
if (_filesystem->exists(newName) && !overwrite)
{
_printMessage("File '%s' already exists.\n", newName);
return false;
}
return _filesystem->rename(fileName, newName);
}
bool FileLibClass::mkdir(const char *path)
{
_printMessage("Creating directory '%s'\n", path);
if (_filesystem->exists(path))
{
_printMessage("Directory '%s' already exists.\n", path);
return false;
}
return _filesystem->mkdir(path);
}
uint64_t FileLibClass::getFreeSpace()
{
_printMessage("Getting free space\n");
if (_type == StorageType::FL_SPIFFS)
{
return LITTLEFS.totalBytes() - LITTLEFS.usedBytes();
}
if (_type == StorageType::FL_MMC)
{
uint64_t cardSize = SD_MMC.totalBytes();
uint64_t usedSpace = SD_MMC.usedBytes();
if (usedSpace > cardSize)
{
_printMessage("ERROR: Used space is greater than card size \n");
return 0;
}
uint64_t freeSpace = cardSize - usedSpace;
return freeSpace;
}
if (_type == StorageType::FL_SD)
{
uint64_t cardSize = SD.totalBytes();
uint64_t usedSpace = SD.usedBytes();
if (usedSpace > cardSize)
{
_printMessage("Error: Used space is greater than card size \n");
return 0;
}
uint64_t freeSpace = cardSize - usedSpace;
return freeSpace;
}
return 0;
}
bool copy(File &source, File &dest)
{
if (!source || !dest)
{
return false;
}
int bufferSize = 512;
char buffer[bufferSize];
while (source.available())
{
int bytesRead = source.readBytes(buffer, bufferSize);
int bytesWritten = dest.write((uint8_t *)&buffer, bytesRead);
if (bytesWritten != bytesRead)
{
return false;
}
}
dest.flush();
return true;
}
void FileLibClass::_printMessage(const char *message, ...)
{
if (_verbose)
{
// get the type name
const char *typeName;
switch (_type)
{
case FL_SPIFFS:
typeName = "FL_SPIFFS";
break;
case FL_SD:
typeName = "FL_SD";
break;
case FL_MMC:
typeName = "FL_MMC";
break;
default:
typeName = "Unknown";
break;
}
char buffer[256];
va_list args;
va_start(args, message);
vsnprintf(buffer, sizeof(buffer), message, args);
va_end(args);
// switch for different storage types
Serial.printf("%s: %s", typeName, buffer);
}
}
//Create FileLibClass::exists function
bool FileLibClass::exists(const char* fileName)
{
return _filesystem->exists(fileName);
}

27
GenericTypeDefs.h Normal file
View File

@ -0,0 +1,27 @@
#ifndef GENERIC_TYPE_DEFS_H
#define GENERIC_TYPE_DEFS_H
#include "Arduino.h"
typedef unsigned long dword;
typedef unsigned long uint32;
typedef union
{
uint32 val;
word w[2];
byte v[4];
struct
{
word lw;
word hw;
} wd;
struct
{
byte lb;
byte hb;
byte ub;
byte mb;
} bt;
} DWORD_VAL;
#endif // GENERIC_TYPE_DEFS_H

222
HTTPSHandler.h Normal file
View File

@ -0,0 +1,222 @@
#ifndef HTTPS_HANDLER_H
#define HTTPS_HANDLER_H
#include <WiFiClientSecure.h>
#include "SD_MMC.h"
/**
* @class HTTPSHandler
*
* This class provides functionality to handle HTTPS connections, including downloading,
* uploading, and sending post requests. It uses a state machine internally to track the
* status of downloads.
*/
class HTTPSHandler
{
public:
/**
* @enum HttpsState
*
* Enumerates the possible states for the https class.
*/
enum class HttpsState
{
IDLE, // Initial state, no download in progress.
CONNECTING, // Connecting to the server.
WAITING, // Waiting for server response.
COMPLETE, // Response is complete.
ERROR // An error occurred during the download.
};
/**
* @enum Type of http request
*/
enum class RequestType
{
GET,
DOWNLOAD,
POST,
UPLOAD
};
// A callback type that notifies about download progress.
using ProgressCallback = void (*)(int bytesDownloaded, int totalBytes);
private:
WiFiClientSecure _client;
// The server's certificate for SSL/TLS connection. Typically the root CA certificate.
const char *_serverCert;
// Must be a domain name, not an IP address.
const char *_host;
// Range between 0 and 65535.
int _port = 443;
// Duration in seconds.
uint8_t _downloadTimeout = 90;
// Duration in seconds.
uint8_t _responseTimeout = 100;
// Callback function to get download progress updates.
ProgressCallback _progressCallback = nullptr;
// Current state of the download.
HttpsState _currentState = HttpsState::IDLE;
// Type of http request.
RequestType _requestType;
// Boundaries for upload.
const String _BOUNDARY = "----WebKitFormBoundary7MA4YWxkTrZu0gW";
String _response;
// The File object where the downloaded data will be saved.
File _destinationFile;
// The path or endpoint on the server.
const char *_path;
// A header to be sent with the request.
String _customHeader;
/**
* Establish a connection to the server.
* @return true if connected successfully, false otherwise.
*/
bool _connect();
/**
* Internal method to get the content length from the server's response headers.
* @return The content length as an integer.
*/
int _getContentLength();
/**
* Processes the server's response for a given request.
* This method reads the available data from the client, appends it to
* the internal response string, and updates the current state
* of the HTTPSHandler based on the contents of the response.
*/
void _processResponse();
/**
* Process the download once initiated.
* @param dest The File object where the downloaded data will be saved.
*/
void _processDownload(File &dest);
/**
* Get only the file name from a given path.
*/
String _extractFileName(const String &fullPath);
/**
* Create boundary for multipart upload.
* Used in http request header.
*/
String _createBoundary(const String &fileNameToUpload);
public:
/**
* Constructor for the HTTPSHandler class.
* @param host The server's domain or IP address.
* @param port The port to connect on the server.
* @param serverCert The server's certificate for SSL/TLS connection.
*/
HTTPSHandler(const char *serverCert);
/**
* Destructor for the HTTPSHandler class.
*/
~HTTPSHandler();
/**
* Establish a connection to the server.
* @param host The server host to connect to.
* @param port The server port to connect to. Defaults to 443.
* @param serverCert The server certificate. Defaults to nullptr.
* @return true if connected successfully, false otherwise.
*/
bool connect(const char *host, int port = 443, const char *serverCert = nullptr);
/**
* Retrieves the server's response for the most recent request.
*
* @return A string containing the full response from the server.
*/
String getResponse() { return _response; }
/**
* Checks if the server's response for the most recent request is complete.
*
* @return True if the response is complete, otherwise false.
*/
bool isResponseComplete() { return _currentState == HttpsState::COMPLETE; }
/**
* Set a callback function to get download progress updates.
* @param callback A function pointer to the callback.
*/
void setProgressCallback(ProgressCallback callback) { _progressCallback = callback; }
/**
* Get the current download state.
* @return The current state of the download.
*/
HttpsState getState() const { return _currentState; }
/**
* Set a custom header for the next HTTP request.
* @param header The header string to set.
*/
void setCustomHeader(const String &header) { _customHeader = header; }
/**
* Initiate the download process.
* @param path The path or endpoint on the server from where to download.
*/
void getRequest(const char *path);
/**
* Upload a file to the server.
* @param path The path or endpoint on the server where to upload.
* @param source The File object containing the data to be uploaded.
* @param fileName The name that will be used for the uploaded file on the server.
* If not specified, the name of the source file will be used.
* @return true if the upload was successful, false otherwise.
*/
bool uploadFile(const char *path, File &source, const String &fileName = "");
/**
* Send a POST request to the server.
* @param path The path or endpoint on the server to send the POST request.
* @param postData The data to be sent in the POST request.
* @return true if the request was successful, false otherwise.
*/
bool postRequest(const char *path, const String &postData);
/**
* Set the target from a given path and destination file.
* @param path The path or endpoint on the server from where to download.
* @param dest The File object where the downloaded data will be saved.
*/
void setTarget(const char *sourcePath, File &dest);
/**
* Continue processing the state machine.
*/
void advanceStateMachine();
/**
* Check if an error occurred during the download.
* @return true if an error occurred, false otherwise.
*/
bool isErrorOccurred() { return _currentState == HttpsState::ERROR; }
/**
* Close the current connection with the server.
*/
void close();
/**
*
*/
// void get(const String &url, Callback callback);
};
#endif // HTTPS_HANDLER_H

367
HTTPSHandler.ino Normal file
View File

@ -0,0 +1,367 @@
#include "HTTPSHandler.h"
HTTPSHandler::HTTPSHandler(const char *serverCert)
{
_client.setCACert(serverCert);
}
bool HTTPSHandler::connect(const char *host, int port, const char *serverCert)
{
_host = host;
_port = port;
if (serverCert != nullptr)
{
_client.setCACert(serverCert);
}
_currentState = HttpsState::CONNECTING;
// reset response
_response = "";
return _connect();
}
bool HTTPSHandler::_connect()
{
return _client.connect(_host, _port);
}
int HTTPSHandler::_getContentLength()
{
unsigned long startTime = millis();
String line;
int contentLength = -1;
if (!_client.connected())
{
Serial.print("Not connected when getting content length");
_currentState = HttpsState::ERROR;
return -1;
}
while (contentLength == -1)
{
while (_client.available())
{
line = _client.readStringUntil('\n');
line.trim(); // Remove any trailing whitespace
if (line.startsWith("Content-Length: "))
{
contentLength = line.substring(15).toInt();
}
// Empty line / end of headers
if (line.length() == 0)
{
break;
}
}
// Check for timeout
if (millis() - startTime > _responseTimeout * 1000)
{
Serial.println("Timeout while trying to get Content-Length!");
_currentState = HttpsState::ERROR;
return -1;
}
}
return contentLength;
}
void HTTPSHandler::getRequest(const char *path)
{
// Check if the client is connected before sending
if (!_client.connected())
{
Serial.println("Error: Client is not connected!");
_currentState = HttpsState::ERROR;
return;
}
String request = "GET " + String(path) + " HTTP/1.1\r\n";
request += "Host: " + String(_host) + "\r\n";
if (_customHeader.length() > 0)
{
request += _customHeader + "\r\n";
}
request += "Connection: keep-alive\r\n\r\n";
size_t bytesSent = _client.print(request);
// Check if all bytes of the request were sent
if (bytesSent != request.length())
{
Serial.println("Error: Not all bytes sent!");
_currentState = HttpsState::ERROR;
return;
}
_currentState = HttpsState::WAITING;
_requestType = RequestType::GET;
}
void HTTPSHandler::_processDownload(File &dest)
{
const uint32_t BUFFER_SIZE = 2048;
int contentLength = _getContentLength();
static int bytesRead = 0; // Number of bytes downloaded so far
byte buffer[BUFFER_SIZE];
unsigned long startMillis = millis();
if (contentLength <= 0)
{
_currentState = HttpsState::ERROR;
}
while (_currentState == HttpsState::WAITING)
{
// Then proceed to download content
while (_client.available())
{
// If no data is received for the duration of _timeout, it times out
if (millis() - startMillis > _downloadTimeout * 1000)
{
Serial.println("Data inactivity timeout!");
_currentState = HttpsState::ERROR;
dest.close();
close();
return;
}
uint32_t bytesAvailable = _client.available();
int bytesToRead = min(bytesAvailable, BUFFER_SIZE);
int bytesReadFromClient = _client.read(buffer, bytesToRead);
dest.write(buffer, bytesReadFromClient);
bytesRead += bytesReadFromClient;
}
if (_progressCallback)
{
_progressCallback(bytesRead, contentLength);
}
if (bytesRead >= contentLength)
{
dest.close();
_currentState = HttpsState::COMPLETE;
close();
return;
}
}
}
String HTTPSHandler::_extractFileName(const String &fullPath)
{
int lastSlashIndex = fullPath.lastIndexOf('/');
if (lastSlashIndex == -1)
{
// No slashes found, return the full path itself.
return fullPath;
}
// Return the substring from the last slash onwards.
return fullPath.substring(lastSlashIndex + 1);
}
String HTTPSHandler::_createBoundary(const String &fileNameToUpload)
{
// Build the multipart form data request
String boundary = "--" + _BOUNDARY + "\r\n";
boundary += "Content-Disposition: form-data; name=\"fileToUpload\"; filename=\"" + fileNameToUpload + "\"\r\n";
boundary += "Content-Type: application/json\r\n";
boundary += "\r\n";
return boundary;
}
bool HTTPSHandler::uploadFile(const char *destPath, File &source, const String &fileName)
{
String fileNameToUpload = fileName == "" ? _extractFileName(source.name()) : fileName;
Serial.println("Uploading file: " + fileNameToUpload);
// Needed for header
String boundary = _createBoundary(fileNameToUpload);
String endBoundary = "\r\n--" + _BOUNDARY + "--\r\n";
// Calculate content length
int contentLength = boundary.length() + endBoundary.length() + source.size();
// Start sending the request
_client.println("POST " + String(destPath) + " HTTP/1.1");
_client.println("Host: appdig.com");
if (_customHeader.length() > 0)
{
_client.println(_customHeader);
}
_client.print("Content-Length: " + String(contentLength) + "\r\n");
_client.println("Content-Type: multipart/form-data; boundary=" + _BOUNDARY);
_client.println();
_client.print(boundary);
const int CHUNK_SIZE = 1024;
const long startTime = millis();
// Start sending body in chunks
while (source.available())
{
// Check for timeout
if (millis() - startTime > _responseTimeout * 1000)
{
Serial.println("Timeout while trying to get Content-Length!");
_currentState = HttpsState::ERROR;
return 0;
}
byte buffer[CHUNK_SIZE];
int bytesRead = source.read(buffer, CHUNK_SIZE);
_client.write(buffer, bytesRead);
if (_progressCallback)
{
_progressCallback(bytesRead, contentLength);
}
}
_client.print(endBoundary);
_currentState = HttpsState::COMPLETE;
// give the server some time to process the request
// update in the future to handle response from server
delay(10);
close();
return 1;
}
bool HTTPSHandler::postRequest(const char *destPath, const String &postData)
{
_client.println("POST " + String(destPath) + " HTTP/1.1");
_client.println("Host: " + String(_host));
_client.println("Content-Length: " + String(postData.length()));
_client.println("Connection: close");
_client.println();
_client.println(postData);
_currentState = HttpsState::WAITING;
_requestType = RequestType::POST;
return 1;
}
void HTTPSHandler::_processResponse()
{
unsigned long startMillis = millis();
// Read available data from the client and append to _response
while (_client.available())
{
// If no data is received for the duration of _timeout, it times out
if (millis() - startMillis > _responseTimeout * 1000)
{
Serial.println("Data inactivity timeout");
_currentState = HttpsState::ERROR;
close();
return;
}
char c = _client.read();
Serial.print(c);
_response += c;
if (_response.endsWith("\r\n0\r\n"))
{
break;
}
}
if (!_client.connected() || _response.endsWith("\r\n0\r\n"))
{
_currentState = HttpsState::COMPLETE;
}
close();
}
void HTTPSHandler::setTarget(const char *sourcePath, File &destFile)
{
_path = sourcePath;
_destinationFile = destFile;
_currentState = HttpsState::WAITING;
getRequest(_path);
_requestType = RequestType::DOWNLOAD;
}
void HTTPSHandler::advanceStateMachine()
{
static int lastState = -1;
if (lastState != static_cast<int>(_currentState))
{
Serial.println("HTTPSHandler state changed to " + static_cast<int>(_currentState));
lastState = static_cast<int>(_currentState);
}
switch (_currentState)
{
case HttpsState::CONNECTING:
if (!_client.connected())
{
Serial.println("Could not connect to server");
_currentState = HttpsState::ERROR;
}
_currentState = HttpsState::WAITING;
break;
case HttpsState::WAITING:
switch (_requestType)
{
case RequestType::DOWNLOAD:
_processDownload(_destinationFile);
break;
default:
_processResponse();
break;
}
break;
case HttpsState::ERROR:
_currentState = HttpsState::IDLE;
break;
case HttpsState::COMPLETE:
_currentState = HttpsState::IDLE;
break;
case HttpsState::IDLE:
break;
default:
break;
}
}
// void HTTPSHandler::get(const String &url, Callback callback)
// {
// if (!_client.connect(_host, _port))
// {
// Serial.println("Could not connect to server");
// return;
// }
// _client.println("GET / HTTP/1.1");
// _client.println("HOST appdig.com");
// _client.println("Connection: close");
// _requestStarted = 1;
// _callback = callback;
// }
void HTTPSHandler::close()
{
if (_client.connected())
{
_client.stop();
}
}
HTTPSHandler::~HTTPSHandler()
{
close();
}

78
IO_ADI.h Normal file
View File

@ -0,0 +1,78 @@
#ifndef IO_ADI
#define IO_ADI 1
#define IOSIZE 40
uint16_t io_array[IOSIZE];
#define BATTERYINPUTPIN 32 // 26 v996
#define SPAREOUTPIN 26 // 32 v996
// #define ADDR_PB 17
enum io
{
// Inputs
RET_LIMIT, // JOG_EXTEND, //0
EXT_LIMIT, // JOG_RETRACT,
OPEN_LIMIT,
ADDRBUTTON, // SPARE_IN2,
COMPRESSOR, // SPARE_IN3,
EN_12V_SW, // RF_KEYPAD,
RF_KEYPAD, // SPARE_IN1,
POWER_RLY, // ADDR_SET,//7
SPARE_IN, // GPIO 25
SPARE_OUT, // GPIO 32
BATT_V // GPIO 26
};
/*
//outputs
RETRACT,//8
EXTEND,
SPARE_RLYA, //was SCRN_DN
SPARE_RLYB, //was SCRN_UP
SPARE_OUT1,
EN_SWA,
EN_SWB,
EN_SWC,//15
COMPRESSOR,//16
ADDR_LED,
HI_TRIP_LED,
CHARGE_EN,
CHARGE_MODE,
SCREEN_UP,
SCREEN_DN,
SCREEN_STP,//23
spare1,//24
spare2,//25
spare3,//26
//ADC
BATT,//27
GND,
ADC4to20,
ADC0to10a,
ADC_CURRENT,
JOG_RETRACT,
JOG_EXTEND,
ADPAREADC3 //34
};
*/
// io Expander Support v125
// set up for 2 modules
uint8_t exRly[4];
uint8_t exOpto[4];
uint8_t exOptoTransition[4];
uint16_t exParm[12]; // 0-3 are input Hz 4-11 are 4 to 20 AD
uint8_t exIndx;
uint8_t exParmIndx;
#endif