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