From 631987c21b2ea0da09e8e8426f19b18953526714 Mon Sep 17 00:00:00 2001
From: Joacim Breiler
Date: Mon, 4 Nov 2024 14:10:23 +0100
Subject: [PATCH] Add initial version for uploading additional files during
install
---
src/components/editormodal/EditorModal.tsx | 2 +-
src/components/fields/BooleanField.tsx | 29 ++++----
src/components/fields/PinField.tsx | 8 +-
src/components/fields/SelectField.tsx | 9 +--
src/components/fields/TextField.tsx | 6 +-
src/components/fields/UnknownField.tsx | 4 +-
src/components/installermodal/ConfirmPage.tsx | 51 ++++++++-----
.../installermodal/InstallerModal.tsx | 45 ++++++++++-
src/components/tooltip/ToolTip.tsx | 27 +++++++
src/pages/connection/Connection.tsx | 37 +++++++---
src/pages/wifisettings/WiFiSettings.tsx | 4 +-
src/panels/firmware/Firmware.tsx | 16 +++-
src/services/GitHubService.ts | 28 +++++--
src/services/InstallService.ts | 74 +++++++++++--------
.../commands/GetAccessPointListCommand.ts | 4 +-
src/utils/flash.ts | 14 +++-
16 files changed, 252 insertions(+), 106 deletions(-)
create mode 100644 src/components/tooltip/ToolTip.tsx
diff --git a/src/components/editormodal/EditorModal.tsx b/src/components/editormodal/EditorModal.tsx
index 467b7f4..ed53953 100644
--- a/src/components/editormodal/EditorModal.tsx
+++ b/src/components/editormodal/EditorModal.tsx
@@ -26,7 +26,7 @@ const EditorModal = ({ file, fileData, onClose, onSave }: EditorModalProps) => {
const [tab, setTab] = useState(ConfigurationTab.GENERAL);
return (
-
+
{
return (
-
- {label}
+
+ {label} {helpText}
-
-
- setValue(Boolean(event.target.checked))
- }
- checked={Boolean(value)}
- />
- {helpText && {helpText} }
+
+
+
+ setValue(Boolean(event.target.checked))
+ }
+ checked={Boolean(value)}
+ />
+
);
diff --git a/src/components/fields/PinField.tsx b/src/components/fields/PinField.tsx
index b1e3b99..557caa7 100644
--- a/src/components/fields/PinField.tsx
+++ b/src/components/fields/PinField.tsx
@@ -5,6 +5,7 @@ import { Board } from "../../model/Boards";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faWarning } from "@fortawesome/free-solid-svg-icons";
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
+import ToolTip from "../tooltip/ToolTip";
type SelectFieldProps = {
label?: string;
@@ -56,10 +57,10 @@ const PinField = ({
return (
-
- {label}
+
+ {label} {helpText}
-
+
)}
- {helpText && {helpText} }
{hasConflict && (
diff --git a/src/components/fields/SelectField.tsx b/src/components/fields/SelectField.tsx
index f53b869..9c8bcab 100644
--- a/src/components/fields/SelectField.tsx
+++ b/src/components/fields/SelectField.tsx
@@ -1,5 +1,6 @@
import React, { ReactElement } from "react";
import { Col, Form, InputGroup, Row } from "react-bootstrap";
+import ToolTip from "../tooltip/ToolTip";
type Option = {
name: string;
@@ -29,10 +30,10 @@ const SelectField = ({
}: SelectFieldProps) => {
return (
-
- {label}
+
+ {label} {helpText}
-
+
{groupedControls && groupedControls}
-
- {helpText && {helpText} }
);
diff --git a/src/components/fields/TextField.tsx b/src/components/fields/TextField.tsx
index 7000df7..71a5be5 100644
--- a/src/components/fields/TextField.tsx
+++ b/src/components/fields/TextField.tsx
@@ -1,6 +1,7 @@
import React from "react";
import { Col, Form, InputGroup, Row } from "react-bootstrap";
import { ReactElement } from "react-markdown/lib/react-markdown";
+import ToolTip from "../tooltip/ToolTip";
type TextFieldProps = {
label?: string;
@@ -31,8 +32,8 @@ const TextField = ({
}: TextFieldProps) => {
return (
-
- {label}
+
+ {label} {helpText}
@@ -53,7 +54,6 @@ const TextField = ({
{unit && {unit} }
{groupedControls && groupedControls}
- {helpText && {helpText} }
);
diff --git a/src/components/fields/UnknownField.tsx b/src/components/fields/UnknownField.tsx
index e76b435..791538e 100644
--- a/src/components/fields/UnknownField.tsx
+++ b/src/components/fields/UnknownField.tsx
@@ -36,10 +36,10 @@ const UnknownField = ({
return (
{label}
-
+
{label}
-
+
void;
- onInstall: (baud: number) => Promise;
+ onInstall: (baud: number, files: string[]) => Promise;
release: GithubRelease;
manifest: GithubReleaseManifest;
choice: FirmwareChoice;
};
-const ConfirmPage = ({ release, choice, onInstall, onCancel }: Props) => {
+const ConfirmPage = ({
+ release,
+ choice,
+ manifest,
+ onInstall,
+ onCancel
+}: Props) => {
const [baud, setBaud] = useLocalStorage("baud", "921600");
+ const [files, setFiles] = useState([]);
return (
<>
@@ -34,23 +42,12 @@ const ConfirmPage = ({ release, choice, onInstall, onCancel }: Props) => {
- setBaud(value)}
- >
-
- {/*manifest.files &&
+ {manifest.files && Extra Options }
+ {manifest.files &&
Object.keys(manifest.files).map((key) => {
return (
{
if (value) {
setFiles((files) => [
@@ -67,14 +64,30 @@ const ConfirmPage = ({ release, choice, onInstall, onCancel }: Props) => {
value={files.includes(key)}
/>
);
- })*/}
+ })}
+
+
+
+ setBaud(value)}
+ helpText="Some controllers need to be installed at a lower speed. If you are experiencing problems you can try and decrease the speed."
+ >
Cancel
- onInstall(+baud)}>
+ onInstall(+baud, files)}>
{
+ const onInstall = async (baud: number, files: string[]) => {
try {
await controllerService?.disconnect(false);
} catch (error) {
@@ -85,6 +88,33 @@ const InstallerModal = ({
setState(InstallerState.ERROR);
}
+ if (!hasErrors) {
+ setState(InstallerState.UPLOADING_FILES);
+ }
+
+ await Promise.all(
+ files.map((file) => {
+ const extraFile = manifest.files?.[file] as FirmwareFile;
+
+ if (!extraFile) {
+ return;
+ }
+
+ console.log("Uploading: ", extraFile);
+ return GithubService.getExtraFile(release, extraFile)
+ .then((data) => {
+ validateSignature(extraFile.signature, data);
+ return data;
+ })
+ .then((data) =>
+ controllerService?.uploadFile(
+ extraFile["controller-path"],
+ Buffer.from(data)
+ )
+ );
+ })
+ );
+
if (!hasErrors) {
setState(InstallerState.DONE);
}
@@ -146,6 +176,17 @@ const InstallerModal = ({
)}
+ {state === InstallerState.UPLOADING_FILES && (
+
+ Uploading files
+
+ Uploading files...
+
+
+ {log}
+
+
+ )}
{state === InstallerState.DONE && (
<>
diff --git a/src/components/tooltip/ToolTip.tsx b/src/components/tooltip/ToolTip.tsx
new file mode 100644
index 0000000..462ad64
--- /dev/null
+++ b/src/components/tooltip/ToolTip.tsx
@@ -0,0 +1,27 @@
+import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
+import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import React, { ReactNode } from "react";
+import { OverlayTrigger, Tooltip } from "react-bootstrap";
+
+type Props = {
+ children: ReactNode;
+};
+
+const ToolTip = ({ children }: Props) => {
+ return (
+
+ {children && (
+ {children}}>
+
+
+ )}
+
+ );
+};
+
+export default ToolTip;
diff --git a/src/pages/connection/Connection.tsx b/src/pages/connection/Connection.tsx
index 4811183..572e4e0 100644
--- a/src/pages/connection/Connection.tsx
+++ b/src/pages/connection/Connection.tsx
@@ -1,6 +1,6 @@
import React, { useCallback } from "react";
import { useState } from "react";
-import { Button } from "../../components";
+import { Button, Spinner } from "../../components";
import ConnectionState from "../../model/ConnectionState";
import { ControllerService } from "../../services";
import { SerialPort } from "../../utils/serialport/SerialPort";
@@ -8,7 +8,7 @@ import "./connection.scss";
import PageTitle from "../../components/pagetitle/PageTitle";
import usePageView from "../../hooks/usePageView";
import ControllerLog from "../../components/controllerlog/ControllerLog";
-import { Col, Container, Row } from "react-bootstrap";
+import { Col, Container, Modal, Row } from "react-bootstrap";
import LatestVersionCard from "../../components/cards/latestversioncard/LatestVersionCard";
const connectImageUrl = new URL("../../assets/connect.svg", import.meta.url);
@@ -109,7 +109,8 @@ const Connection = ({ onConnect }: Props) => {
}
loading={
connectionState ===
- ConnectionState.CONNECTING
+ ConnectionState.CONNECTING &&
+ !!controllerService
}
>
<>
@@ -124,14 +125,28 @@ const Connection = ({ onConnect }: Props) => {
)}
- {connectionState === ConnectionState.CONNECTING && (
- {}}
- />
- )}
+ {connectionState === ConnectionState.CONNECTING &&
+ controllerService && (
+ <>
+
+
+ Connecting
+
+ Establishing connection to
+ controller
+
+ {}}
+ />
+
+
+ >
+ )}
diff --git a/src/pages/wifisettings/WiFiSettings.tsx b/src/pages/wifisettings/WiFiSettings.tsx
index ec03e27..265cfe8 100644
--- a/src/pages/wifisettings/WiFiSettings.tsx
+++ b/src/pages/wifisettings/WiFiSettings.tsx
@@ -263,7 +263,7 @@ const WiFiSettings = () => {
<>
-
+
{!isSaving && (
{
Client station settings
-
+
SSID
diff --git a/src/panels/firmware/Firmware.tsx b/src/panels/firmware/Firmware.tsx
index 36a8da4..7c34aab 100644
--- a/src/panels/firmware/Firmware.tsx
+++ b/src/panels/firmware/Firmware.tsx
@@ -1,6 +1,6 @@
import React, { useEffect, useMemo, useState } from "react";
-import { Card } from "../../components";
+import { Card, Spinner } from "../../components";
import {
FirmwareChoice,
GithubRelease,
@@ -12,7 +12,7 @@ import Choice from "../../components/choice";
import { Markdown } from "../../components/markdown/Markdown";
import PageTitle from "../../components/pagetitle/PageTitle";
import FirmwareBreadCrumbList from "./FirmwareBreadcrumbList";
-import { FormCheck } from "react-bootstrap";
+import { Col, FormCheck } from "react-bootstrap";
import { useLocalStorage } from "../../hooks/useLocalStorage";
type Props = {
@@ -37,6 +37,7 @@ const Firmware = ({ onInstall }: Props) => {
"showPrerelease",
false
);
+ const [isLoading, setLoading] = useState(false);
const choice = useMemo(
() => selectedChoices[selectedChoices.length - 1],
@@ -47,6 +48,7 @@ const Firmware = ({ onInstall }: Props) => {
setSelectedRelease(release);
if (release) {
+ setLoading(true);
GithubService.getReleaseManifest(release)
.then((manifest) => {
setReleaseManifest(manifest);
@@ -56,7 +58,8 @@ const Firmware = ({ onInstall }: Props) => {
setErrorMessage(
"Could not download the release asset " + error
);
- });
+ })
+ .finally(() => setLoading(false));
}
};
@@ -122,7 +125,12 @@ const Firmware = ({ onInstall }: Props) => {
>
)}
- {selectedRelease && (
+ {isLoading && (
+
+ Fetching
+
+ )}
+ {!isLoading && selectedRelease && (
<>
{choice && releaseManifest && !choice.images && (
<>
diff --git a/src/services/GitHubService.ts b/src/services/GitHubService.ts
index 91fb9cd..f93ff4e 100644
--- a/src/services/GitHubService.ts
+++ b/src/services/GitHubService.ts
@@ -1,5 +1,3 @@
-import { convertUint8ArrayToBinaryString } from "../utils/utils";
-
export type GithubReleaseAsset = {
id: number;
name: string;
@@ -114,7 +112,7 @@ export const GithubService = {
getImageFiles: (
release: GithubRelease,
images: FirmwareImage[]
- ): Promise => {
+ ): Promise => {
const baseUrl = RESOURCES_BASE_URL + "/" + release.name + "/";
return Promise.all(
@@ -133,9 +131,29 @@ export const GithubService = {
);
}
})
- .then((buffer) => new Uint8Array(buffer))
- .then((buffer) => convertUint8ArrayToBinaryString(buffer));
+ .then((buffer) => new Uint8Array(buffer));
})
);
+ },
+
+ getExtraFile: (
+ release: GithubRelease,
+ file: FirmwareFile
+ ): Promise => {
+ const baseUrl = RESOURCES_BASE_URL + "/" + release.name + "/";
+
+ return fetch(baseUrl + file.path, {
+ headers: {
+ Accept: "application/octet-stream"
+ }
+ })
+ .then((response) => {
+ if (response.status === 200 || response.status === 0) {
+ return response.arrayBuffer();
+ } else {
+ return Promise.reject(new Error(response.statusText));
+ }
+ })
+ .then((buffer) => new Uint8Array(buffer));
}
};
diff --git a/src/services/InstallService.ts b/src/services/InstallService.ts
index 78229f8..11d8849 100644
--- a/src/services/InstallService.ts
+++ b/src/services/InstallService.ts
@@ -1,6 +1,7 @@
import {
FirmwareChoice,
FirmwareImage,
+ FirmwareImageSignature,
GithubRelease,
GithubReleaseManifest,
GithubService
@@ -10,6 +11,7 @@ import { flashDevice } from "../utils/flash";
import sha256 from "crypto-js/sha256";
import { enc } from "crypto-js/core";
import { analytics, logEvent } from "./FirebaseService";
+import { convertUint8ArrayToBinaryString } from "../utils/utils";
export enum FirmwareType {
WIFI = "wifi",
@@ -23,14 +25,15 @@ export enum InstallerState {
ENTER_FLASH_MODE,
FLASHING,
RESTARTING,
+ UPLOADING_FILES,
DONE,
ERROR
}
const convertImagesToFlashFiles = (
images: FirmwareImage[],
- files: string[]
-) => {
+ files: Uint8Array[]
+): FlashFile[] => {
if (images.length != files.length) {
throw new Error("Could not extract files from package");
}
@@ -69,9 +72,9 @@ export const InstallService = {
(imageName) => manifest.images[imageName]
) as FirmwareImage[];
- let files: string[] = [];
+ let imageFiles: Uint8Array[] = [];
try {
- files = await GithubService.getImageFiles(release, images);
+ imageFiles = await GithubService.getImageFiles(release, images);
} catch (error) {
logEvent(analytics, "install", {
version: release.name,
@@ -86,7 +89,7 @@ export const InstallService = {
try {
onState(InstallerState.CHECKING_SIGNATURES);
- validateImageSignatures(images, files);
+ validateImageSignatures(images, imageFiles);
} catch (error) {
logEvent(analytics, "install", {
version: release.name,
@@ -99,7 +102,10 @@ export const InstallService = {
}
try {
- const flashFiles = await convertImagesToFlashFiles(images, files);
+ const flashFiles = await convertImagesToFlashFiles(
+ images,
+ imageFiles
+ );
await flashDevice(
serialPort.getNativeSerialPort(),
flashFiles,
@@ -129,32 +135,36 @@ export const InstallService = {
return Promise.resolve();
}
};
-function validateImageSignatures(images: FirmwareImage[], files: string[]) {
+export const validateImageSignatures = (
+ images: FirmwareImage[],
+ files: Uint8Array[]
+) => {
images.forEach((image, index) => {
- if (image.signature.algorithm === "SHA2-256") {
- const signature = sha256(enc.Latin1.parse(files[index])).toString();
- if (image.signature.value !== signature) {
- console.error(
- "The image " +
- image.path +
- " is possible corrupt. Signature was " +
- signature +
- " but manifest says " +
- image.signature.value
- );
- throw (
- "The image " +
- image.path +
- " might be corrupt as its signature does not match the manifest."
- );
- }
- } else {
- throw (
- "The image " +
- image.path +
- " has an unknown signature algorithm: " +
- image.signature.algorithm
+ validateSignature(image.signature, files[index]);
+ });
+};
+
+export const validateSignature = (
+ signature: FirmwareImageSignature,
+ data: Uint8Array
+) => {
+ if (signature.algorithm === "SHA2-256") {
+ const sign = sha256(
+ enc.Latin1.parse(convertUint8ArrayToBinaryString(data))
+ ).toString();
+ if (signature.value !== sign) {
+ console.error(
+ "The files is possible corrupt. Signature was " +
+ sign +
+ ", expected " +
+ signature.value
);
+ throw "The file might be corrupt as its signature does not match the manifest.";
}
- });
-}
+ } else {
+ throw (
+ "The file has an unknown signature algorithm: " +
+ signature.algorithm
+ );
+ }
+};
diff --git a/src/services/controllerservice/commands/GetAccessPointListCommand.ts b/src/services/controllerservice/commands/GetAccessPointListCommand.ts
index 5aaaa51..05c3a14 100644
--- a/src/services/controllerservice/commands/GetAccessPointListCommand.ts
+++ b/src/services/controllerservice/commands/GetAccessPointListCommand.ts
@@ -26,7 +26,9 @@ export class GetAccessPointListCommand extends Command {
const r = this.response
.filter(
(line) =>
- !line.startsWith("[MSG") && !line.startsWith("[OPT")
+ !line.startsWith("[MSG") &&
+ !line.startsWith("[OPT") &&
+ !line.startsWith("$Wifi")
)
.slice(0, -1)
.join("");
diff --git a/src/utils/flash.ts b/src/utils/flash.ts
index 462c486..a986ebc 100644
--- a/src/utils/flash.ts
+++ b/src/utils/flash.ts
@@ -3,10 +3,17 @@ import CryptoJS from "crypto-js";
import { FlashProgress } from "../services/FlashService";
import { NativeSerialPort } from "./serialport/typings";
import { InstallerState } from "../services";
+import { convertUint8ArrayToBinaryString } from "./utils";
+
+type FlashFile = {
+ fileName: string;
+ data: Uint8Array;
+ address: number;
+};
export const flashDevice = async (
serialPort: NativeSerialPort,
- files,
+ files: FlashFile[],
erase: boolean,
baud: number,
onProgress: (progress: FlashProgress) => void,
@@ -38,7 +45,10 @@ export const flashDevice = async (
onState(InstallerState.FLASHING);
const flashOptions: FlashOptions = {
- fileArray: files,
+ fileArray: files.map((f) => ({
+ address: f.address,
+ data: convertUint8ArrayToBinaryString(f.data)
+ })),
flashSize: "keep",
eraseAll: erase,
compress: true,