From 1f07dcdab231e4891e182a536ef06384b80e09bb Mon Sep 17 00:00:00 2001 From: rjwats Date: Mon, 29 Jun 2020 00:25:58 +0100 Subject: [PATCH] OTA Upload Feature (#162) * Improve restart behaviour under esp8266 * Backend to support firmware update over HTTP * UI for uploading new firmware * Documentation changes --- README.md | 18 ++-- features.ini | 1 + interface/package-lock.json | 23 +++++ interface/package.json | 1 + interface/src/ap/APSettingsForm.tsx | 2 +- interface/src/api/Endpoints.ts | 1 + .../src/authentication/Authentication.ts | 35 ++++++- interface/src/components/RestFormLoader.tsx | 2 +- interface/src/components/SingleUpload.tsx | 96 +++++++++++++++++++ interface/src/components/index.ts | 1 + interface/src/features/types.ts | 1 + interface/src/system/System.tsx | 10 +- .../src/system/UploadFirmwareController.tsx | 71 ++++++++++++++ interface/src/system/UploadFirmwareForm.tsx | 35 +++++++ interface/src/wifi/WiFiNetworkScanner.tsx | 4 +- lib/framework/ESP8266React.cpp | 3 + lib/framework/ESP8266React.h | 4 + lib/framework/FactoryResetService.cpp | 2 +- lib/framework/FactoryResetService.h | 1 + lib/framework/Features.h | 6 ++ lib/framework/FeaturesService.cpp | 5 + lib/framework/RestartService.cpp | 2 +- lib/framework/RestartService.h | 6 ++ lib/framework/UploadFirmwareService.cpp | 85 ++++++++++++++++ lib/framework/UploadFirmwareService.h | 38 ++++++++ 25 files changed, 437 insertions(+), 16 deletions(-) create mode 100644 interface/src/components/SingleUpload.tsx create mode 100644 interface/src/system/UploadFirmwareController.tsx create mode 100644 interface/src/system/UploadFirmwareForm.tsx create mode 100644 lib/framework/UploadFirmwareService.cpp create mode 100644 lib/framework/UploadFirmwareService.h diff --git a/README.md b/README.md index ddf0756..e40168d 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Provides many of the features required for IoT projects: * Configurable Access Point - Can be continuous or automatically enabled when WiFi connection fails * Network Time - Synchronization with NTP * MQTT - Connection to an MQTT broker for automation and monitoring -* Remote Firmware Updates - Enable secured OTA updates +* Remote Firmware Updates - Firmware replacement using OTA update or upload via UI * Security - Protected RESTful endpoints and a secured user interface Features may be [enabled or disabled](#selecting-features) as required at compile time. @@ -174,15 +174,17 @@ Customize the settings as you see fit. A value of 0 will disable the specified f -D FT_MQTT=1 -D FT_NTP=1 -D FT_OTA=1 + -D FT_UPLOAD_FIRMWARE=1 ``` -Flag | Description -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- -FT_PROJECT | Controls whether the "project" section of the UI is enabled. Disable this if you don't intend to have your own screens in the UI. -FT_SECURITY | Controls whether the [security features](#security-features) are enabled. Disabling this means you won't need to authenticate to access the device and all authentication predicates will be bypassed. -FT_MQTT | Controls whether the MQTT features are enabled. Disable this if your project does not require MQTT support. -FT_NTP | Controls whether network time protocol synchronization features are enabled. Disable this if your project does not require accurate time. -FT_OTA | Controls whether OTA update support is enabled. Disable this if you won't be using the remote update feature. +Flag | Description +------------------ | ---------------------------------------------- +FT_PROJECT | Controls whether the "project" section of the UI is enabled. Disable this if you don't intend to have your own screens in the UI. +FT_SECURITY | Controls whether the [security features](#security-features) are enabled. Disabling this means you won't need to authenticate to access the device and all authentication predicates will be bypassed. +FT_MQTT | Controls whether the MQTT features are enabled. Disable this if your project does not require MQTT support. +FT_NTP | Controls whether network time protocol synchronization features are enabled. Disable this if your project does not require accurate time. +FT_OTA | Controls whether OTA update support is enabled. Disable this if you won't be using the remote update feature. +FT_UPLOAD_FIRMWARE | Controls the whether the manual upload firmware feature is enabled. Disable this if you won't be manually uploading firmware. ## Factory settings diff --git a/features.ini b/features.ini index e5a5078..f68b0ae 100644 --- a/features.ini +++ b/features.ini @@ -5,3 +5,4 @@ build_flags = -D FT_MQTT=1 -D FT_NTP=1 -D FT_OTA=1 + -D FT_UPLOAD_FIRMWARE=1 diff --git a/interface/package-lock.json b/interface/package-lock.json index fb4c79a..deeaa5b 100644 --- a/interface/package-lock.json +++ b/interface/package-lock.json @@ -2311,6 +2311,11 @@ "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" }, + "attr-accept": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.1.0.tgz", + "integrity": "sha512-sLzVM3zCCmmDtDNhI0i96k6PUztkotSOXqE4kDGQt/6iDi5M+H0srjeF+QC6jN581l4X/Zq3Zu/tgcErEssavg==" + }, "autoprefixer": { "version": "9.7.5", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.7.5.tgz", @@ -5587,6 +5592,14 @@ "schema-utils": "^2.5.0" } }, + "file-selector": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.1.12.tgz", + "integrity": "sha512-Kx7RTzxyQipHuiqyZGf+Nz4vY9R1XGxuQl/hLoJwq+J4avk/9wxxgZyHKtbyIPJmbD4A66DWGYfyykWNpcYutQ==", + "requires": { + "tslib": "^1.9.0" + } + }, "filesize": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.0.1.tgz", @@ -10540,6 +10553,16 @@ "scheduler": "^0.19.1" } }, + "react-dropzone": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-11.0.1.tgz", + "integrity": "sha512-x/6wqRHaR8jsrNiu/boVMIPYuoxb83Vyfv77hO7/3ZRn8Pr+KH5onsCsB8MLBa3zdJl410C5FXPUINbu16XIzw==", + "requires": { + "attr-accept": "^2.0.0", + "file-selector": "^0.1.12", + "prop-types": "^15.7.2" + } + }, "react-error-overlay": { "version": "6.0.7", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.7.tgz", diff --git a/interface/package.json b/interface/package.json index 49e381c..33331e9 100644 --- a/interface/package.json +++ b/interface/package.json @@ -21,6 +21,7 @@ "notistack": "^0.9.16", "react": "^16.13.1", "react-dom": "^16.13.1", + "react-dropzone": "^11.0.1", "react-form-validator-core": "^0.6.4", "react-material-ui-form-validator": "^2.0.10", "react-router": "^5.1.2", diff --git a/interface/src/ap/APSettingsForm.tsx b/interface/src/ap/APSettingsForm.tsx index 40f4093..1973037 100644 --- a/interface/src/ap/APSettingsForm.tsx +++ b/interface/src/ap/APSettingsForm.tsx @@ -18,7 +18,7 @@ class APSettingsForm extends React.Component { return ( ) => void): Promise { + return new Promise((resolve, reject) => { + xhr.open("POST", url, true); + const accessToken = getStorage().getItem(ACCESS_TOKEN); + if (accessToken) { + xhr.withCredentials = true; + xhr.setRequestHeader("Authorization", 'Bearer ' + accessToken); + } + xhr.upload.onprogress = onProgress; + xhr.onload = function () { + if (xhr.status === 401 || xhr.status === 403) { + history.push("/unauthorized"); + } else { + resolve(); + } + }; + xhr.onerror = function (event: ProgressEvent) { + reject(new DOMException('Error', 'UploadError')); + }; + xhr.onabort = function () { + reject(new DOMException('Aborted', 'AbortError')); + }; + const formData = new FormData(); + formData.append('file', file); + xhr.send(formData); + }); +} + /** * Wraps the normal fetch routene which redirects on 401 response. */ export function redirectingAuthorizedFetch(url: RequestInfo, params?: RequestInit): Promise { return new Promise((resolve, reject) => { authorizedFetch(url, params).then(response => { - if (response.status === 401) { + if (response.status === 401 || response.status === 403) { history.push("/unauthorized"); } else { resolve(response); diff --git a/interface/src/components/RestFormLoader.tsx b/interface/src/components/RestFormLoader.tsx index 946e806..ffd558b 100644 --- a/interface/src/components/RestFormLoader.tsx +++ b/interface/src/components/RestFormLoader.tsx @@ -35,7 +35,7 @@ export default function RestFormLoader(props: RestFormLoaderProps) {
- Loading... + Loading…
); diff --git a/interface/src/components/SingleUpload.tsx b/interface/src/components/SingleUpload.tsx new file mode 100644 index 0000000..2dd7bfa --- /dev/null +++ b/interface/src/components/SingleUpload.tsx @@ -0,0 +1,96 @@ +import React, { FC, Fragment } from 'react'; +import { useDropzone, DropzoneState } from 'react-dropzone'; + +import { makeStyles, createStyles } from '@material-ui/styles'; +import CloudUploadIcon from '@material-ui/icons/CloudUpload'; +import CancelIcon from '@material-ui/icons/Cancel'; +import { Theme, Box, Typography, LinearProgress, Button } from '@material-ui/core'; + +interface SingleUploadStyleProps extends DropzoneState { + uploading: boolean; +} + +const progressPercentage = (progress: ProgressEvent) => Math.round((progress.loaded * 100) / progress.total); + +const getBorderColor = (theme: Theme, props: SingleUploadStyleProps) => { + if (props.isDragAccept) { + return theme.palette.success.main; + } + if (props.isDragReject) { + return theme.palette.error.main; + } + if (props.isDragActive) { + return theme.palette.info.main; + } + return theme.palette.grey[700]; +} + +const useStyles = makeStyles((theme: Theme) => createStyles({ + dropzone: { + padding: theme.spacing(8, 2), + borderWidth: 2, + borderRadius: 2, + borderStyle: 'dashed', + color: theme.palette.grey[700], + transition: 'border .24s ease-in-out', + cursor: (props: SingleUploadStyleProps) => props.uploading ? 'default' : 'pointer', + width: '100%', + borderColor: (props: SingleUploadStyleProps) => getBorderColor(theme, props) + } +})); + +export interface SingleUploadProps { + onDrop: (acceptedFiles: File[]) => void; + onCancel: () => void; + accept?: string | string[]; + uploading: boolean; + progress?: ProgressEvent; +} + +const SingleUpload: FC = ({ onDrop, onCancel, accept, uploading, progress }) => { + const dropzoneState = useDropzone({ onDrop, accept, disabled: uploading, multiple: false }); + const { getRootProps, getInputProps } = dropzoneState; + const classes = useStyles({ ...dropzoneState, uploading }); + + + const renderProgressText = () => { + if (uploading) { + if (progress?.lengthComputable) { + return `Uploading: ${progressPercentage(progress)}%`; + } + return "Uploading\u2026"; + } + return "Drop file here or click to browse"; + } + + const renderProgress = (progress?: ProgressEvent) => ( + + ); + + return ( +
+ + + + + {renderProgressText()} + + {uploading && ( + + + {renderProgress(progress)} + + + + )} + +
+ ); +} + +export default SingleUpload; diff --git a/interface/src/components/index.ts b/interface/src/components/index.ts index 8d712d8..4a6cc6a 100644 --- a/interface/src/components/index.ts +++ b/interface/src/components/index.ts @@ -8,6 +8,7 @@ export { default as RestFormLoader } from './RestFormLoader'; export { default as SectionContent } from './SectionContent'; export { default as WebSocketFormLoader } from './WebSocketFormLoader'; export { default as ErrorButton } from './ErrorButton'; +export { default as SingleUpload } from './SingleUpload'; export * from './RestFormLoader'; export * from './RestController'; diff --git a/interface/src/features/types.ts b/interface/src/features/types.ts index 08f5aaf..1753d9a 100644 --- a/interface/src/features/types.ts +++ b/interface/src/features/types.ts @@ -4,4 +4,5 @@ export interface Features { mqtt: boolean; ntp: boolean; ota: boolean; + upload_firmware: boolean; } diff --git a/interface/src/system/System.tsx b/interface/src/system/System.tsx index 30f63e1..671d5e0 100644 --- a/interface/src/system/System.tsx +++ b/interface/src/system/System.tsx @@ -3,12 +3,14 @@ import { Redirect, Switch, RouteComponentProps } from 'react-router-dom' import { Tabs, Tab } from '@material-ui/core'; +import { WithFeaturesProps, withFeatures } from '../features/FeaturesContext'; + import { withAuthenticatedContext, AuthenticatedContextProps, AuthenticatedRoute } from '../authentication'; import { MenuAppBar } from '../components'; import SystemStatusController from './SystemStatusController'; import OTASettingsController from './OTASettingsController'; -import { WithFeaturesProps, withFeatures } from '../features/FeaturesContext'; +import UploadFirmwareController from './UploadFirmwareController'; type SystemProps = AuthenticatedContextProps & RouteComponentProps & WithFeaturesProps; @@ -27,12 +29,18 @@ class System extends Component { {features.ota && ( )} + {features.upload_firmware && ( + + )} {features.ota && ( )} + {features.upload_firmware && ( + + )} diff --git a/interface/src/system/UploadFirmwareController.tsx b/interface/src/system/UploadFirmwareController.tsx new file mode 100644 index 0000000..16d21ff --- /dev/null +++ b/interface/src/system/UploadFirmwareController.tsx @@ -0,0 +1,71 @@ +import React, { Component } from 'react'; + +import { SectionContent } from '../components'; +import { UPLOAD_FIRMWARE_ENDPOINT } from '../api'; + +import UploadFirmwareForm from './UploadFirmwareForm'; +import { redirectingAuthorizedUpload } from '../authentication'; +import { withSnackbar, WithSnackbarProps } from 'notistack'; + +interface UploadFirmwareControllerState { + xhr?: XMLHttpRequest; + progress?: ProgressEvent; +} + +class UploadFirmwareController extends Component { + + state: UploadFirmwareControllerState = { + xhr: undefined, + progress: undefined + }; + + componentWillUnmount() { + this.state.xhr?.abort(); + } + + updateProgress = (progress: ProgressEvent) => { + this.setState({ progress }); + } + + uploadFile = (file: File) => { + if (this.state.xhr) { + return; + } + var xhr = new XMLHttpRequest(); + this.setState({ xhr }); + redirectingAuthorizedUpload(xhr, UPLOAD_FIRMWARE_ENDPOINT, file, this.updateProgress).then(() => { + if (xhr.status !== 200) { + throw Error("Invalid status code: " + xhr.status); + } + this.props.enqueueSnackbar("Activating new firmware", { variant: 'success' }); + this.setState({ xhr: undefined, progress: undefined }); + }).catch((error: Error) => { + if (error.name === 'AbortError') { + this.props.enqueueSnackbar("Upload cancelled by user", { variant: 'warning' }); + } else { + const errorMessage = error.name === 'UploadError' ? "Error during upload" : (error.message || "Unknown error"); + this.props.enqueueSnackbar("Problem uploading: " + errorMessage, { variant: 'error' }); + this.setState({ xhr: undefined, progress: undefined }); + } + }); + } + + cancelUpload = () => { + if (this.state.xhr) { + this.state.xhr.abort(); + this.setState({ xhr: undefined, progress: undefined }); + } + } + + render() { + const { xhr, progress } = this.state; + return ( + + + + ); + } + +} + +export default withSnackbar(UploadFirmwareController); diff --git a/interface/src/system/UploadFirmwareForm.tsx b/interface/src/system/UploadFirmwareForm.tsx new file mode 100644 index 0000000..2c40be3 --- /dev/null +++ b/interface/src/system/UploadFirmwareForm.tsx @@ -0,0 +1,35 @@ +import React, { Fragment } from 'react'; +import { SingleUpload } from '../components'; +import { Box } from '@material-ui/core'; + +interface UploadFirmwareFormProps { + uploading: boolean; + progress?: ProgressEvent; + onFileSelected: (file: File) => void; + onCancel: () => void; +} + +class UploadFirmwareForm extends React.Component { + + handleDrop = (files: File[]) => { + const file = files[0]; + if (file) { + this.props.onFileSelected(files[0]); + } + }; + + render() { + const { uploading, progress, onCancel } = this.props; + return ( + + + Upload a new firmware (.bin) file below to replace the existing firmware. + + + + ); + } + +} + +export default UploadFirmwareForm; diff --git a/interface/src/wifi/WiFiNetworkScanner.tsx b/interface/src/wifi/WiFiNetworkScanner.tsx index c1be649..744f515 100644 --- a/interface/src/wifi/WiFiNetworkScanner.tsx +++ b/interface/src/wifi/WiFiNetworkScanner.tsx @@ -130,7 +130,7 @@ class WiFiNetworkScanner extends Component - Scanning... + Scanning… ); @@ -156,7 +156,7 @@ class WiFiNetworkScanner extends Component } variant="contained" color="secondary" onClick={this.requestNetworkScan} disabled={scanningForNetworks}> - Scan again... + Scan again… diff --git a/lib/framework/ESP8266React.cpp b/lib/framework/ESP8266React.cpp index 2ccd3cf..b4f8f4c 100644 --- a/lib/framework/ESP8266React.cpp +++ b/lib/framework/ESP8266React.cpp @@ -15,6 +15,9 @@ ESP8266React::ESP8266React(AsyncWebServer* server, FS* fs) : #if FT_ENABLED(FT_OTA) _otaSettingsService(server, fs, &_securitySettingsService), #endif +#if FT_ENABLED(FT_UPLOAD_FIRMWARE) + _uploadFirmwareService(server, &_securitySettingsService), +#endif #if FT_ENABLED(FT_MQTT) _mqttSettingsService(server, fs, &_securitySettingsService), _mqttStatus(server, &_mqttSettingsService, &_securitySettingsService), diff --git a/lib/framework/ESP8266React.h b/lib/framework/ESP8266React.h index 193d31c..1bb23a7 100644 --- a/lib/framework/ESP8266React.h +++ b/lib/framework/ESP8266React.h @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -98,6 +99,9 @@ class ESP8266React { #if FT_ENABLED(FT_OTA) OTASettingsService _otaSettingsService; #endif +#if FT_ENABLED(FT_UPLOAD_FIRMWARE) + UploadFirmwareService _uploadFirmwareService; +#endif #if FT_ENABLED(FT_MQTT) MqttSettingsService _mqttSettingsService; MqttStatus _mqttStatus; diff --git a/lib/framework/FactoryResetService.cpp b/lib/framework/FactoryResetService.cpp index 4f45564..c3ecd9e 100644 --- a/lib/framework/FactoryResetService.cpp +++ b/lib/framework/FactoryResetService.cpp @@ -30,5 +30,5 @@ void FactoryResetService::factoryReset() { fs->remove(configDirectory.fileName()); } #endif - ESP.restart(); + RestartService::restartNow(); } diff --git a/lib/framework/FactoryResetService.h b/lib/framework/FactoryResetService.h index 81a968e..2336e6f 100644 --- a/lib/framework/FactoryResetService.h +++ b/lib/framework/FactoryResetService.h @@ -11,6 +11,7 @@ #include #include +#include #include #define FS_CONFIG_DIRECTORY "/config" diff --git a/lib/framework/Features.h b/lib/framework/Features.h index 4c16534..2de82c5 100644 --- a/lib/framework/Features.h +++ b/lib/framework/Features.h @@ -28,4 +28,10 @@ #define FT_OTA 1 #endif +// upload firmware feature off by default +#ifndef FT_UPLOAD_FIRMWARE +#define FT_UPLOAD_FIRMWARE 0 +#endif + + #endif diff --git a/lib/framework/FeaturesService.cpp b/lib/framework/FeaturesService.cpp index cf1033f..095f1f2 100644 --- a/lib/framework/FeaturesService.cpp +++ b/lib/framework/FeaturesService.cpp @@ -31,6 +31,11 @@ void FeaturesService::features(AsyncWebServerRequest* request) { root["ota"] = true; #else root["ota"] = false; +#endif +#if FT_ENABLED(FT_UPLOAD_FIRMWARE) + root["upload_firmware"] = true; +#else + root["upload_firmware"] = false; #endif response->setLength(); request->send(response); diff --git a/lib/framework/RestartService.cpp b/lib/framework/RestartService.cpp index 3eb0df8..9036e40 100644 --- a/lib/framework/RestartService.cpp +++ b/lib/framework/RestartService.cpp @@ -8,6 +8,6 @@ RestartService::RestartService(AsyncWebServer* server, SecurityManager* security } void RestartService::restart(AsyncWebServerRequest* request) { - request->onDisconnect([]() { ESP.restart(); }); + request->onDisconnect(RestartService::restartNow); request->send(200); } diff --git a/lib/framework/RestartService.h b/lib/framework/RestartService.h index 0703881..45a1008 100644 --- a/lib/framework/RestartService.h +++ b/lib/framework/RestartService.h @@ -18,6 +18,12 @@ class RestartService { public: RestartService(AsyncWebServer* server, SecurityManager* securityManager); + static void restartNow() { + WiFi.disconnect(true); + delay(500); + ESP.restart(); + } + private: void restart(AsyncWebServerRequest* request); }; diff --git a/lib/framework/UploadFirmwareService.cpp b/lib/framework/UploadFirmwareService.cpp new file mode 100644 index 0000000..1858ace --- /dev/null +++ b/lib/framework/UploadFirmwareService.cpp @@ -0,0 +1,85 @@ +#include + +UploadFirmwareService::UploadFirmwareService(AsyncWebServer* server, SecurityManager* securityManager) : + _securityManager(securityManager) { + server->on(UPLOAD_FIRMWARE_PATH, + HTTP_POST, + std::bind(&UploadFirmwareService::uploadComplete, this, std::placeholders::_1), + std::bind(&UploadFirmwareService::handleUpload, + this, + std::placeholders::_1, + std::placeholders::_2, + std::placeholders::_3, + std::placeholders::_4, + std::placeholders::_5, + std::placeholders::_6)); +#ifdef ESP8266 + Update.runAsync(true); +#endif +} + +void UploadFirmwareService::handleUpload(AsyncWebServerRequest* request, + const String& filename, + size_t index, + uint8_t* data, + size_t len, + bool final) { + if (!index) { + Authentication authentication = _securityManager->authenticateRequest(request); + if (AuthenticationPredicates::IS_ADMIN(authentication)) { + if (Update.begin(request->contentLength())) { + // success, let's make sure we end the update if the client hangs up + request->onDisconnect(UploadFirmwareService::handleEarlyDisconnect); + } else { + // failed to begin, send an error response + Update.printError(Serial); + handleError(request, 500); + } + } else { + // send the forbidden response + handleError(request, 403); + } + } + + // if we haven't delt with an error, continue with the update + if (!request->_tempObject) { + if (Update.write(data, len) != len) { + Update.printError(Serial); + handleError(request, 500); + } + if (final) { + if (!Update.end(true)) { + Update.printError(Serial); + handleError(request, 500); + } + } + } +} + +void UploadFirmwareService::uploadComplete(AsyncWebServerRequest* request) { + // if no error, send the success response + if (!request->_tempObject) { + request->onDisconnect(RestartService::restartNow); + AsyncWebServerResponse* response = request->beginResponse(200); + request->send(response); + } +} + +void UploadFirmwareService::handleError(AsyncWebServerRequest* request, int code) { + // if we have had an error already, do nothing + if (request->_tempObject) { + return; + } + // send the error code to the client and record the error code in the temp object + request->_tempObject = new int(code); + AsyncWebServerResponse* response = request->beginResponse(code); + request->send(response); +} + +void UploadFirmwareService::handleEarlyDisconnect() { +#ifdef ESP32 + Update.abort(); +#elif defined(ESP8266) + Update.end(); +#endif +} diff --git a/lib/framework/UploadFirmwareService.h b/lib/framework/UploadFirmwareService.h new file mode 100644 index 0000000..6312af1 --- /dev/null +++ b/lib/framework/UploadFirmwareService.h @@ -0,0 +1,38 @@ +#ifndef UploadFirmwareService_h +#define UploadFirmwareService_h + +#include + +#ifdef ESP32 +#include +#include +#include +#elif defined(ESP8266) +#include +#include +#endif + +#include +#include +#include + +#define UPLOAD_FIRMWARE_PATH "/rest/uploadFirmware" + +class UploadFirmwareService { + public: + UploadFirmwareService(AsyncWebServer* server, SecurityManager* securityManager); + + private: + SecurityManager* _securityManager; + void handleUpload(AsyncWebServerRequest* request, + const String& filename, + size_t index, + uint8_t* data, + size_t len, + bool final); + void uploadComplete(AsyncWebServerRequest* request); + void handleError(AsyncWebServerRequest* request, int code); + static void handleEarlyDisconnect(); +}; + +#endif // end UploadFirmwareService_h