diff --git a/graphql.schema.json b/graphql.schema.json index cf10fb581..b1d4920fa 100644 --- a/graphql.schema.json +++ b/graphql.schema.json @@ -2329,6 +2329,12 @@ "description": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "TargetMismatch", + "description": null, + "isDeprecated": false, + "deprecationReason": null } ], "possibleTypes": null @@ -2455,6 +2461,12 @@ "description": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "ForceFlash", + "description": null, + "isDeprecated": false, + "deprecationReason": null } ], "possibleTypes": null diff --git a/src/api/src/library/FirmwareBuilder/index.ts b/src/api/src/library/FirmwareBuilder/index.ts index f3b8cafe5..9f4d93e2a 100644 --- a/src/api/src/library/FirmwareBuilder/index.ts +++ b/src/api/src/library/FirmwareBuilder/index.ts @@ -3,6 +3,7 @@ import path from 'path'; import Platformio from '../Platformio'; import { CommandResult, NoOpFunc, OnOutputFunc } from '../Commander'; import UserDefineKey from './Enum/UserDefineKey'; +import UploadType from '../Platformio/Enum/UploadType'; interface UserDefinesCompatiblityResult { compatible: boolean; @@ -75,9 +76,16 @@ export default class FirmwareBuilder { userDefines: string, firmwarePath: string, serialPort: string | undefined, - onOutput: OnOutputFunc = NoOpFunc + onOutput: OnOutputFunc = NoOpFunc, + uploadType: UploadType ): Promise { await this.storeUserDefines(firmwarePath, userDefines); - return this.platformio.flash(firmwarePath, target, serialPort, onOutput); + return this.platformio.flash( + firmwarePath, + target, + serialPort, + onOutput, + uploadType + ); } } diff --git a/src/api/src/library/Platformio/Enum/UploadType.ts b/src/api/src/library/Platformio/Enum/UploadType.ts new file mode 100644 index 000000000..443cb4cab --- /dev/null +++ b/src/api/src/library/Platformio/Enum/UploadType.ts @@ -0,0 +1,6 @@ +enum UploadType { + Normal = 'upload', + Force = 'uploadforce', +} + +export default UploadType; diff --git a/src/api/src/library/Platformio/index.ts b/src/api/src/library/Platformio/index.ts index bd6e96cc0..e011a1e90 100644 --- a/src/api/src/library/Platformio/index.ts +++ b/src/api/src/library/Platformio/index.ts @@ -5,6 +5,7 @@ import path from 'path'; import child_process from 'child_process'; import Commander, { CommandResult, NoOpFunc, OnOutputFunc } from '../Commander'; import { LoggerService } from '../../logger'; +import UploadType from './Enum/UploadType'; interface PlatformioCoreState { core_version: string; @@ -229,7 +230,8 @@ export default class Platformio { projectDir: string, environment: string, serialPort: string | undefined, - onUpdate: OnOutputFunc = NoOpFunc + onUpdate: OnOutputFunc = NoOpFunc, + uploadType: UploadType ) { const params = [ 'run', @@ -238,7 +240,7 @@ export default class Platformio { '--environment', environment, '--target', - 'upload', + uploadType, ]; if (serialPort !== undefined && serialPort !== null) { params.push('--upload-port'); diff --git a/src/api/src/models/enum/BuildFirmwareErrorType.ts b/src/api/src/models/enum/BuildFirmwareErrorType.ts index c90c17023..9f291ac66 100644 --- a/src/api/src/models/enum/BuildFirmwareErrorType.ts +++ b/src/api/src/models/enum/BuildFirmwareErrorType.ts @@ -7,6 +7,7 @@ enum BuildFirmwareErrorType { BuildError = 'BuildError', FlashError = 'FlashError', GenericError = 'GenericError', + TargetMismatch = 'TargetMismatch', } registerEnumType(BuildFirmwareErrorType, { diff --git a/src/api/src/models/enum/BuildJobType.ts b/src/api/src/models/enum/BuildJobType.ts index fba9b5866..8b52454e9 100644 --- a/src/api/src/models/enum/BuildJobType.ts +++ b/src/api/src/models/enum/BuildJobType.ts @@ -3,6 +3,7 @@ import { registerEnumType } from 'type-graphql'; enum BuildJobType { Build = 'Build', BuildAndFlash = 'BuildAndFlash', + ForceFlash = 'ForceFlash', } registerEnumType(BuildJobType, { diff --git a/src/api/src/services/Firmware/index.ts b/src/api/src/services/Firmware/index.ts index 69fa905a8..97cbd3296 100644 --- a/src/api/src/services/Firmware/index.ts +++ b/src/api/src/services/Firmware/index.ts @@ -25,6 +25,7 @@ import FirmwareBuilder from '../../library/FirmwareBuilder'; import { LoggerService } from '../../logger'; import UserDefineKey from '../../library/FirmwareBuilder/Enum/UserDefineKey'; import PullRequest from '../../models/PullRequest'; +import UploadType from '../../library/Platformio/Enum/UploadType'; interface FirmwareVersionData { source: FirmwareSource; @@ -454,11 +455,27 @@ export default class FirmwareService { ); } - if (params.type === BuildJobType.BuildAndFlash) { + if ( + params.type === BuildJobType.BuildAndFlash || + params.type === BuildJobType.ForceFlash + ) { await this.updateProgress( BuildProgressNotificationType.Info, BuildFirmwareStep.FLASHING_FIRMWARE ); + + let uploadType: UploadType; + switch (params.type) { + case BuildJobType.BuildAndFlash: + uploadType = UploadType.Normal; + break; + case BuildJobType.ForceFlash: + uploadType = UploadType.Force; + break; + default: + throw new Error(`Unknown build job type ${params.type}`); + } + const flashResult = await this.builder.flash( params.target, userDefines, @@ -466,17 +483,26 @@ export default class FirmwareService { params.serialDevice, (output) => { this.updateLogs(output); - } + }, + uploadType ); if (!flashResult.success) { this.logger?.error('flash error', undefined, { stderr: flashResult.stderr, stdout: flashResult.stdout, }); + const uploadErrorRegexp = /\*\*\* \[upload\] Error (-*\d+)/g; + let uploadError = 0; + const matches = [...flashResult.stderr.matchAll(uploadErrorRegexp)]; + if (matches.length > 0) { + uploadError = Number.parseInt(matches[0][1], 10); + } return new BuildFlashFirmwareResult( false, flashResult.stderr, - BuildFirmwareErrorType.FlashError + uploadError === -2 + ? BuildFirmwareErrorType.TargetMismatch + : BuildFirmwareErrorType.FlashError ); } } diff --git a/src/ui/components/BuildProgressBar/index.tsx b/src/ui/components/BuildProgressBar/index.tsx index ce8ce308f..4e0bad349 100644 --- a/src/ui/components/BuildProgressBar/index.tsx +++ b/src/ui/components/BuildProgressBar/index.tsx @@ -46,6 +46,7 @@ const BuildProgressBar: FunctionComponent = memo( } break; case BuildJobType.BuildAndFlash: + case BuildJobType.ForceFlash: switch (notification.step) { case BuildFirmwareStep.VERIFYING_BUILD_SYSTEM: return 5; diff --git a/src/ui/components/BuildResponse/index.tsx b/src/ui/components/BuildResponse/index.tsx index aeb5eb12a..6c34f8ab4 100644 --- a/src/ui/components/BuildResponse/index.tsx +++ b/src/ui/components/BuildResponse/index.tsx @@ -37,6 +37,8 @@ const BuildResponse: FunctionComponent = memo( return 'Build error'; case BuildFirmwareErrorType.FlashError: return 'Flash error'; + case BuildFirmwareErrorType.TargetMismatch: + return 'The target you are trying to flash does not match the devices current target, if you are sure you want to do this, click Force Flash below'; default: return ''; } diff --git a/src/ui/components/SplitButton/index.tsx b/src/ui/components/SplitButton/index.tsx new file mode 100644 index 000000000..3b2370360 --- /dev/null +++ b/src/ui/components/SplitButton/index.tsx @@ -0,0 +1,112 @@ +import * as React from 'react'; +import Button from '@mui/material/Button'; +import ButtonGroup, { ButtonGroupProps } from '@mui/material/ButtonGroup'; +import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; +import ClickAwayListener from '@mui/material/ClickAwayListener'; +import Grow from '@mui/material/Grow'; +import Paper from '@mui/material/Paper'; +import Popper from '@mui/material/Popper'; +import MenuItem from '@mui/material/MenuItem'; +import MenuList from '@mui/material/MenuList'; +import { FunctionComponent, useState } from 'react'; +import { uniqueId } from 'lodash'; + +export interface Option { + label: string; + value: string; +} + +interface SplitButtonProps extends ButtonGroupProps { + options: Option[]; + onButtonClick: (value: string | null) => void; +} + +const SplitButton: FunctionComponent = ({ + options, + onButtonClick, + ...props +}) => { + const [open, setOpen] = React.useState(false); + const anchorRef = React.useRef(null); + const [selectedIndex, setSelectedIndex] = React.useState(0); + const [splitButtonMenuId] = useState(() => uniqueId('split-button-menu-')); + + const handleClick = () => { + onButtonClick(options[selectedIndex].value); + }; + + const handleMenuItemClick = (event: any, index: number) => { + setSelectedIndex(index); + setOpen(false); + }; + + const handleToggle = () => { + setOpen((prevOpen) => !prevOpen); + }; + + const handleClose = (event: any) => { + if ( + anchorRef.current && + event.target && + anchorRef.current.contains(event.target) + ) { + return; + } + + setOpen(false); + }; + + return ( + <> + + + + + + {({ TransitionProps, placement }) => ( + + + + + {options.map((option, index) => ( + handleMenuItemClick(event, index)} + > + {option.label} + + ))} + + + + + )} + + + ); +}; + +export default SplitButton; diff --git a/src/ui/gql/generated/types.ts b/src/ui/gql/generated/types.ts index 8b6853f29..494be4e1e 100644 --- a/src/ui/gql/generated/types.ts +++ b/src/ui/gql/generated/types.ts @@ -294,6 +294,7 @@ export enum BuildFirmwareErrorType { BuildError = 'BuildError', FlashError = 'FlashError', GenericError = 'GenericError', + TargetMismatch = 'TargetMismatch', } export type BuildFlashFirmwareInput = { @@ -309,6 +310,7 @@ export type BuildFlashFirmwareInput = { export enum BuildJobType { Build = 'Build', BuildAndFlash = 'BuildAndFlash', + ForceFlash = 'ForceFlash', } export type FirmwareVersionDataInput = { diff --git a/src/ui/views/ConfiguratorView/index.tsx b/src/ui/views/ConfiguratorView/index.tsx index f14f4777b..011309970 100644 --- a/src/ui/views/ConfiguratorView/index.tsx +++ b/src/ui/views/ConfiguratorView/index.tsx @@ -56,6 +56,7 @@ import { UserDefineKey, UserDefineKind, TargetDeviceOptionsQuery, + BuildFirmwareErrorType, } from '../../gql/generated/types'; import Loader from '../../components/Loader'; import BuildResponse from '../../components/BuildResponse'; @@ -77,6 +78,7 @@ import ShowTimeoutAlerts from '../../components/ShowTimeoutAlerts'; import useAppState from '../../hooks/useAppState'; import AppStatus from '../../models/enum/AppStatus'; import MainLayout from '../../layouts/MainLayout'; +import SplitButton from '../../components/SplitButton'; const styles = { button: { @@ -661,6 +663,7 @@ const ConfiguratorView: FunctionComponent = (props) => { const onBuild = () => sendJob(BuildJobType.Build); const onBuildAndFlash = () => sendJob(BuildJobType.BuildAndFlash); + const onForceFlash = () => sendJob(BuildJobType.ForceFlash); const deviceOptionsRef = useRef(null); @@ -913,14 +916,28 @@ const ConfiguratorView: FunctionComponent = (props) => { Build {deviceTarget?.flashingMethod !== FlashingMethod.Radio && ( - + options={[ + { + label: 'Build & Flash', + value: BuildJobType.BuildAndFlash, + }, + { + label: 'Force Flash', + value: BuildJobType.ForceFlash, + }, + ]} + onButtonClick={(value: string | null) => { + if (value === BuildJobType.BuildAndFlash) { + onBuildAndFlash(); + } else if (value === BuildJobType.ForceFlash) { + onForceFlash(); + } + }} + /> )} @@ -1053,16 +1070,27 @@ const ConfiguratorView: FunctionComponent = (props) => { sx={styles.button} size="large" variant="contained" - onClick={ - currentJobType === BuildJobType.Build - ? onBuild - : onBuildAndFlash - } + onClick={() => { + sendJob(currentJobType); + }} > Retry )} + {!response?.buildFlashFirmware.success && + response?.buildFlashFirmware.errorType === + BuildFirmwareErrorType.TargetMismatch && ( + + )} + {response?.buildFlashFirmware.success && luaDownloadButton()}