add setting to flash current user program

This commit is contained in:
David Lechner
2021-01-14 18:03:04 -06:00
parent 9ee24d0b0c
commit f4cb69f995
8 changed files with 103 additions and 6 deletions
+21
View File
@@ -20,12 +20,14 @@ type StateProps = {
open: boolean;
showDocs: boolean;
darkMode: boolean;
flashCurrentProgram: boolean;
};
type DispatchProps = {
onClose: () => void;
onShowDocsChanged: (checked: boolean) => void;
onDarkModeChanged: (checked: boolean) => void;
onFlashCurrentProgramChanged: (checked: boolean) => void;
};
type SettingsProps = StateProps & DispatchProps & WithI18nProps;
@@ -40,6 +42,8 @@ class SettingsDrawer extends React.PureComponent<SettingsProps> {
onShowDocsChanged,
darkMode,
onDarkModeChanged,
flashCurrentProgram,
onFlashCurrentProgramChanged,
} = this.props;
return (
<Drawer
@@ -85,6 +89,20 @@ class SettingsDrawer extends React.PureComponent<SettingsProps> {
}
/>
</FormGroup>
<FormGroup label={i18n.translate(SettingsStringId.FirmwareTitle)}>
<Switch
label={i18n.translate(
SettingsStringId.FirmwareCurrentProgramLabel,
)}
large={true}
checked={flashCurrentProgram}
onChange={(e) =>
onFlashCurrentProgramChanged(
(e.target as HTMLInputElement).checked,
)
}
/>
</FormGroup>
</div>
</Drawer>
);
@@ -95,6 +113,7 @@ const mapStateToProps = (state: RootState): StateProps => ({
open: state.app.showSettings,
showDocs: state.settings.showDocs,
darkMode: state.settings.darkMode,
flashCurrentProgram: state.settings.flashCurrentProgram,
});
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
@@ -103,6 +122,8 @@ const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
dispatch(setBoolean(SettingId.ShowDocs, checked)),
onDarkModeChanged: (checked): Action =>
dispatch(setBoolean(SettingId.DarkMode, checked)),
onFlashCurrentProgramChanged: (checked): Action =>
dispatch(setBoolean(SettingId.FlashCurrentProgram, checked)),
});
export default connect(
+6
View File
@@ -12,6 +12,12 @@
"zoom": {
"help": "Use {in} and {out} to zoom."
}
},
"firmware": {
"title": "Firmware",
"flash-current-program": {
"label": "Flash my program"
}
}
}
}
+2
View File
@@ -9,4 +9,6 @@ export enum SettingsStringId {
AppearanceDocumentationLabel = 'settings.appearance.documentation.label',
AppearanceDarkModeLabel = 'settings.appearance.dark-mode.label',
AppearanceZoomHelp = 'settings.appearance.zoom.help',
FirmwareTitle = 'settings.firmware.title',
FirmwareCurrentProgramLabel = 'settings.firmware.flash-current-program.label',
}
+1 -1
View File
@@ -3,7 +3,7 @@
// Custom styling for the Settings* controls.
.pb-settings {
.pb-settings .bp3-form-group {
margin: 25px;
}
+17 -1
View File
@@ -9,6 +9,7 @@ import { SettingId, getDefaultBooleanValue } from '../settings';
export interface SettingsState {
readonly darkMode: boolean;
readonly showDocs: boolean;
readonly flashCurrentProgram: boolean;
}
const darkMode: Reducer<boolean, Action> = (
@@ -41,4 +42,19 @@ const showDocs: Reducer<boolean, Action> = (
}
};
export default combineReducers({ darkMode, showDocs });
const flashCurrentProgram: Reducer<boolean, Action> = (
state = getDefaultBooleanValue(SettingId.FlashCurrentProgram),
action,
) => {
switch (action.type) {
case SettingsActionType.DidBooleanChange:
if (action.id === SettingId.FlashCurrentProgram) {
return action.newState;
}
return state;
default:
return state;
}
};
export default combineReducers({ darkMode, showDocs, flashCurrentProgram });
+33 -4
View File
@@ -5,6 +5,7 @@ import { FirmwareMetadata, FirmwareReader, HubType } from '@pybricks/firmware';
import cityHubZip from '@pybricks/firmware/build/cityhub.zip';
import moveHubZip from '@pybricks/firmware/build/movehub.zip';
import technicHubZip from '@pybricks/firmware/build/technichub.zip';
import { Ace } from 'ace-builds';
import {
Effect,
all,
@@ -12,6 +13,7 @@ import {
delay,
put,
race,
select,
take,
takeEvery,
} from 'redux-saga/effects';
@@ -59,6 +61,7 @@ import {
} from '../actions/mpy';
import * as notification from '../actions/notification';
import { MaxProgramFlashSize } from '../protocols/lwp3-bootloader';
import { RootState } from '../reducers';
import { fmod, sumComplement32 } from '../utils/math';
const firmwareZipMap = new Map<HubType, string>([
@@ -112,15 +115,21 @@ function* firmwareIterator(data: DataView, maxSize: number): Generator<number> {
/**
* Loads Pybricks firmware from a .zip file
* @param data The zip file raw data
* @param program User program or `undefined` to use main.py from firmware.zip
*/
function* loadFirmware(
data: ArrayBuffer,
program: string | undefined,
): Generator<unknown, { firmware: Uint8Array; deviceId: HubType }> {
const reader = (yield call(() => FirmwareReader.load(data))) as FirmwareReader;
const firmwareBase = (yield call(() => reader.readFirmwareBase())) as Uint8Array;
const metadata = (yield call(() => reader.readMetadata())) as FirmwareMetadata;
const main = (yield call(() => reader.readMainPy())) as string;
// if a user program was not given, then use main.py from the frimware.zip
if (program === undefined) {
program = (yield call(() => reader.readMainPy())) as string;
}
if (metadata['mpy-abi-version'] !== 5) {
throw Error(
@@ -128,7 +137,7 @@ function* loadFirmware(
);
}
yield put(compile(main, metadata['mpy-cross-options']));
yield put(compile(program, metadata['mpy-cross-options']));
const [mpy, mpyFail] = (yield race([
take(MpyActionType.DidCompile),
take(MpyActionType.DidFailToCompile),
@@ -174,8 +183,28 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator {
let firmware: Uint8Array | undefined = undefined;
let deviceId: HubType | undefined = undefined;
let program: string | undefined = undefined;
const flashCurrentProgram = (yield select(
(s: RootState) => s.settings.flashCurrentProgram,
)) as boolean;
if (flashCurrentProgram) {
const editor = (yield select(
(s: RootState) => s.editor.current,
)) as Ace.EditSession | null;
// istanbul ignore if: it is a bug to dispatch this action with no current editor
if (editor === null) {
console.error('flashFirmware: No current editor');
return;
}
program = editor.getValue();
}
if (action.data !== undefined) {
({ firmware, deviceId } = yield* loadFirmware(action.data));
({ firmware, deviceId } = yield* loadFirmware(action.data, program));
}
yield put(connect());
@@ -227,7 +256,7 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator {
}
const data = (yield call(() => response.arrayBuffer())) as ArrayBuffer;
({ firmware, deviceId } = yield* loadFirmware(data));
({ firmware, deviceId } = yield* loadFirmware(data, program));
if (deviceId !== undefined && info[0].hubType !== deviceId) {
throw Error(
+20
View File
@@ -275,6 +275,26 @@ describe('store settings to local storage', () => {
await saga.end();
});
test('flashCurrentProgram', async () => {
const saga = new AsyncSaga(settings);
const mockSetItem = jest
.spyOn(Object.getPrototypeOf(window.localStorage), 'setItem')
.mockImplementation((key, value) => {
expect(key).toBe('setting.flashCurrentProgram');
expect(value).toBe('false');
});
saga.setState({ settings: { flashCurrentProgram: true } as SettingsState });
saga.put(setBoolean(SettingId.FlashCurrentProgram, false));
expect(mockSetItem).toHaveBeenCalled();
const action = await saga.take();
expect(action).toEqual(didBooleanChange(SettingId.FlashCurrentProgram, false));
await saga.end();
});
});
describe('storage monitor', () => {
+3
View File
@@ -6,6 +6,7 @@
export enum SettingId {
ShowDocs = 'showDocs',
DarkMode = 'darkMode',
FlashCurrentProgram = 'flashCurrentProgram',
}
export function getDefaultBooleanValue(id: SettingId): boolean {
@@ -13,7 +14,9 @@ export function getDefaultBooleanValue(id: SettingId): boolean {
case SettingId.ShowDocs:
return window.innerWidth >= 1024;
case SettingId.DarkMode:
case SettingId.FlashCurrentProgram:
return false;
// istanbul ignore next: it is a programmer error if we hit this
default:
throw Error(`Bad setting id: ${id}`);
}