OTA Upload Feature (#162)
* Improve restart behaviour under esp8266 * Backend to support firmware update over HTTP * UI for uploading new firmware * Documentation changes
This commit is contained in:
@ -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<SystemProps> {
|
||||
{features.ota && (
|
||||
<Tab value="/system/ota" label="OTA Settings" disabled={!authenticatedContext.me.admin} />
|
||||
)}
|
||||
{features.upload_firmware && (
|
||||
<Tab value="/system/upload" label="Upload Firmware" disabled={!authenticatedContext.me.admin} />
|
||||
)}
|
||||
</Tabs>
|
||||
<Switch>
|
||||
<AuthenticatedRoute exact path="/system/status" component={SystemStatusController} />
|
||||
{features.ota && (
|
||||
<AuthenticatedRoute exact path="/system/ota" component={OTASettingsController} />
|
||||
)}
|
||||
{features.upload_firmware && (
|
||||
<AuthenticatedRoute exact path="/system/upload" component={UploadFirmwareController} />
|
||||
)}
|
||||
<Redirect to="/system/status" />
|
||||
</Switch>
|
||||
</MenuAppBar>
|
||||
|
71
interface/src/system/UploadFirmwareController.tsx
Normal file
71
interface/src/system/UploadFirmwareController.tsx
Normal file
@ -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<WithSnackbarProps, UploadFirmwareControllerState> {
|
||||
|
||||
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 (
|
||||
<SectionContent title="Upload Firmware">
|
||||
<UploadFirmwareForm onFileSelected={this.uploadFile} onCancel={this.cancelUpload} uploading={!!xhr} progress={progress} />
|
||||
</SectionContent>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withSnackbar(UploadFirmwareController);
|
35
interface/src/system/UploadFirmwareForm.tsx
Normal file
35
interface/src/system/UploadFirmwareForm.tsx
Normal file
@ -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<UploadFirmwareFormProps> {
|
||||
|
||||
handleDrop = (files: File[]) => {
|
||||
const file = files[0];
|
||||
if (file) {
|
||||
this.props.onFileSelected(files[0]);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { uploading, progress, onCancel } = this.props;
|
||||
return (
|
||||
<Fragment>
|
||||
<Box py={2}>
|
||||
Upload a new firmware (.bin) file below to replace the existing firmware.
|
||||
</Box>
|
||||
<SingleUpload accept="application/octet-stream" onDrop={this.handleDrop} uploading={uploading} progress={progress} onCancel={onCancel} />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default UploadFirmwareForm;
|
Reference in New Issue
Block a user