Files
pybricks-code/src/sagas/lwp3-bootloader-protocol.ts
T
David Lechner 816053ba7c don't expect response on disconnect commands
The LWP3 bootloader disconnect and reboot commands cause the device
to disconnect before sending a response, so we must always use
write without response for these, otherwise the program can hang
waiting for a response.
2021-01-19 17:20:56 -06:00

158 lines
5.6 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 { Channel } from 'redux-saga';
import { actionChannel, fork, put, take, takeEvery } from 'redux-saga/effects';
import { Action } from '../actions';
import {
BootloaderConnectionActionType,
BootloaderConnectionDidReceiveAction,
BootloaderConnectionDidSendAction,
BootloaderRequestAction,
BootloaderRequestActionType,
checksumResponse,
didError,
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((a: Action) =>
Object.values(BootloaderRequestActionType).includes(
a.type as BootloaderRequestActionType,
),
)) as Channel<BootloaderRequestAction>;
while (true) {
const action = (yield take(chan)) as BootloaderRequestAction;
// 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 sent = (yield take(
BootloaderConnectionActionType.DidSend,
)) as BootloaderConnectionDidSendAction;
yield put(didRequest(action.id, sent.err));
}
}
/**
* 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);
}