mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 01:23:52 +00:00
add setting to flash current user program
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -12,6 +12,12 @@
|
||||
"zoom": {
|
||||
"help": "Use {in} and {out} to zoom."
|
||||
}
|
||||
},
|
||||
"firmware": {
|
||||
"title": "Firmware",
|
||||
"flash-current-program": {
|
||||
"label": "Flash my program"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
// Custom styling for the Settings* controls.
|
||||
|
||||
.pb-settings {
|
||||
.pb-settings .bp3-form-group {
|
||||
margin: 25px;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user