Files
pybricks-code/src/sagas/lwp3-bootloader-protocol.ts
T

177 lines
6.0 KiB
TypeScript

// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
// File: sagas/lwp3-bootloader-protocol.ts
// Handles LEGO Wireless Protocol v3 Bootloader protocol.
import {
actionChannel,
fork,
put,
race,
take,
takeEvery,
} from 'typed-redux-saga/macro';
import { Action } from '../actions';
import {
BootloaderConnectionActionType,
BootloaderConnectionDidFailToSendAction,
BootloaderConnectionDidReceiveAction,
BootloaderConnectionDidSendAction,
BootloaderRequestAction,
BootloaderRequestActionType,
checksumResponse,
didError,
didFailToRequest,
didRequest,
eraseResponse,
errorResponse,
infoResponse,
initResponse,
programResponse,
send,
stateResponse,
} from '../actions/lwp3-bootloader';
import {
Command,
ErrorBytecode,
ProtocolError,
createDisconnectRequest,
createEraseFlashRequest,
createGetChecksumRequest,
createGetFlashStateRequest,
createGetInfoRequest,
createInitLoaderRequest,
createProgramFlashRequest,
createStartAppRequest,
getMessageType,
parseEraseFlashResponse,
parseErrorResponse,
parseGetChecksumResponse,
parseGetFlashStateResponse,
parseGetInfoResponse,
parseInitLoaderResponse,
parseProgramFlashResponse,
} from '../protocols/lwp3-bootloader';
import { hex } from '../utils';
/**
* Converts a request action into bytecodes and creates a new action to send
* the bytecodes to to the device.
*/
function* encodeRequest(): Generator {
// Using a while loop to serialize sending data to avoid "busy" errors.
const chan = yield* actionChannel<BootloaderRequestAction>((a: Action) =>
Object.values(BootloaderRequestActionType).includes(
a.type as BootloaderRequestActionType,
),
);
while (true) {
const action = yield* take(chan);
// NB: Commands other than program on city hub will cause BlueZ to
// disconnect because they will send a response even if we write without
// response, so we always write with response on those commands. The
// program command needs to be write without response for performance
// reasons (and also the city hub will disconnect if write with response
// is used on this command).
switch (action.type) {
case BootloaderRequestActionType.Erase:
yield* put(send(createEraseFlashRequest()));
break;
case BootloaderRequestActionType.Program:
yield* put(
send(
createProgramFlashRequest(action.address, action.payload),
/* withResponse */ false,
),
);
break;
case BootloaderRequestActionType.Reboot:
yield* put(send(createStartAppRequest(), /* withResponse */ false));
break;
case BootloaderRequestActionType.Init:
yield* put(send(createInitLoaderRequest(action.firmwareSize)));
break;
case BootloaderRequestActionType.Info:
yield* put(send(createGetInfoRequest()));
break;
case BootloaderRequestActionType.Checksum:
yield* put(send(createGetChecksumRequest()));
break;
case BootloaderRequestActionType.State:
yield* put(send(createGetFlashStateRequest()));
break;
case BootloaderRequestActionType.Disconnect:
yield* put(send(createDisconnectRequest(), /* withResponse */ false));
break;
/* istanbul ignore next: should not be possible to reach */
default:
console.error(`Unknown bootloader request action ${action}`);
continue;
}
const { failedToSend } = yield* race({
sent: take<BootloaderConnectionDidSendAction>(
BootloaderConnectionActionType.DidSend,
),
failedToSend: take<BootloaderConnectionDidFailToSendAction>(
BootloaderConnectionActionType.DidFailToSend,
),
});
if (failedToSend) {
yield* put(didFailToRequest(action.id, failedToSend.err));
} else {
yield* put(didRequest(action.id));
}
}
}
/**
* Converts an incoming connection message to a response action.
* @param action The received response action.
*/
function* decodeResponse(action: BootloaderConnectionDidReceiveAction): Generator {
try {
const responseType = getMessageType(action.data);
switch (responseType) {
case Command.EraseFlash:
yield* put(eraseResponse(parseEraseFlashResponse(action.data)));
break;
case Command.ProgramFlash:
yield* put(programResponse(...parseProgramFlashResponse(action.data)));
break;
case Command.InitLoader:
yield* put(initResponse(parseInitLoaderResponse(action.data)));
break;
case Command.GetInfo:
yield* put(infoResponse(...parseGetInfoResponse(action.data)));
break;
case Command.GetChecksum:
yield* put(checksumResponse(parseGetChecksumResponse(action.data)));
break;
case Command.GetFlashState:
yield* put(stateResponse(parseGetFlashStateResponse(action.data)));
break;
case ErrorBytecode:
yield* put(errorResponse(parseErrorResponse(action.data)));
break;
default:
throw new ProtocolError(
`unknown bootloader response type: ${hex(responseType, 2)}`,
action.data,
);
}
} catch (err) {
yield* put(didError(err));
}
}
export default function* (): Generator {
yield* fork(encodeRequest);
yield* takeEvery(BootloaderConnectionActionType.DidReceive, decodeResponse);
}