diff --git a/interface/src/components/upload/SingleUpload.tsx b/interface/src/components/upload/SingleUpload.tsx index ccf8bfed3..46732ab46 100644 --- a/interface/src/components/upload/SingleUpload.tsx +++ b/interface/src/components/upload/SingleUpload.tsx @@ -35,7 +35,8 @@ const SingleUpload: FC = ({ onDrop, onCancel, uploading, prog onDrop, accept: { 'application/octet-stream': ['.bin'], - 'application/json': ['.json'] + 'application/json': ['.json'], + 'text/plain': ['.md5'] }, disabled: uploading, multiple: false diff --git a/interface/src/components/upload/useFileUpload.ts b/interface/src/components/upload/useFileUpload.ts index 05d4431fe..cc2f296d3 100644 --- a/interface/src/components/upload/useFileUpload.ts +++ b/interface/src/components/upload/useFileUpload.ts @@ -16,6 +16,7 @@ const useFileUpload = ({ upload }: MediaUploadOptions) => { const { enqueueSnackbar } = useSnackbar(); const [uploading, setUploading] = useState(false); + const [md5, setMd5] = useState(''); const [uploadProgress, setUploadProgress] = useState(); const [uploadCancelToken, setUploadCancelToken] = useState(); @@ -23,6 +24,7 @@ const useFileUpload = ({ upload }: MediaUploadOptions) => { setUploading(false); setUploadProgress(undefined); setUploadCancelToken(undefined); + setMd5(''); }; const cancelUpload = useCallback(() => { @@ -41,12 +43,17 @@ const useFileUpload = ({ upload }: MediaUploadOptions) => { const cancelToken = axios.CancelToken.source(); setUploadCancelToken(cancelToken); setUploading(true); - await upload(images[0], { + const response = await upload(images[0], { onUploadProgress: setUploadProgress, cancelToken: cancelToken.token }); resetUploadingStates(); - enqueueSnackbar(LL.UPLOAD() + ' ' + LL.SUCCESSFUL(), { variant: 'success' }); + if (response.status === 200) { + enqueueSnackbar(LL.UPLOAD() + ' ' + LL.SUCCESSFUL(), { variant: 'success' }); + } else if (response.status === 201) { + setMd5((String)(response.data)); + enqueueSnackbar(LL.UPLOAD() + ' MD5 ' + LL.SUCCESSFUL(), { variant: 'success' }); + } } catch (error) { if (axios.isCancel(error)) { enqueueSnackbar(LL.UPLOAD() + ' ' + LL.ABORTED(), { variant: 'warning' }); @@ -57,7 +64,7 @@ const useFileUpload = ({ upload }: MediaUploadOptions) => { } }; - return [uploadFile, cancelUpload, uploading, uploadProgress] as const; + return [uploadFile, cancelUpload, uploading, uploadProgress, md5] as const; }; export default useFileUpload; diff --git a/interface/src/framework/system/GeneralFileUpload.tsx b/interface/src/framework/system/GeneralFileUpload.tsx index 8d77657b8..f5aa03f50 100644 --- a/interface/src/framework/system/GeneralFileUpload.tsx +++ b/interface/src/framework/system/GeneralFileUpload.tsx @@ -4,6 +4,7 @@ import { AxiosPromise } from 'axios'; import { Typography, Button, Box } from '@mui/material'; import { FileUploadConfig } from '../../api/endpoints'; + import { SingleUpload, useFileUpload } from '../../components'; import DownloadIcon from '@mui/icons-material/GetApp'; @@ -21,7 +22,8 @@ interface UploadFileProps { } const GeneralFileUpload: FC = ({ uploadGeneralFile }) => { - const [uploadFile, cancelUpload, uploading, uploadProgress] = useFileUpload({ upload: uploadGeneralFile }); + + const [uploadFile, cancelUpload, uploading, uploadProgress, md5] = useFileUpload({ upload: uploadGeneralFile }); const { enqueueSnackbar } = useSnackbar(); @@ -80,6 +82,11 @@ const GeneralFileUpload: FC = ({ uploadGeneralFile }) => { )} + {md5 !== '' && ( + + {'MD5: ' + md5} + + )} {!uploading && ( diff --git a/interface/src/framework/system/UploadFileForm.tsx b/interface/src/framework/system/UploadFileForm.tsx index 5a5948217..4222a1508 100644 --- a/interface/src/framework/system/UploadFileForm.tsx +++ b/interface/src/framework/system/UploadFileForm.tsx @@ -16,7 +16,9 @@ const UploadFileForm: FC = () => { const uploadFile = useRef(async (file: File, config?: FileUploadConfig) => { const response = await SystemApi.uploadFile(file, config); - setRestarting(true); + if (response.status === 200) { + setRestarting(true); + } return response; }); diff --git a/interface/src/utils/endpoints.ts b/interface/src/utils/endpoints.ts index 2326991f2..3570797c5 100644 --- a/interface/src/utils/endpoints.ts +++ b/interface/src/utils/endpoints.ts @@ -1,6 +1,6 @@ export const extractErrorMessage = (error: any, defaultMessage: string) => { if (error.request) { - return defaultMessage + ' (' + error.request.statusText + ')'; + return defaultMessage + ' (' + error.request.status + ': ' + error.request.statusText + ')'; } else if (error instanceof Error) { return defaultMessage + ' (' + error.message + ')'; } diff --git a/lib/framework/UploadFileService.cpp b/lib/framework/UploadFileService.cpp index c78be7fda..e2d07d83b 100644 --- a/lib/framework/UploadFileService.cpp +++ b/lib/framework/UploadFileService.cpp @@ -3,6 +3,7 @@ using namespace std::placeholders; // for `_1` etc static bool is_firmware = false; +static char md5[33] = "\0"; UploadFileService::UploadFileService(AsyncWebServer * server, SecurityManager * securityManager) : _securityManager(securityManager) { @@ -33,45 +34,53 @@ void UploadFileService::handleUpload(AsyncWebServerRequest * request, const Stri Serial.println(); #endif + is_firmware = false; if ((extension == "bin") && (fsize > 1500000)) { is_firmware = true; } else if (extension == "json") { - is_firmware = false; + md5[0] = '\0'; // clear md5 + } else if (extension == "md5") { + if (len == 32) { + memcpy(md5, data, 32); + md5[32] = '\0'; + } + return; } else { - is_firmware = false; + md5[0] = '\0'; return; // not support file type } if (is_firmware) { // Check firmware header, 0xE9 magic offset 0 indicates esp bin, chip offset 12: esp32:0, S2:2, C3:5 #if CONFIG_IDF_TARGET_ESP32 // ESP32/PICO-D4 - bool isC3 = (fname.find("C3") != std::string::npos); - bool isS2 = (fname.find("S2") != std::string::npos); - if (isC3 || isS2 || (len > 12 && (data[0] != 0xE9 || data[12] != 0))) { + if (len > 12 && (data[0] != 0xE9 || data[12] != 0)) { handleError(request, 503); // service unavailable return; } #elif CONFIG_IDF_TARGET_ESP32S2 - bool isS2 = (fname.find("S2") != std::string::npos); - if (!isS2 || (len > 12 && (data[0] != 0xE9 || data[12] != 2))) { + if (len > 12 && (data[0] != 0xE9 || data[12] != 2)) { handleError(request, 503); // service unavailable return; } #elif CONFIG_IDF_TARGET_ESP32C3 - bool isC3 = (fname.find("C3") != std::string::npos); - if (!isC3 || (len > 12 && (data[0] != 0xE9 || data[12] != 5))) { + if (len > 12 && (data[0] != 0xE9 || data[12] != 5)) { handleError(request, 503); // service unavailable return; } #endif // it's firmware - initialize the ArduinoOTA updater if (Update.begin(fsize)) { + if (strlen(md5) == 32) { + Update.setMD5(md5); + md5[0] = '\0'; + } request->onDisconnect(UploadFileService::handleEarlyDisconnect); // success, let's make sure we end the update if the client hangs up } else { #if defined(EMSESP_USE_SERIAL) Update.printError(Serial); #endif handleError(request, 507); // failed to begin, send an error response Insufficient Storage + return; } } else { // its a normal file, open a new temp file to write the contents too @@ -83,7 +92,6 @@ void UploadFileService::handleUpload(AsyncWebServerRequest * request, const Stri if (len) { request->_tempFile.write(data, len); // stream the incoming chunk to the opened file } - } else { // if we haven't delt with an error, continue with the firmware update if (!request->_tempObject) { @@ -123,6 +131,11 @@ void UploadFileService::uploadComplete(AsyncWebServerRequest * request) { request->send(response); return; } + if (strlen(md5) == 32) { + AsyncWebServerResponse * response = request->beginResponse(201, "text/plain", md5); // created + request->send(response); + return; + } handleError(request, 403); // send the forbidden response } diff --git a/scripts/rename_fw.py b/scripts/rename_fw.py index f963c2795..1e53fbfe5 100644 --- a/scripts/rename_fw.py +++ b/scripts/rename_fw.py @@ -2,6 +2,7 @@ import shutil import re import os Import("env") +import hashlib OUTPUT_DIR = "build{}".format(os.path.sep) @@ -18,7 +19,6 @@ def bin_copy(source, target, env): bag[var] = m.group(1) app_version = bag.get('app_version') - platform = "ESP32" chip_target = env.get('PIOENV').upper() @@ -33,14 +33,13 @@ def bin_copy(source, target, env): # alternatively take platform from the pio target # platform = str(target[0]).split(os.path.sep)[2] - chip_target = env.get('PIOENV').upper() print("app version: " + app_version) print("platform: " + platform) print("chip_target: " + chip_target) # convert . to _ so Windows doesn't complain - variant = "EMS-ESP-" + chip_target + "-" + app_version.replace(".", "_") + variant = "EMS-ESP-" + app_version.replace(".", "_") + "-" + chip_target.replace("CI","ESP32") # check if output directories exist and create if necessary if not os.path.isdir(OUTPUT_DIR): @@ -52,15 +51,29 @@ def bin_copy(source, target, env): # create string with location and file names based on variant bin_file = "{}firmware{}{}.bin".format(OUTPUT_DIR, os.path.sep, variant) + md5_file = "{}firmware{}{}.md5".format(OUTPUT_DIR, os.path.sep, variant) # check if new target files exist and remove if necessary for f in [bin_file]: if os.path.isfile(f): os.remove(f) + # check if new target files exist and remove if necessary + for f in [md5_file]: + if os.path.isfile(f): + os.remove(f) + print("Renaming file to "+bin_file) # copy firmware.bin to firmware/.bin shutil.copy(str(target[0]), bin_file) + with open(bin_file,"rb") as f: + result = hashlib.md5(f.read()) + print("Calculating MD5: "+result.hexdigest()) + file1 = open(md5_file, 'w') + file1.write(result.hexdigest()) + file1.close() + env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", [bin_copy]) +env.AddPostAction("$BUILD_DIR/${PROGNAME}.md5", [bin_copy])