mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 17:45:22 +00:00
305 lines
8.2 KiB
TypeScript
305 lines
8.2 KiB
TypeScript
// SPDX-License-Identifier: MIT
|
|
// Copyright (c) 2020 The Pybricks Authors
|
|
|
|
// Ref: https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#lego-hub-boot-loader-service
|
|
|
|
import { assert, hex } from '../utils';
|
|
|
|
/**
|
|
* LEGO Powered Up Bootloader Service UUID.
|
|
*/
|
|
export const ServiceUUID = '00001625-1212-efde-1623-785feabcd123';
|
|
|
|
/**
|
|
* LEGO Powered Up Bootloader Characteristic UUID.
|
|
*/
|
|
export const CharacteristicUUID = '00001626-1212-efde-1623-785feabcd123';
|
|
|
|
/**
|
|
* The maximum message size that can be sent or received.
|
|
*/
|
|
export const MaxMessageSize = 20;
|
|
|
|
/**
|
|
* LEGO Powered Up Hub IDs
|
|
*/
|
|
export enum HubType {
|
|
MoveHub = 0x40,
|
|
CityHub = 0x41,
|
|
TechnicHub = 0x80,
|
|
}
|
|
|
|
/**
|
|
* LEGO bootloader command bytecodes.
|
|
*/
|
|
export enum Command {
|
|
EraseFlash = 0x11,
|
|
ProgramFlash = 0x22,
|
|
StartApp = 0x33,
|
|
InitLoader = 0x44,
|
|
GetInfo = 0x55,
|
|
GetChecksum = 0x66,
|
|
GetFlashState = 0x77,
|
|
Disconnect = 0x88,
|
|
}
|
|
|
|
/**
|
|
* Error message bytecode.
|
|
*/
|
|
export type ErrorMessage = 0x05;
|
|
export const ErrorBytecode: ErrorMessage = 0x05;
|
|
|
|
enum ErrorCode {
|
|
UnknownCommand = 0x05,
|
|
}
|
|
|
|
/**
|
|
* Result status.
|
|
*/
|
|
export enum Result {
|
|
OK = 0x00,
|
|
Error = 0xff,
|
|
}
|
|
|
|
/**
|
|
* The largest allowable size for the payload of the ProgramFlash command.
|
|
*
|
|
* Theoretically, this is MTU - 9. However, City hub and Control+ hub report
|
|
* MTU of 158 but don't seem to be able to handle receiving that much data.
|
|
* Anything larger than 32 causes the bootloader to lock up.
|
|
*/
|
|
export const MaxProgramFlashSize: ReadonlyMap<HubType, number> = new Map<
|
|
HubType,
|
|
number
|
|
>([
|
|
[HubType.MoveHub, 14],
|
|
[HubType.CityHub, 32],
|
|
[HubType.TechnicHub, 32],
|
|
]);
|
|
|
|
/**
|
|
* Flash memory protection level.
|
|
*
|
|
* Refer to STM32 technical reference.
|
|
*/
|
|
export enum ProtectionLevel {
|
|
None = 0x00,
|
|
Level1 = 0x01,
|
|
Level2 = 0x02,
|
|
}
|
|
|
|
/**
|
|
* Protocol error. Thrown e.g. when there is a malformed message.
|
|
*/
|
|
export class ProtocolError extends Error {
|
|
/**
|
|
* Creates a new ProtocolError.
|
|
* @param message The error message
|
|
* @param data The bytecodes that caused the error
|
|
*/
|
|
constructor(message: string, public data: DataView) {
|
|
super(message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a new message to erase the flash memory.
|
|
*/
|
|
export function createEraseFlashRequest(): Uint8Array {
|
|
const msg = new Uint8Array(1);
|
|
const view = new DataView(msg.buffer);
|
|
view.setUint8(0, Command.EraseFlash);
|
|
return msg;
|
|
}
|
|
|
|
/**
|
|
* Creates a new message to program the flash memory.
|
|
* @param address The starting address.
|
|
* @param payload The data (14 bytes max)
|
|
*/
|
|
export function createProgramFlashRequest(
|
|
address: number,
|
|
payload: ArrayBuffer,
|
|
): Uint8Array {
|
|
const size = payload.byteLength;
|
|
const msg = new Uint8Array(size + 6);
|
|
const view = new DataView(msg.buffer);
|
|
view.setUint8(0, Command.ProgramFlash);
|
|
view.setUint8(1, size + 4);
|
|
view.setUint32(2, address, true);
|
|
const payloadView = new DataView(payload);
|
|
for (let i = 0; i < size; i++) {
|
|
view.setUint8(6 + i, payloadView.getUint8(i));
|
|
}
|
|
return msg;
|
|
}
|
|
|
|
/**
|
|
* Creates a new message to reboot an start the new firmware.
|
|
*/
|
|
export function createStartAppRequest(): Uint8Array {
|
|
const msg = new Uint8Array(1);
|
|
const view = new DataView(msg.buffer);
|
|
view.setUint8(0, Command.StartApp);
|
|
return msg;
|
|
}
|
|
|
|
/**
|
|
* Creates a new message to prepare the bootloader to receive a new firmware.
|
|
* @param fwSize The total size of the firmware to be flashed.
|
|
*/
|
|
export function createInitLoaderRequest(fwSize: number): Uint8Array {
|
|
const msg = new Uint8Array(5);
|
|
const view = new DataView(msg.buffer);
|
|
view.setUint8(0, Command.InitLoader);
|
|
view.setUint32(1, fwSize, true);
|
|
return msg;
|
|
}
|
|
|
|
/**
|
|
* Creates a new message to get bootloader and device info.
|
|
*/
|
|
export function createGetInfoRequest(): Uint8Array {
|
|
const msg = new Uint8Array(1);
|
|
const view = new DataView(msg.buffer);
|
|
view.setUint8(0, Command.GetInfo);
|
|
return msg;
|
|
}
|
|
|
|
/**
|
|
* Creates a new message to get the current checksum.
|
|
*/
|
|
export function createGetChecksumRequest(): Uint8Array {
|
|
const msg = new Uint8Array(1);
|
|
const view = new DataView(msg.buffer);
|
|
view.setUint8(0, Command.GetChecksum);
|
|
return msg;
|
|
}
|
|
|
|
/**
|
|
* Creates a new message to get the flash memory protection state.
|
|
*
|
|
* This command is not implemented on some devices.
|
|
*/
|
|
export function createGetFlashStateRequest(): Uint8Array {
|
|
const msg = new Uint8Array(1);
|
|
const view = new DataView(msg.buffer);
|
|
view.setUint8(0, Command.GetFlashState);
|
|
return msg;
|
|
}
|
|
|
|
/**
|
|
* Creates a new message to disconnect the connection.
|
|
*/
|
|
export function createDisconnectRequest(): Uint8Array {
|
|
const msg = new Uint8Array(1);
|
|
const view = new DataView(msg.buffer);
|
|
view.setUint8(0, Command.Disconnect);
|
|
return msg;
|
|
}
|
|
|
|
/**
|
|
* Gets the type of message.
|
|
* @param msg The raw message data.
|
|
*/
|
|
export function getMessageType(msg: DataView): Command | ErrorMessage {
|
|
// Technically, the first byte of an error message is the length, but it
|
|
// is always 0x05 which is the same as the error message bytecode.
|
|
return msg.getUint8(0);
|
|
}
|
|
|
|
/**
|
|
* Parses an error message.
|
|
* @param msg The raw message data.
|
|
*/
|
|
export function parseErrorResponse(msg: DataView): Command {
|
|
assert(msg.getUint8(0) === 5, 'unexpected length');
|
|
// Error responses are ordered differently compared to command responses.
|
|
if (msg.getUint8(2) !== ErrorBytecode) {
|
|
throw new ProtocolError(
|
|
`expecting error bytecode 0x05 but got ${hex(msg.getUint8(2), 2)}`,
|
|
msg,
|
|
);
|
|
}
|
|
if (msg.getUint8(4) !== ErrorCode.UnknownCommand) {
|
|
// "command not recognized" is only possible error code
|
|
throw new ProtocolError(`unknown error code: ${hex(msg.getUint8(4), 2)}`, msg);
|
|
}
|
|
const command = msg.getUint8(3);
|
|
return command;
|
|
}
|
|
|
|
/**
|
|
* Parses an erase flash response message.
|
|
* @param msg The raw message data.
|
|
* @returns The result of the erase operation.
|
|
*/
|
|
export function parseEraseFlashResponse(msg: DataView): Result {
|
|
assert(msg.getUint8(0) === Command.EraseFlash, 'expecting erase flash command');
|
|
const result = msg.getUint8(1);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Parses a program flash response message.
|
|
* @param msg The raw message data.
|
|
* @returns The final checksum and the number of bytes written.
|
|
*/
|
|
export function parseProgramFlashResponse(msg: DataView): [number, number] {
|
|
assert(msg.getUint8(0) === Command.ProgramFlash, 'expecting program flash command');
|
|
const checksum = msg.getUint8(1);
|
|
const count = msg.getUint32(2, true);
|
|
return [checksum, count];
|
|
}
|
|
|
|
/**
|
|
* Parses an initialization response message.
|
|
* @param msg The raw message data.
|
|
* @returns The result of the initialization.
|
|
*/
|
|
export function parseInitLoaderResponse(msg: DataView): Result {
|
|
assert(msg.getUint8(0) === Command.InitLoader, 'expecting init loader command');
|
|
const result = msg.getUint8(1);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Parses an information response message.
|
|
* @param msg The raw message data.
|
|
* @returns The bootloader software version, the starting and ending addresses
|
|
* of where firmware can be flashed, and the hub type identifier.
|
|
*/
|
|
export function parseGetInfoResponse(msg: DataView): [number, number, number, HubType] {
|
|
assert(msg.getUint8(0) === Command.GetInfo, 'expecting get info command');
|
|
const version = msg.getUint32(1, true);
|
|
const startAddress = msg.getUint32(5, true);
|
|
const endAddress = msg.getUint32(9, true);
|
|
const hubType = msg.getUint8(13);
|
|
return [version, startAddress, endAddress, hubType];
|
|
}
|
|
|
|
/**
|
|
* Parses a checksum response message.
|
|
* @param msg The raw message data.
|
|
* @returns The checksum of the data that has been flashed so far.
|
|
*/
|
|
export function parseGetChecksumResponse(msg: DataView): number {
|
|
assert(msg.getUint8(0) === Command.GetChecksum, 'expecting get checksum command');
|
|
const checksum = msg.getUint8(1);
|
|
return checksum;
|
|
}
|
|
|
|
/**
|
|
* Parses a flash protection state response message.
|
|
* @param msg The raw message data.
|
|
* @returns The protection level
|
|
*/
|
|
export function parseGetFlashStateResponse(msg: DataView): ProtectionLevel {
|
|
assert(
|
|
msg.getUint8(0) === Command.GetFlashState,
|
|
'expecting get flash state command',
|
|
);
|
|
const level = msg.getUint8(1);
|
|
return level;
|
|
}
|