├── rtos_ram.ld ├── .gitignore ├── dump2bin.sh ├── keys1.ld ├── keys2.ld ├── pack.h ├── ware.h ├── mmap.h ├── Makefile ├── read_logs.py ├── srec.c ├── dump.c ├── update.py ├── endian.h ├── unpack.c ├── pack.c ├── crc32.c ├── vanmoof_sx3_sound_bitmask.md ├── keys.c ├── ble-patch.c ├── patch.c └── README.md /rtos_ram.ld: -------------------------------------------------------------------------------- 1 | xdcRomConstPtr = 0x20000100; 2 | xdcRomExternFuncPtr = 0x20000104; 3 | xdcRomStatePtr = 0x20000108; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | pack 3 | unpack 4 | crc32 5 | patch 6 | patch-dump 7 | ble-patch 8 | keys 9 | keys1 10 | keys2 11 | *.bin 12 | *.hex 13 | *.exe 14 | -------------------------------------------------------------------------------- /dump2bin.sh: -------------------------------------------------------------------------------- 1 | if [ $# -ne 2 ]; then 2 | echo "usage: $0 " 3 | exit 1 4 | fi 5 | 6 | exec cat $1 | sed -e 's/[^\t]*\t\([^\t]*\)\t.*/\1/' | xxd -r -p >$2 7 | -------------------------------------------------------------------------------- /keys1.ld: -------------------------------------------------------------------------------- 1 | OUTPUT_FORMAT(elf32-littlearm) 2 | 3 | ENTRY(_start) 4 | 5 | INCLUDE rtos_ram.ld 6 | 7 | MEMORY 8 | { 9 | free_rom : ORIGIN = 0x2c67c, LENGTH = 0x1000 10 | } 11 | 12 | SECTIONS 13 | { 14 | .text : 15 | { 16 | *(.text) 17 | *(.rodata*) 18 | } > free_rom = 0xff 19 | } 20 | -------------------------------------------------------------------------------- /keys2.ld: -------------------------------------------------------------------------------- 1 | OUTPUT_FORMAT(elf32-littlearm) 2 | 3 | ENTRY(_start) 4 | 5 | INCLUDE rtos_ram.ld 6 | 7 | MEMORY 8 | { 9 | free_rom : ORIGIN = 0x3531c, LENGTH = 0x1000 10 | } 11 | 12 | SECTIONS 13 | { 14 | .text : 15 | { 16 | *(.text) 17 | *(.rodata*) 18 | } > free_rom = 0xff 19 | } 20 | -------------------------------------------------------------------------------- /pack.h: -------------------------------------------------------------------------------- 1 | #ifndef _PACK_H 2 | #define _PACK_H 1 3 | 4 | #include 5 | 6 | #define PACK_MAGIC "PACK" 7 | 8 | typedef struct { 9 | char magic[4]; 10 | uint32_t offset; 11 | uint32_t length; 12 | } pack_header_t; 13 | 14 | typedef struct { 15 | char filename[56]; 16 | uint32_t offset; 17 | uint32_t length; 18 | } pack_entry_t; 19 | 20 | #endif 21 | -------------------------------------------------------------------------------- /ware.h: -------------------------------------------------------------------------------- 1 | #ifndef _WARE_H 2 | #define _WARE_H 1 3 | 4 | #define WARE_MAGIC 0xaa55aa55 5 | 6 | #define MAINWARE_OFFSET 0x08020000 7 | 8 | typedef struct { 9 | uint32_t magic; 10 | uint32_t version; 11 | uint32_t crc; 12 | uint32_t length; 13 | char date[12]; 14 | char time[12]; 15 | } vanmoof_ware_t; 16 | 17 | #define BLE_WARE_MAGIC "OAD NVM1" 18 | #define BLE_WARE_OFFSET 0x00000000 19 | 20 | typedef struct { 21 | uint8_t magic[8]; 22 | uint32_t crc; 23 | uint8_t meta_ver; 24 | uint8_t bim_ver; 25 | uint16_t tech_type; 26 | uint8_t crc_stat; 27 | uint8_t img_cp_stat; 28 | uint8_t img_no; 29 | uint8_t img_type; 30 | uint32_t img_vld; 31 | uint32_t len; 32 | uint32_t prg_entry; 33 | uint32_t soft_ver; 34 | uint32_t img_end_addr; 35 | uint16_t hdr_len; 36 | uint16_t rfu; 37 | } ble_ware_t; 38 | 39 | typedef struct __attribute__((packed)) { 40 | uint8_t seg_type; 41 | uint16_t wireless_tech; 42 | uint8_t rfu; 43 | uint32_t seg_len; 44 | } ble_ware_seg_t; 45 | 46 | #define BLE_SEG_TYPE_BOUNDARY 0 47 | #define BLE_SEG_TYPE_CONTIGUOUS 1 48 | #define BLE_SEG_TYPE_NONCONTIGUOUS 2 49 | #define BLE_SEG_TYPE_SECURITY 3 50 | #define BLE_SEG_TYPE_NVRAM 4 51 | #define BLE_SEG_TYPE_DELTA 5 52 | 53 | typedef struct __attribute__((packed)) { 54 | uint8_t sig_ver; 55 | uint32_t timestamp; 56 | uint8_t ecdsa_signer[8]; 57 | uint8_t ecdsa_signature[64]; 58 | } ble_ware_signature_seg_t; 59 | 60 | typedef struct { 61 | uint32_t start_addr; 62 | } ble_ware_code_seg_t; 63 | 64 | #endif 65 | -------------------------------------------------------------------------------- /mmap.h: -------------------------------------------------------------------------------- 1 | #ifndef _MMAP_H 2 | #define _MMAP_H 1 3 | 4 | #ifndef _WIN32 5 | 6 | #include 7 | 8 | #ifndef O_BINARY 9 | #define O_BINARY 0 10 | #endif 11 | 12 | #else /* _WIN32 */ 13 | 14 | #define WIN32_LEAN_AND_MEAN /* ::rolleyes:: */ 15 | #include 16 | 17 | #define PROT_READ 1 18 | #define PROT_WRITE 2 19 | 20 | #define MAP_SHARED 1 21 | 22 | static void* mmap(void* addr, size_t size, int prot, int flags, int fd, off_t offset) 23 | { 24 | HANDLE hFile; 25 | HANDLE hMap; 26 | DWORD protect; 27 | DWORD access; 28 | void* data; 29 | hFile = (HANDLE)_get_osfhandle(fd); 30 | if (hFile == INVALID_HANDLE_VALUE) { 31 | fprintf(stderr, "FileSystem_MapFile: get_osfhandle failed\n"); 32 | return (void*)-1; 33 | } 34 | protect = PAGE_READONLY; 35 | if (prot & PROT_WRITE) { 36 | protect = PAGE_READWRITE; 37 | } 38 | hMap = CreateFileMapping(hFile, NULL, protect, 0, size, NULL); 39 | if (hMap == NULL) { 40 | fprintf(stderr, "FileSystem_MapFile: CreateFileMapping failed\n"); 41 | return (void*)-1; 42 | } 43 | access = FILE_MAP_READ; 44 | if (prot & PROT_WRITE) { 45 | access = FILE_MAP_WRITE; 46 | } 47 | data = MapViewOfFile(hMap, access, 0, 0, size); 48 | if (data == NULL) { 49 | fprintf(stderr, "FileSystem_MapFile: MapViewOfFile failed\n"); 50 | CloseHandle(hMap); 51 | return (void*)-1; 52 | } 53 | CloseHandle(hMap); 54 | return data; 55 | } 56 | 57 | static int munmap(void *addr, size_t len) 58 | { 59 | FlushViewOfFile(addr, len); 60 | UnmapViewOfFile(addr); 61 | return 0; 62 | } 63 | 64 | #endif /* _WIN32 */ 65 | 66 | #endif /* _MMAP_H */ 67 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CC = gcc 2 | LDLIBS = -lz 3 | 4 | CFLAGS = -O1 -g 5 | 6 | ARM_FLAGS = -Os -mthumb -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 \ 7 | -ffreestanding -fno-toplevel-reorder 8 | 9 | all: pack unpack crc32 patch patch-dump ble-patch 10 | 11 | pack: pack.o 12 | unpack: unpack.o 13 | crc32: crc32.o 14 | patch: patch.o 15 | patch-dump: patch-dump.o 16 | ble-patch: ble-patch.o 17 | 18 | pack.o: pack.c pack.h 19 | unpack.o: unpack.c pack.h 20 | crc32.o: crc32.c ware.h 21 | patch.o: patch.c ware.h 22 | 23 | ble-patch.o: ble-patch.c ware.h keys1.hex keys2.hex 24 | $(eval SYSTEM_PUTCHAR1=$(shell nm keys1 | grep System_putchar | cut -d' ' -f1)) 25 | $(eval SYSTEM_PUTCHAR2=$(shell nm keys2 | grep System_putchar | cut -d' ' -f1)) 26 | $(CC) $(CFLAGS) -DSYSTEM_PUTCHAR1=0x$(SYSTEM_PUTCHAR1) -DSYSTEM_PUTCHAR2=0x$(SYSTEM_PUTCHAR2) -o $@ -c $< 27 | 28 | patch-dump.o: patch.c ware.h dump.hex 29 | $(CC) $(CFLAGS) -DDUMP -o $@ -c $< 30 | 31 | dump.hex: dump.bin 32 | od -v -An -tx2 $< | sed -e 's/\([0-9a-f][0-9a-f][0-9a-f][0-9a-f]\)/0x\1,/g' >$@ 33 | 34 | dump.bin: dump.o 35 | arm-none-eabi-objcopy -O binary $< $@ 36 | 37 | dump.o: dump.c 38 | arm-none-eabi-gcc $(ARM_FLAGS) -fPIC -c $< 39 | 40 | keys1.hex: keys1.bin 41 | od -v -An -tx2 $< | sed -e 's/\([0-9a-f][0-9a-f][0-9a-f][0-9a-f]\)/0x\1,/g' >$@ 42 | 43 | keys2.hex: keys2.bin 44 | od -v -An -tx2 $< | sed -e 's/\([0-9a-f][0-9a-f][0-9a-f][0-9a-f]\)/0x\1,/g' >$@ 45 | 46 | keys1.bin: keys1 47 | arm-none-eabi-objcopy -O binary $< $@ 48 | 49 | keys2.bin: keys2 50 | arm-none-eabi-objcopy -O binary $< $@ 51 | 52 | keys1: keys1.o keys1.ld 53 | arm-none-eabi-ld -T keys1.ld -e dump -o $@ $< 54 | 55 | keys2: keys2.o keys2.ld 56 | arm-none-eabi-ld -T keys2.ld -e dump -o $@ $< 57 | 58 | keys1.o: keys.c 59 | arm-none-eabi-gcc $(ARM_FLAGS) -DVERSION_1_4_1 -c $< -o $@ 60 | 61 | keys2.o: keys.c 62 | arm-none-eabi-gcc $(ARM_FLAGS) -DVERSION_2_4_1 -c $< -o $@ 63 | 64 | clean: 65 | rm -f *.o *.hex *.bin *.exe keys1 keys2 pack unpack crc32 patch patch-dump ble-patch 66 | -------------------------------------------------------------------------------- /read_logs.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import asyncio 3 | 4 | import bleak 5 | import zlib 6 | 7 | from pymoof.clients.sx3 import Sound 8 | from pymoof.clients.sx3 import SX3Client 9 | from pymoof.tools import discover_bike 10 | from pymoof.tools import retrieve_encryption_key 11 | from pymoof.util import bleak_utils 12 | 13 | import enum 14 | import math 15 | 16 | 17 | class Maintenance(enum.Enum): 18 | 19 | SERVICE_UUID = '6acc55c0-e631-4069-944d-b8ca7598ad50' 20 | 21 | LOG_MODE = '6acc55c1-e631-4069-944d-b8ca7598ad50' 22 | LOG_SIZE = '6acc55c2-e631-4069-944d-b8ca7598ad50' 23 | LOG_BLOCK = '6acc55c3-e631-4069-944d-b8ca7598ad50' 24 | 25 | 26 | async def read_logs(): 27 | # print('Getting key from vanmoof servers') 28 | # key, user_key_id = retrieve_encryption_key.query() 29 | 30 | # Insert your API key here: 31 | key = '5f5f5f5f5f4f574e45525f5045524d53' 32 | user_key_id = 1 33 | print(f'key: {key}, user_key_id {user_key_id}') 34 | 35 | print('Discovering nearby vanmoof bikes') 36 | device = await discover_bike.query() 37 | if device is None: 38 | exit(1) 39 | 40 | print('Reading logs') 41 | async with bleak.BleakClient(device) as bleak_client: 42 | client = SX3Client(bleak_client, key, user_key_id) 43 | 44 | await client.authenticate() 45 | 46 | await client.play_sound(Sound.BEEP_POSITIVE) 47 | 48 | result = await client._read(Maintenance.LOG_MODE) 49 | mode = int(result[0]) 50 | print(f'Log mode: {mode}') 51 | result = await client._read(Maintenance.LOG_SIZE) 52 | size = int.from_bytes(result[0:4], "big") 53 | print(f'Log size: {size}') 54 | 55 | log_data = '' 56 | offset = 0 57 | 58 | try: 59 | print('Log data:') 60 | while offset < size: 61 | block = bytearray() 62 | block.append((offset >> 24) & 0xff) 63 | block.append((offset >> 16) & 0xff) 64 | block.append((offset >> 8) & 0xff) 65 | block.append(offset & 0xff) 66 | block.append(255) # Number of blocks 67 | await client._write(Maintenance.LOG_BLOCK, block) 68 | result = await client._read(Maintenance.LOG_BLOCK) 69 | text = result.decode('ASCII') 70 | print(text, end='') 71 | log_data += text 72 | offset += int(len(result) / 16) 73 | print('') 74 | except Exception as e: 75 | print(f'Error reading logs: {e}, last offset {offset}') 76 | 77 | 78 | asyncio.run(read_logs()) 79 | -------------------------------------------------------------------------------- /srec.c: -------------------------------------------------------------------------------- 1 | typedef unsigned char uint8_t; 2 | typedef unsigned int uint32_t; 3 | typedef uint32_t size_t; 4 | 5 | #define NULL ((void *)0) 6 | 7 | #define FLASH_START 0x08000000 8 | #define FLASH_SIZE 0x00180000 9 | 10 | #define UART7_START 0x40007800 11 | #define WWDG_START 0x40002c00 12 | 13 | typedef struct { 14 | volatile uint32_t CR; 15 | } WWDG_t; 16 | 17 | typedef struct { 18 | volatile uint32_t SR; 19 | volatile uint32_t DR; 20 | volatile uint32_t BRR; 21 | volatile uint32_t CR1; 22 | } UART_t; 23 | 24 | static int send_srec(uint8_t type, uint32_t addr, size_t addr_len, const uint8_t *data, size_t len); 25 | 26 | void dump_flash(void) 27 | { 28 | UART_t *UART7 = (void *)UART7_START; 29 | uint8_t *FLASH = (void *)FLASH_START; 30 | uint32_t head = 0x48445200; 31 | size_t count = 0; 32 | size_t i, j; 33 | 34 | uint32_t cr1 = UART7->CR1; 35 | UART7->CR1 = cr1 & ~(0x1f0); 36 | 37 | send_srec(0, 0, 2, (uint8_t *)&head, 4); 38 | for (i = 0; i < FLASH_SIZE; i += 32) { 39 | send_srec(3, FLASH_START + i, 4, FLASH + i, 32); 40 | count++; 41 | } 42 | send_srec(5, count, 2, NULL, 0); 43 | send_srec(7, FLASH_START, 4, NULL, 0); 44 | 45 | while (!(UART7->SR & 0x40)) 46 | /* wait */; 47 | UART7->CR1 = cr1; 48 | } 49 | 50 | static uint8_t byte_to_rec(char* buffer, uint8_t byte) 51 | { 52 | uint8_t nib = (byte >> 4) & 0x0f; 53 | if (nib < 10) 54 | buffer[0] = '0' + nib; 55 | else 56 | buffer[0] = 'A' + (nib - 10); 57 | nib = byte & 0x0f; 58 | if (nib < 10) 59 | buffer[1] = '0' + nib; 60 | else 61 | buffer[1] = 'A' + (nib - 10); 62 | return byte; 63 | } 64 | 65 | static void wdg(void) 66 | { 67 | WWDG_t *WWDG = (void *)WWDG_START; 68 | WWDG->CR = 0x7f; 69 | } 70 | 71 | static void uart_send(const char* data, size_t len) 72 | { 73 | UART_t *UART7 = (void *)UART7_START; 74 | size_t i; 75 | 76 | for (i = 0; i < len; i++) { 77 | while (!(UART7->SR & 0x80)) 78 | /* wait */; 79 | UART7->DR = data[i]; 80 | } 81 | wdg(); 82 | } 83 | 84 | static int send_srec(uint8_t type, uint32_t addr, size_t addr_len, const uint8_t *data, size_t len) 85 | { 86 | char buffer[2 + 2 + 8 + 64 + 2 + 2]; 87 | char* p = buffer + 2; 88 | uint8_t total; 89 | uint8_t sum = 0; 90 | size_t i; 91 | 92 | buffer[0] = 'S'; 93 | buffer[1] = '0' + type; 94 | total = addr_len + len + 1; 95 | sum += byte_to_rec(p, total); 96 | p += 2; 97 | switch (addr_len) { 98 | case 4: 99 | sum += byte_to_rec(p, (addr >> 24) & 0xff); 100 | p += 2; 101 | case 3: 102 | sum += byte_to_rec(p, (addr >> 16) & 0xff); 103 | p += 2; 104 | default: 105 | sum += byte_to_rec(p, (addr >> 8) & 0xff); 106 | p += 2; 107 | sum += byte_to_rec(p, addr & 0xff); 108 | p += 2; 109 | } 110 | for (i = 0; i < len; i++) { 111 | sum += byte_to_rec(p, data[i]); 112 | p += 2; 113 | } 114 | sum ^= 0xff; 115 | byte_to_rec(p, sum); 116 | p += 2; 117 | *p++ = '\r'; 118 | *p++ = '\n'; 119 | 120 | uart_send(buffer, p - buffer); 121 | } 122 | -------------------------------------------------------------------------------- /dump.c: -------------------------------------------------------------------------------- 1 | typedef unsigned char uint8_t; 2 | typedef unsigned int uint32_t; 3 | typedef uint32_t size_t; 4 | 5 | #define NULL ((void *)0) 6 | 7 | #define FLASH_START 0x08000000 8 | #define FLASH_SIZE 0x00180000 9 | 10 | // #define USART3_START 0x40004800 11 | #define UART7_START 0x40007800 12 | #define WWDG_START 0x40002c00 13 | 14 | typedef struct { 15 | volatile uint32_t CR; 16 | } WWDG_t; 17 | 18 | typedef struct { 19 | volatile uint32_t SR; 20 | volatile uint32_t DR; 21 | volatile uint32_t BRR; 22 | volatile uint32_t CR1; 23 | } UART_t; 24 | 25 | typedef uint32_t (*strtoul_t) (const char *, char **, uint32_t base); 26 | typedef void (*help_t) (void); 27 | 28 | static void dump_dataline(UART_t* uart, uint32_t addr, const uint8_t *data); 29 | static void uart_send(UART_t* uart, const char* data, size_t len); 30 | 31 | void 32 | dump(const char *args) 33 | { 34 | UART_t *uart = (void *)UART7_START; 35 | strtoul_t strtoul = (strtoul_t)(0x3f8c8 + 1); 36 | help_t help = (help_t)(0x35e04 + 1); 37 | char *end; 38 | uint32_t addr; 39 | uint32_t n; 40 | 41 | addr = strtoul(args, &end, 16); 42 | if (*end != ' ') { 43 | help(); 44 | return; 45 | } 46 | 47 | n = strtoul(end + 1, &end, 16); 48 | 49 | addr &= ~(0xf); 50 | n = (n + 0xf) & ~(0xf); 51 | 52 | uint32_t cr1 = uart->CR1; 53 | uart->CR1 = cr1 & ~(0x1f0); 54 | 55 | uint8_t *data = (uint8_t *) addr; 56 | while (n > 0) { 57 | dump_dataline(uart, addr, data); 58 | 59 | addr += 16; 60 | data += 16; 61 | n -= 16; 62 | } 63 | 64 | while (!(uart->SR & 0x40)) 65 | /* wait */; 66 | uart->CR1 = cr1; 67 | } 68 | 69 | static uint8_t byte_to_rec(char* buffer, uint8_t byte) 70 | { 71 | uint8_t nib = (byte >> 4) & 0x0f; 72 | if (nib < 10) 73 | buffer[0] = '0' + nib; 74 | else 75 | buffer[0] = 'A' + (nib - 10); 76 | nib = byte & 0x0f; 77 | if (nib < 10) 78 | buffer[1] = '0' + nib; 79 | else 80 | buffer[1] = 'A' + (nib - 10); 81 | return byte; 82 | } 83 | 84 | static void wdg(void) 85 | { 86 | WWDG_t *WWDG = (void *)WWDG_START; 87 | WWDG->CR = 0x7f; 88 | } 89 | 90 | static void uart_send(UART_t* uart, const char* data, size_t len) 91 | { 92 | size_t i; 93 | 94 | for (i = 0; i < len; i++) { 95 | while (!(uart->SR & 0x80)) 96 | /* wait */; 97 | uart->DR = data[i]; 98 | } 99 | 100 | wdg(); 101 | } 102 | 103 | static void 104 | dump_dataline(UART_t* uart, uint32_t addr, const uint8_t *data) 105 | { 106 | char buffer[80]; 107 | char *p = buffer; 108 | uint32_t i; 109 | 110 | byte_to_rec(p, (addr >> 24) & 0xff); p += 2; 111 | byte_to_rec(p, (addr >> 16) & 0xff); p += 2; 112 | byte_to_rec(p, (addr >> 8) & 0xff); p += 2; 113 | byte_to_rec(p, addr & 0xff); p += 2; 114 | *p++ = '\t'; 115 | 116 | for (i = 0; i < 16; i++) { 117 | if (i > 0) { 118 | *p++ = ' '; 119 | if (i == 8) { 120 | *p++ = ' '; 121 | *p++ = ' '; 122 | } 123 | } 124 | byte_to_rec(p, data[i]); p += 2; 125 | } 126 | *p++ = '\t'; 127 | 128 | for (i = 0; i < 16; i++) { 129 | if (i == 8) 130 | *p++ = ' '; 131 | if (0x1f < data[i] && data[i] < 0x7f) 132 | *p++ = data[i]; 133 | else 134 | *p++ = '.'; 135 | } 136 | *p++ = '\r'; 137 | *p++ = '\n'; 138 | 139 | uart_send(uart, buffer, p - buffer); 140 | } 141 | -------------------------------------------------------------------------------- /update.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import asyncio 3 | 4 | import bleak 5 | import zlib 6 | 7 | from pymoof.clients.sx3 import Sound 8 | from pymoof.clients.sx3 import SX3Client 9 | from pymoof.tools import discover_bike 10 | from pymoof.tools import retrieve_encryption_key 11 | from pymoof.util import bleak_utils 12 | 13 | import enum 14 | import math 15 | 16 | from cryptography.hazmat.primitives.ciphers import algorithms 17 | from cryptography.hazmat.primitives.ciphers import Cipher 18 | from cryptography.hazmat.primitives.ciphers import modes 19 | 20 | 21 | class Firmware(enum.Enum): 22 | SERVICE_UUID = '6acc5510-e631-4069-944d-b8ca7598ad50' 23 | 24 | METADATA = '6acc5511-e631-4069-944d-b8ca7598ad50' 25 | BLOCK = '6acc5512-e631-4069-944d-b8ca7598ad50' 26 | UNKNOWN_13 = '6acc5513-e631-4069-944d-b8ca7598ad50' 27 | 28 | 29 | async def firmware_update(client, key, name): 30 | with open(name, 'rb') as f: 31 | data = f.read() 32 | 33 | length = len(data) 34 | crc = zlib.crc32(data) & 0xffffffff 35 | 36 | print(f'file {name}: len {length} crc {hex(crc)}') 37 | 38 | cipher = Cipher(algorithms.AES(bytes.fromhex(key)), modes.ECB()) 39 | encryptor = cipher.encryptor() 40 | 41 | metadata = bytearray() 42 | metadata.append(0) 43 | metadata.append((length >> 24) & 0xff) 44 | metadata.append((length >> 16) & 0xff) 45 | metadata.append((length >> 8) & 0xff) 46 | metadata.append((length >> 0) & 0xff) 47 | metadata.append((crc >> 24) & 0xff) 48 | metadata.append((crc >> 16) & 0xff) 49 | metadata.append((crc >> 8) & 0xff) 50 | metadata.append((crc >> 0) & 0xff) 51 | print(f'metadata: {metadata.hex()}') 52 | 53 | await client._write(Firmware.METADATA, metadata) 54 | 55 | # Pad to the nearest cipher 16 byte block size 56 | pad = bytearray() 57 | for _ in range(math.ceil(len(data) / 16) * 16 - len(data)): 58 | pad.append(0xff) 59 | data = data + pad 60 | 61 | enc = bytes(encryptor.update(data) + encryptor.finalize()) 62 | length = len(enc) 63 | 64 | offset = 0 65 | while offset < length: 66 | chunk = enc[offset:offset+240] 67 | print(f'chunk {offset}/{length}: {len(chunk)} {chunk.hex()}') 68 | 69 | await bleak_utils.write_to_characteristic(client._gatt_client, Firmware.BLOCK, chunk) 70 | 71 | offset += 240 72 | 73 | 74 | async def update(): 75 | # print('Getting key from vanmoof servers') 76 | # key, user_key_id = retrieve_encryption_key.query() 77 | 78 | # Insert your API key here: 79 | key = '5f5f5f5f5f4f574e45525f5045524d53' 80 | 81 | user_key_id = 1 82 | print(f'key: {key}, user_key_id {user_key_id}') 83 | 84 | # Insert your manufacturer key here: 85 | mkey = '4638384135453030303030304d4f4f46' 86 | 87 | print('Discovering nearby vanmoof bikes') 88 | device = await discover_bike.query() 89 | if device is None: 90 | exit(1) 91 | 92 | print('Doing firmware update') 93 | async with bleak.BleakClient(device) as bleak_client: 94 | client = SX3Client(bleak_client, key, user_key_id) 95 | 96 | await client.authenticate() 97 | 98 | await client.play_sound(Sound.BEEP_POSITIVE) 99 | 100 | await firmware_update(client, mkey, sys.argv[1]) 101 | 102 | 103 | asyncio.run(update()) 104 | -------------------------------------------------------------------------------- /endian.h: -------------------------------------------------------------------------------- 1 | // 2 | // endian.h 3 | // 4 | // https://gist.github.com/panzi/6856583 5 | // 6 | // I, Mathias Panzenböck, place this file hereby into the public domain. Use 7 | // it at your own risk for whatever you like. In case there are 8 | // jurisdictions that don't support putting things in the public domain you 9 | // can also consider it to be "dual licensed" under the BSD, MIT and Apache 10 | // licenses, if you want to. This code is trivial anyway. Consider it an 11 | // example on how to get the endian conversion functions on different 12 | // platforms. 13 | 14 | #ifndef PORTABLE_ENDIAN_H__ 15 | #define PORTABLE_ENDIAN_H__ 16 | 17 | #if (defined(_WIN16) || defined(_WIN32) || defined(_WIN64)) && !defined(__WINDOWS__) 18 | 19 | # define __WINDOWS__ 20 | 21 | #endif 22 | 23 | #if defined(__linux__) || defined(__CYGWIN__) 24 | 25 | # include 26 | 27 | #elif defined(__APPLE__) 28 | 29 | # include 30 | 31 | # define htobe16(x) OSSwapHostToBigInt16(x) 32 | # define htole16(x) OSSwapHostToLittleInt16(x) 33 | # define be16toh(x) OSSwapBigToHostInt16(x) 34 | # define le16toh(x) OSSwapLittleToHostInt16(x) 35 | 36 | # define htobe32(x) OSSwapHostToBigInt32(x) 37 | # define htole32(x) OSSwapHostToLittleInt32(x) 38 | # define be32toh(x) OSSwapBigToHostInt32(x) 39 | # define le32toh(x) OSSwapLittleToHostInt32(x) 40 | 41 | # define htobe64(x) OSSwapHostToBigInt64(x) 42 | # define htole64(x) OSSwapHostToLittleInt64(x) 43 | # define be64toh(x) OSSwapBigToHostInt64(x) 44 | # define le64toh(x) OSSwapLittleToHostInt64(x) 45 | 46 | # define __BYTE_ORDER BYTE_ORDER 47 | # define __BIG_ENDIAN BIG_ENDIAN 48 | # define __LITTLE_ENDIAN LITTLE_ENDIAN 49 | # define __PDP_ENDIAN PDP_ENDIAN 50 | 51 | #elif defined(__OpenBSD__) 52 | 53 | # include 54 | 55 | #elif defined(__NetBSD__) || defined(__FreeBSD__) || defined(__DragonFly__) 56 | 57 | # include 58 | 59 | # define be16toh(x) betoh16(x) 60 | # define le16toh(x) letoh16(x) 61 | 62 | # define be32toh(x) betoh32(x) 63 | # define le32toh(x) letoh32(x) 64 | 65 | # define be64toh(x) betoh64(x) 66 | # define le64toh(x) letoh64(x) 67 | 68 | #elif defined(__WINDOWS__) 69 | 70 | # include 71 | # include 72 | 73 | # if BYTE_ORDER == LITTLE_ENDIAN 74 | 75 | # define htobe16(x) htons(x) 76 | # define htole16(x) (x) 77 | # define be16toh(x) ntohs(x) 78 | # define le16toh(x) (x) 79 | 80 | # define htobe32(x) htonl(x) 81 | # define htole32(x) (x) 82 | # define be32toh(x) ntohl(x) 83 | # define le32toh(x) (x) 84 | 85 | # define htobe64(x) htonll(x) 86 | # define htole64(x) (x) 87 | # define be64toh(x) ntohll(x) 88 | # define le64toh(x) (x) 89 | 90 | # elif BYTE_ORDER == BIG_ENDIAN 91 | 92 | /* that would be xbox 360 */ 93 | # define htobe16(x) (x) 94 | # define htole16(x) __builtin_bswap16(x) 95 | # define be16toh(x) (x) 96 | # define le16toh(x) __builtin_bswap16(x) 97 | 98 | # define htobe32(x) (x) 99 | # define htole32(x) __builtin_bswap32(x) 100 | # define be32toh(x) (x) 101 | # define le32toh(x) __builtin_bswap32(x) 102 | 103 | # define htobe64(x) (x) 104 | # define htole64(x) __builtin_bswap64(x) 105 | # define be64toh(x) (x) 106 | # define le64toh(x) __builtin_bswap64(x) 107 | 108 | # else 109 | 110 | # error byte order not supported 111 | 112 | # endif 113 | 114 | # define __BYTE_ORDER BYTE_ORDER 115 | # define __BIG_ENDIAN BIG_ENDIAN 116 | # define __LITTLE_ENDIAN LITTLE_ENDIAN 117 | # define __PDP_ENDIAN PDP_ENDIAN 118 | 119 | #else 120 | 121 | # error platform not supported 122 | 123 | #endif 124 | 125 | #endif 126 | -------------------------------------------------------------------------------- /unpack.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "pack.h" 10 | #include "endian.h" 11 | #include "mmap.h" /* for O_BINARY */ 12 | 13 | static char *progname; 14 | 15 | static void 16 | usage(void) 17 | { 18 | fprintf(stderr, "usage: %s \n", progname); 19 | exit(1); 20 | } 21 | 22 | int 23 | main(int argc, char **argv) 24 | { 25 | char buffer[8196]; 26 | char *packfile; 27 | int fd, out; 28 | struct stat st; 29 | pack_header_t header; 30 | pack_entry_t entry; 31 | size_t offset; 32 | size_t total; 33 | ssize_t n, m; 34 | int i; 35 | 36 | progname = strrchr(argv[0], '/'); 37 | if (progname) 38 | progname++; 39 | else 40 | progname = argv[0]; 41 | 42 | if (argc < 2) 43 | usage(); 44 | 45 | packfile = argv[1]; 46 | 47 | fd = open(packfile, O_RDONLY | O_BINARY); 48 | if (fd < 0) { 49 | fprintf(stderr, "%s: open(%s): %s\n", progname, packfile, strerror(errno)); 50 | exit(1); 51 | } 52 | 53 | if (fstat(fd, &st) < 0) { 54 | fprintf(stderr, "%s: fstat(%s): %s\n", progname, packfile, strerror(errno)); 55 | exit(1); 56 | } 57 | 58 | n = read(fd, &header, sizeof(header)); 59 | if (n != sizeof(header)) { 60 | fprintf(stderr, "%s: read(%zu): %zd\n", progname, sizeof(header), n); 61 | exit(1); 62 | } 63 | 64 | if (memcmp(header.magic, PACK_MAGIC, sizeof(header.magic))) { 65 | fprintf(stderr, "%s: %s: not a PACK file\n", progname, packfile); 66 | exit(1); 67 | } 68 | 69 | if (le32toh(header.offset) + le32toh(header.length) > st.st_size) { 70 | fprintf(stderr, "%s: WARNING: PACK offset 0x%08zx + length 0x%08x is beyond end of file 0x%08zx\n", 71 | progname, offset, le32toh(header.length), st.st_size); 72 | exit(1); 73 | } 74 | 75 | offset = le32toh(header.offset); 76 | for (i = 0; i < le32toh(header.length) / sizeof(entry); i++) { 77 | n = lseek(fd, offset, SEEK_SET); 78 | if (n != offset) { 79 | fprintf(stderr, "%s: seek(%u): %zd\n", progname, offset, n); 80 | exit(1); 81 | } 82 | 83 | n = read(fd, &entry, sizeof(entry)); 84 | if (n != sizeof(entry)) { 85 | fprintf(stderr, "%s: read(%zu): %zd\n", progname, sizeof(entry), n); 86 | exit(1); 87 | } 88 | 89 | if (le32toh(entry.offset) + le32toh(entry.length) > le32toh(header.offset)) { 90 | fprintf(stderr, "%s: file %s offset 0x%08zx + length 0x%08x is beyond start of PACK directoy 0x%08zx\n", 91 | progname, entry.filename, le32toh(entry.offset), le32toh(entry.length), le32toh(header.offset)); 92 | exit(1); 93 | } 94 | 95 | printf("file: %s, offset 0x%08x, length 0x%08x\n", entry.filename, 96 | le32toh(entry.offset), le32toh(entry.length)); 97 | 98 | out = open(entry.filename, O_WRONLY | O_BINARY | O_CREAT | O_TRUNC, 0666); 99 | if (out < 0) { 100 | fprintf(stderr, "%s: open(%s): %s\n", progname, packfile, strerror(errno)); 101 | exit(1); 102 | } 103 | 104 | n = lseek(fd, le32toh(entry.offset), SEEK_SET); 105 | if (n != le32toh(entry.offset)) { 106 | fprintf(stderr, "%s: seek(%u): %zd\n", progname, le32toh(entry.offset), n); 107 | exit(1); 108 | } 109 | 110 | total = 0; 111 | while (total < le32toh(entry.length)) { 112 | m = le32toh(entry.length) - total; 113 | if (m > sizeof(buffer)) 114 | m = sizeof(buffer); 115 | n = read(fd, buffer, m); 116 | if (n != m) { 117 | fprintf(stderr, "%s: read(%zu): %zd\n", progname, m, n); 118 | exit(1); 119 | } 120 | n = write(out, buffer, m); 121 | if (n != m) { 122 | fprintf(stderr, "%s: write(%zu): %zd\n", progname, m, n); 123 | exit(1); 124 | } 125 | total += n; 126 | } 127 | 128 | close(out); 129 | 130 | offset += sizeof(entry); 131 | } 132 | 133 | return 0; 134 | } 135 | -------------------------------------------------------------------------------- /pack.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "pack.h" 10 | #include "endian.h" 11 | #include "mmap.h" /* for O_BINARY */ 12 | 13 | static char *progname; 14 | 15 | static void 16 | usage(void) 17 | { 18 | fprintf(stderr, "usage: %s [ ...]\n", progname); 19 | exit(1); 20 | } 21 | 22 | int 23 | main(int argc, char **argv) 24 | { 25 | char buffer[8196]; 26 | char *packfile; 27 | int fd, out; 28 | struct stat st; 29 | pack_header_t header; 30 | pack_entry_t *entries; 31 | size_t offset; 32 | size_t total; 33 | ssize_t n, m; 34 | int i; 35 | 36 | progname = strrchr(argv[0], '/'); 37 | if (progname) 38 | progname++; 39 | else 40 | progname = argv[0]; 41 | 42 | if (argc < 3) 43 | usage(); 44 | 45 | packfile = argv[1]; 46 | 47 | entries = malloc((argc - 2) * sizeof(pack_entry_t)); 48 | if (entries == NULL) { 49 | fprintf(stderr, "%s: malloc(%zu): Out of memory\n", progname, (argc - 2) * sizeof(pack_entry_t)); 50 | exit(1); 51 | } 52 | memset(entries, 0, (argc - 2) * sizeof(pack_entry_t)); 53 | 54 | out = open(packfile, O_WRONLY | O_BINARY | O_CREAT | O_TRUNC, 0666); 55 | if (out < 0) { 56 | fprintf(stderr, "%s: open(%s): %s\n", progname, packfile, strerror(errno)); 57 | exit(1); 58 | } 59 | 60 | memcpy(header.magic, PACK_MAGIC, sizeof(header.magic)); 61 | header.length = htole32((argc - 2) * sizeof(pack_entry_t)); 62 | 63 | n = write(out, &header, sizeof(header)); 64 | if (n != sizeof(header)) { 65 | fprintf(stderr, "%s: write(%zu): %zd\n", progname, sizeof(header), n); 66 | exit(1); 67 | } 68 | 69 | offset = sizeof(header); 70 | for (i = 0; i < argc - 2; i++) { 71 | fd = open(argv[i + 2], O_RDONLY | O_BINARY); 72 | if (fd < 0) { 73 | fprintf(stderr, "%s: open(%s): %s\n", progname, argv[i + 2], strerror(errno)); 74 | exit(1); 75 | } 76 | 77 | if (fstat(fd, &st) < 0) { 78 | fprintf(stderr, "%s: fstat(%s): %s\n", progname, argv[i + 2], strerror(errno)); 79 | exit(1); 80 | } 81 | 82 | char *basename = strrchr(argv[i + 2], '/'); 83 | if (basename) 84 | basename++; 85 | else 86 | basename = argv[i + 2]; 87 | strncpy(entries[i].filename, basename, sizeof(entries[i].filename)); 88 | entries[i].offset = htole32(offset); 89 | entries[i].length = htole32(st.st_size); 90 | 91 | printf("file: %s, offset 0x%08x, length 0x%08x\n", entries[i].filename, 92 | le32toh(entries[i].offset), le32toh(entries[i].length)); 93 | 94 | total = 0; 95 | while (total < st.st_size) { 96 | m = st.st_size - total; 97 | if (m > sizeof(buffer)) 98 | m = sizeof(buffer); 99 | n = read(fd, buffer, m); 100 | if (n != m) { 101 | fprintf(stderr, "%s: read(%zu): %zd\n", progname, m, n); 102 | exit(1); 103 | } 104 | n = write(out, buffer, m); 105 | if (n != m) { 106 | fprintf(stderr, "%s: write(%zu): %zd\n", progname, m, n); 107 | exit(1); 108 | } 109 | total += n; 110 | } 111 | 112 | close(fd); 113 | 114 | offset += st.st_size; 115 | 116 | switch (offset & 3) { 117 | case 1: 118 | write(out, "\0", 1); 119 | offset++; 120 | case 2: 121 | write(out, "\0", 1); 122 | offset++; 123 | case 3: 124 | write(out, "\0", 1); 125 | offset++; 126 | } 127 | } 128 | 129 | header.offset = htole32(offset); 130 | 131 | n = write(out, entries, le32toh(header.length)); 132 | if (n != le32toh(header.length)) { 133 | fprintf(stderr, "%s: write(%zu): %zd\n", progname, le32toh(header.length), n); 134 | exit(1); 135 | } 136 | 137 | n = lseek(out, 0, SEEK_SET); 138 | if (n != 0) { 139 | fprintf(stderr, "%s: seek(%u): %zd\n", progname, 0, n); 140 | exit(1); 141 | } 142 | 143 | n = write(out, &header, sizeof(header)); 144 | if (n != sizeof(header)) { 145 | fprintf(stderr, "%s: write(%zu): %zd\n", progname, sizeof(header), n); 146 | exit(1); 147 | } 148 | 149 | return 0; 150 | } 151 | -------------------------------------------------------------------------------- /crc32.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | #include "mmap.h" 13 | #include "endian.h" 14 | #include "ware.h" 15 | 16 | static char *progname; 17 | 18 | static void 19 | usage(void) 20 | { 21 | fprintf(stderr, "usage: %s \n", progname); 22 | exit(1); 23 | } 24 | 25 | static const uint32_t crc_poly = 0x4c11db7; 26 | static const uint32_t initial_crc = 0xffffffff; 27 | 28 | static uint32_t crc32_calculate(uint32_t crc, const void *data, size_t length) 29 | { 30 | const uint32_t *p = data; 31 | size_t i, b; 32 | 33 | for (i = 0; i < length; i += sizeof(uint32_t)) { 34 | crc ^= *p++; 35 | for (b = 0; b < 32; b++) { 36 | if (crc & (1 << 31)) 37 | crc = (crc << 1) ^ crc_poly; 38 | else 39 | crc <<= 1; 40 | } 41 | } 42 | 43 | return crc; 44 | } 45 | 46 | static uint32_t ware_crc(uint32_t crc, const vanmoof_ware_t *ware, const void *data, size_t length) 47 | { 48 | vanmoof_ware_t tmp; 49 | 50 | memcpy(&tmp, ware, sizeof(tmp)); 51 | tmp.crc = 0xffffffff; 52 | tmp.length = 0xffffffff; 53 | 54 | crc = crc32_calculate(crc, &tmp, sizeof(tmp)); 55 | 56 | crc = crc32_calculate(crc, data + sizeof(tmp), length - sizeof(tmp)); 57 | 58 | return crc; 59 | } 60 | 61 | int main(int argc, char** argv) 62 | { 63 | progname = strrchr(argv[0], '/'); 64 | if (progname) 65 | progname++; 66 | else 67 | progname = argv[0]; 68 | 69 | if (argc < 2) 70 | usage(); 71 | 72 | char *filename = argv[1]; 73 | 74 | int fd = open(filename, O_RDONLY | O_BINARY); 75 | if (fd < 0) { 76 | fprintf(stderr, "%s: open(%s): %s\n", progname, filename, strerror(errno)); 77 | exit(1); 78 | } 79 | 80 | struct stat st; 81 | if (fstat(fd, &st) < 0) { 82 | fprintf(stderr, "%s: stat(%s): %s\n", progname, filename, strerror(errno)); 83 | exit(1); 84 | } 85 | 86 | void *data = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0); 87 | if (data == (void *)-1) { 88 | fprintf(stderr, "%s: mmap(%s): %s\n", progname, filename, strerror(errno)); 89 | exit(1); 90 | } 91 | 92 | close(fd); 93 | 94 | vanmoof_ware_t ware; 95 | memcpy(&ware, data, sizeof(ware)); 96 | 97 | ble_ware_t ble_ware; 98 | memcpy(&ble_ware, data, sizeof(ble_ware)); 99 | 100 | if (le32toh(ware.magic) == WARE_MAGIC) { 101 | printf("%s: vanmoof ware magic OK\n", filename); 102 | printf("%s: vanmoof ware version %08x\n", filename, le32toh(ware.version)); 103 | printf("%s: vanmoof ware CRC 0x%08x\n", filename, le32toh(ware.crc)); 104 | printf("%s: vanmoof ware length 0x%08x\n", filename, le32toh(ware.length)); 105 | printf("%s: vanmoof ware date %s\n", filename, ware.date); 106 | printf("%s: vanmoof ware time %s\n", filename, ware.time); 107 | 108 | uint32_t length = le32toh(ware.length); 109 | if (length > st.st_size) { 110 | printf("%s: vanmoof ware length 0x%08x extends beyond file size 0x%08zx\n", 111 | filename, length, st.st_size); 112 | exit(1); 113 | } 114 | 115 | uint32_t crc = ware_crc(initial_crc, &ware, data, length); 116 | 117 | printf("%s: CRC 0x%08x %s\n", filename, crc, crc == le32toh(ware.crc) ? "OK" : "FAIL"); 118 | 119 | if (crc != le32toh(ware.crc)) 120 | exit(1); 121 | } else if (memcmp(ble_ware.magic, BLE_WARE_MAGIC, sizeof(ble_ware.magic)) == 0) { 122 | printf("%s: BLE ware magic OK\n", filename); 123 | printf("%s: BLE ware version %08x\n", filename, le32toh(ble_ware.soft_ver)); 124 | printf("%s: BLE ware CRC 0x%08x\n", filename, le32toh(ble_ware.crc)); 125 | printf("%s: BLE ware length 0x%08x\n", filename, le32toh(ble_ware.len)); 126 | 127 | uint32_t length = le32toh(ble_ware.len); 128 | if (length > st.st_size) { 129 | printf("%s: BLE ware length 0x%08x extends beyond file size 0x%08zx\n", 130 | filename, length, st.st_size); 131 | exit(1); 132 | } 133 | 134 | uint32_t crc = crc32(0, data + 12, length - 12); 135 | 136 | printf("%s: CRC 0x%08x %s\n", filename, crc, crc == le32toh(ble_ware.crc) ? "OK" : "FAIL"); 137 | 138 | if (crc != le32toh(ble_ware.crc)) 139 | exit(1); 140 | 141 | printf("%s: BLE ware entry 0x%08x\n", filename, le32toh(ble_ware.prg_entry)); 142 | printf("%s: BLE ware hdr len 0x%08x\n", filename, le32toh(ble_ware.hdr_len)); 143 | 144 | ble_ware_seg_t seg; 145 | size_t offset = le32toh(ble_ware.hdr_len); 146 | 147 | while (offset < st.st_size) { 148 | memcpy(&seg, data + offset, sizeof(ble_ware_seg_t)); 149 | printf("%s: BLE ware seg type 0x%02x\n", filename, seg.seg_type); 150 | printf("%s: BLE ware seg len 0x%08x\n", filename, le32toh(seg.seg_len)); 151 | 152 | if (seg.seg_type == BLE_SEG_TYPE_SECURITY) { 153 | ble_ware_signature_seg_t sig; 154 | 155 | memcpy(&sig, data + offset + sizeof(ble_ware_seg_t), 156 | le32toh(seg.seg_len) - sizeof(ble_ware_seg_t)); 157 | printf("%s: BLE ware signature ver 0x%02x\n", filename, sig.sig_ver); 158 | printf("%s: BLE ware signature timestamp 0x%08x\n", filename, le32toh(sig.timestamp)); 159 | printf("%s: BLE ware signature signer %02x %02x %02x %02x %02x %02x %02x %02x\n", 160 | filename, sig.ecdsa_signer[0], sig.ecdsa_signer[1], sig.ecdsa_signer[2], 161 | sig.ecdsa_signer[3], sig.ecdsa_signer[4], sig.ecdsa_signer[5], 162 | sig.ecdsa_signer[6], sig.ecdsa_signer[7]); 163 | printf("%s: BLE ware signature %02x %02x %02x %02x ...\n", 164 | filename, sig.ecdsa_signature[0], sig.ecdsa_signature[1], 165 | sig.ecdsa_signature[2], sig.ecdsa_signature[3]); 166 | } 167 | 168 | offset += le32toh(seg.seg_len); 169 | } 170 | } else { 171 | printf("%s: vanmoof ware magic not found, assume boot-loader binary\n", filename); 172 | 173 | uint32_t expected_crc = le32toh(*(uint32_t *)(data + st.st_size - sizeof(uint32_t))); 174 | uint8_t *version = (uint8_t *)(data + st.st_size - 2 * sizeof(uint32_t)); 175 | 176 | printf("%s: version %c%c%c\n", filename, version[3], version[2], version[1]); 177 | printf("%s: expected CRC 0x%08x\n", filename, expected_crc); 178 | 179 | uint32_t crc = crc32_calculate(initial_crc, data, st.st_size - sizeof(uint32_t)); 180 | 181 | printf("%s: CRC 0x%08x %s\n", filename, crc, crc == expected_crc ? "OK" : "FAIL"); 182 | 183 | if (crc != expected_crc) 184 | exit(1); 185 | } 186 | 187 | return 0; 188 | } 189 | -------------------------------------------------------------------------------- /vanmoof_sx3_sound_bitmask.md: -------------------------------------------------------------------------------- 1 | # VanMoof Sound Bitmask Documentation 2 | 3 | ## Table of Contents 4 | - [Overview](#overview) 5 | - [Configuration Partition and SRAM Mapping](#configuration-partition-and-sram-mapping) 6 | - [Sound Indexes](#sound-indexes) 7 | - [Special Cases](#special-cases) 8 | - [SX3 Behavior](#sx3-behavior) 9 | - [Tables](#tables) 10 | 11 | ## Overview 12 | The firmware for SX3 and SX4 models manages sounds using three **volume group bitmasks**. 13 | 14 | - Bitmask for volume group **Low** 15 | - Bitmask for volume group **Medium** 16 | - Bitmask for volume group **High** 17 | 18 | Each bitmask is 32 bits wide, where each **bit position corresponds to one sound**. 19 | If a bit is set (`1`), that sound is assigned to the respective volume group. 20 | A sound can belong to **exactly one group** at a time – if it is assigned to Medium, it will not be set in Low or High. 21 | 22 | ## Configuration Partition and SRAM Mapping 23 | 24 | Both the bitmasks and the volume levels are stored on the **configuration partition** in flash: 25 | - Primary: `0x08008000` 26 | - Backup: `0x0800C000` 27 | 28 | At startup, the primary partition is copied into **SRAM** at offsets after base address `0x20000A00`. 29 | All offsets documented here (e.g., `+0x0F4`) are relative to this SRAM base. 30 | 31 | Example: `0x20000A00 + 0x0F4` → bitmask for sounds in group _Low_. 32 | 33 | The **volume group bitmasks** are stored at following offsets: 34 | - **+0x0F4** → Bitmask for volume group **Low** 35 | - **+0x0F8** → Bitmask for volume group **Medium** 36 | - **+0x0FC** → Bitmask for volume group **High** 37 | 38 | The actual **volume levels** for each group are stored separately: 39 | - **+0x105** → Volume level for **Group Low** (Default: 20) 40 | - **+0x106** → Volume level for **Group Medium** (Default: 30) 41 | - **+0x107** → Volume level for **Group High** (Default: 38) 42 | 43 | ## Sound Indexes 44 | 45 | Two different index identifiers exist: 46 | 47 | - **BLEware index** → used by BLEware commands, e.g. 48 | `audio-play 23` 49 | plays the sound at index `23` (hex 0x17, Mainware index `N`) 50 | 51 | - **Mainware index** → used by Mainware commands, e.g. 52 | `sound N` 53 | plays the sound at index `N` (hex 0x17, BLEware index `23`). 54 | 55 | Both indexes are shown in the tables for reference. 56 | 57 | ## Special Cases 58 | 59 | - **No Sound Entries** → Positions **0** and **31** have no assigned sound. 60 | - In firmware ≤ 1.8.2, they are not set in any bitmask. 61 | - In firmware ≥ 1.9.1, they are set in the **Medium** group. The reason is unknown; setting these bits has no audible effect. 62 | 63 | - **Loud Noise** → Position **26** has changed 64 | - Firmware ≤ 1.8.2: correctly assigned to **High** (intended as a loud anti-theft locator sound for bike hunters). 65 | - Firmware ≥ 1.9.1: moved to **Medium**, which contradicts its purpose as a loud sound. 66 | 67 | - **Firmware Update Success** → Position **25** not logical 68 | - Present in both firmware generations. 69 | - Currently mapped to **High**, but logically belongs to **Medium** like all other firmware-related sounds (Download, Update Failed, etc.). 70 | 71 | - **Flute (Horn sound)** → Position **11** exists in the sound table, but cannot be selected as the default bicycle bell tone in firmware. 72 | 73 | - **Find-My Sounds** → (e.g., **Pairing Loop**, **Pairing Success**, **Pairing Failed**, **Locate Sound**) 74 | - Available only on bikes of the **2021 generation** equipped with **BLEware 2.4.1**. 75 | - On older bikes with **BLEware 1.x** (2020 models), the corresponding index slots (**5, 8, 9, 27–30**) are present but completely **empty**. 76 | - This suggests that VanMoof had already reserved these slots for Find-My on the 2020 generation, but did not enable them due to the missing Apple certification. 77 | 78 | ## SX3 Behavior 79 | 80 | - On SX3 models, **only Medium and High groups are effectively used**. 81 | - The **Low group bitmask is always `0x00000000`**, meaning no sounds are mapped to Low. 82 | - Volume levels (+0x105 … +0x107) still apply if Low were ever to be used. 83 | 84 | ## Tables 85 | See below for the detailed sound mapping: 86 | 87 | ## SX3 – Firmware up to 1.8.2 88 | 89 | | Bit | Sound Name | BLE Idx | Main Idx | Low | Medium | High | 90 | |-----|-------------------------|---------|----------|-----|--------|------| 91 | | 0 | --- no sound --- | 0 | 0 | | | | 92 | | 1 | Click | 1 | 1 | | ✔️ | | 93 | | 2 | Error | 2 | 2 | | ✔️ | | 94 | | 3 | Ding | 3 | 3 | | ✔️ | | 95 | | 4 | Unlock Countdown | 4 | 4 | | ✔️ | | 96 | | 5 | Find-My Pairing Loop | 5 | 5 | | ✔️ | | 97 | | 6 | Enter PIN | 6 | 6 | | ✔️ | | 98 | | 7 | Reset | 7 | 7 | | ✔️ | | 99 | | 8 | Find-My Pairing Success | 8 | 8 | | ✔️ | | 100 | | 9 | Find-My Pairing Failed | 9 | 9 | | ✔️ | | 101 | | 10 | Horn: Submarine | 10 | A | | | ✔️ | 102 | | 11 | Horn: Flute | 11 | B | | | ✔️ | 103 | | 12 | Locked | 12 | C | | ✔️ | | 104 | | 13 | Unlock | 13 | D | | ✔️ | | 105 | | 14 | Alarm Stage 1 | 14 | E | | | ✔️ | 106 | | 15 | Alarm Stage 2 | 15 | F | | | ✔️ | 107 | | 16 | Startup | 16 | G | | ✔️ | | 108 | | 17 | Shutdown | 17 | H | | ✔️ | | 109 | | 18 | Charging | 18 | I | | ✔️ | | 110 | | 19 | Transfer Diag Log | 19 | J | | ✔️ | | 111 | | 20 | Firmware Download | 20 | K | | ✔️ | | 112 | | 21 | Firmware Update Success | 21 | L | | ✔️ | | 113 | | 22 | Horn: Ding Dong | 22 | M | | | ✔️ | 114 | | 23 | Horn: Troot | 23 | N | | | ✔️ | 115 | | 24 | Horn: Foghorn/Ping | 24 | O | | | ✔️ | 116 | | 25 | Firmware Update Failed | 25 | P | | | ✔️ | 117 | | 26 | Loud Noise | 26 | Q | | | ✔️ | 118 | | 27 | Find-My Unpair | 27 | R | | ✔️ | | 119 | | 28 | Find-My Disable | 28 | S | | ✔️ | | 120 | | 29 | Find-My Enable | 29 | T | | ✔️ | | 121 | | 30 | Find-My Locate Sound | 30 | U | | | ✔️ | 122 | | 31 | --- no sound --- | 31 | V | | | | 123 | 124 | The default values result in following bitmasks: 125 | 126 | Volume Group Low: 0x00000000 127 | Volume Group Medium: 0x383F33FE 128 | Volume Group High: 0x47C0CC00 129 | 130 | ### SX3 – Firmware 1.9.1 and higher 131 | 132 | | Bit | Sound Name | BLE Idx | Main Idx | Low | Medium | High | 133 | |-----|-------------------------|---------|----------|-----|--------|------| 134 | | 0 | --- no sound --- | 0 | 0 | | ✔️ | | 135 | | 1 | Click | 1 | 1 | | ✔️ | | 136 | | 2 | Error | 2 | 2 | | ✔️ | | 137 | | 3 | Ding | 3 | 3 | | ✔️ | | 138 | | 4 | Unlock Countdown | 4 | 4 | | ✔️ | | 139 | | 5 | Find-My Pairing Loop | 5 | 5 | | ✔️ | | 140 | | 6 | Enter PIN | 6 | 6 | | ✔️ | | 141 | | 7 | Reset | 7 | 7 | | ✔️ | | 142 | | 8 | Find-My Pairing Success | 8 | 8 | | ✔️ | | 143 | | 9 | Find-My Pairing Failed | 9 | 9 | | ✔️ | | 144 | | 10 | Horn: Submarine | 10 | A | | | ✔️ | 145 | | 11 | Horn: Flute | 11 | B | | | ✔️ | 146 | | 12 | Locked | 12 | C | | ✔️ | | 147 | | 13 | Unlock | 13 | D | | ✔️ | | 148 | | 14 | Alarm Stage 1 | 14 | E | | | ✔️ | 149 | | 15 | Alarm Stage 2 | 15 | F | | | ✔️ | 150 | | 16 | Startup | 16 | G | | ✔️ | | 151 | | 17 | Shutdown | 17 | H | | ✔️ | | 152 | | 18 | Charging | 18 | I | | ✔️ | | 153 | | 19 | Transfer Diag Log | 19 | J | | ✔️ | | 154 | | 20 | Firmware Download | 20 | K | | ✔️ | | 155 | | 21 | Firmware Update Success | 21 | L | | ✔️ | | 156 | | 22 | Horn: Ding Dong | 22 | M | | | ✔️ | 157 | | 23 | Horn: Troot | 23 | N | | | ✔️ | 158 | | 24 | Horn: Foghorn/Ping | 24 | O | | | ✔️ | 159 | | 25 | Firmware Update Failed | 25 | P | | | ✔️ | 160 | | 26 | Loud Noise | 26 | Q | | ✔️ | | 161 | | 27 | Find-My Unpair | 27 | R | | ✔️ | | 162 | | 28 | Find-My Disable | 28 | S | | ✔️ | | 163 | | 29 | Find-My Enable | 29 | T | | ✔️ | | 164 | | 30 | Find-My Locate Sound | 30 | U | | | ✔️ | 165 | | 31 | --- no sound --- | 31 | V | | ✔️ | | 166 | 167 | The default values result in following bitmasks: 168 | 169 | Volume Group Low: 0x00000000 170 | Volume Group Medium: 0xBC3F33FF 171 | Volume Group High: 0x43C0CC00 172 | -------------------------------------------------------------------------------- /keys.c: -------------------------------------------------------------------------------- 1 | typedef unsigned char uint8_t; 2 | typedef unsigned short uint16_t; 3 | typedef unsigned int uint32_t; 4 | typedef uint32_t size_t; 5 | 6 | #ifdef VERSION_1_4_1 7 | #define LOGGER (0x6d90 + 1) 8 | #define READ_EXTFLASH (0x1c5a4 + 1) 9 | #define GET_KEY (0x20bb8 + 1) 10 | #define SHOW_HELP (0x21244 + 1) 11 | #define SSCANF (0x23838 + 1) 12 | #define SYSTEM_PUTCHAR (0x260f8 + 1) 13 | #endif 14 | 15 | #ifdef VERSION_2_4_1 16 | #define LOGGER (0x7714 + 1) 17 | #define READ_EXTFLASH (0x21640 + 1) 18 | #define GET_KEY (0x26ea2 + 1) 19 | #define SHOW_HELP (0x27744 + 1) 20 | #define SSCANF (0x2a670 + 1) 21 | #define SYSTEM_PUTCHAR (0x2dbc8 + 1) 22 | #endif 23 | 24 | #define WDT 0x40080000 25 | #define JTAGCFG 0x40090034 26 | 27 | typedef struct { 28 | __volatile__ uint32_t LOAD; 29 | __volatile__ uint32_t VALUE; 30 | __volatile__ uint32_t CTL; 31 | __volatile__ uint32_t ICR; 32 | __volatile__ uint32_t RIS; 33 | __volatile__ uint32_t MIS; 34 | uint32_t unused0[0x100]; 35 | __volatile__ uint32_t TEST; 36 | __volatile__ uint32_t INT_CAUS; 37 | uint32_t unused1[0x1f8]; 38 | __volatile__ uint32_t LOCK; 39 | } WDT_t; 40 | 41 | typedef void (*logger_t) (const char* file, int lineno, const char* function, uint32_t flags, const char* fmt, ...); 42 | typedef int (*get_key_t) (unsigned int index, uint8_t *data); 43 | typedef int (*read_extflash_t) (uint32_t addr, size_t len, uint8_t *data); 44 | typedef void (*show_help_t) (const char*, const char*); 45 | typedef int (*sscanf_t) (const char *str, const char *fmt, ...); 46 | typedef void (*system_putchar_t) (char c); 47 | 48 | static void strcpy(char *, const char *); 49 | static int strcmp(const char *, const char *); 50 | static int strncmp(const char *, const char *, size_t); 51 | 52 | static int usage(const char *name, const char *args); 53 | 54 | static int dump_keys(const char *args); 55 | static int dump_mem(const char *args); 56 | static int dump_extflash(const char *args); 57 | 58 | static int patch_ble_boot(const char *args); 59 | 60 | int 61 | dump(int what, char *cmdline) 62 | { 63 | static const char text[] = "dump keys/memory/extflash"; 64 | static const char file[] = __FILE__; 65 | show_help_t show_help = (show_help_t)SHOW_HELP; 66 | logger_t logger = (logger_t)LOGGER; 67 | 68 | switch (what) { 69 | case 0: 70 | show_help("dump", text); 71 | return 0; 72 | 73 | case 1: 74 | strcpy(cmdline, "dump"); 75 | return 0; 76 | 77 | case 2: 78 | if (strncmp(cmdline, "dump", 4) != 0) 79 | return 2; 80 | 81 | if (cmdline[4] == ' ') { 82 | char *args = cmdline + 5; 83 | 84 | if (strcmp(args, "keys") == 0) 85 | return dump_keys(args); 86 | 87 | if (strncmp(args, "mem", 3) == 0) 88 | return dump_mem(args); 89 | 90 | if (strncmp(args, "extflash", 8) == 0) 91 | return dump_extflash(args); 92 | 93 | if (strncmp(args, "ccfg", 4) == 0) 94 | return patch_ble_boot(args); 95 | } 96 | 97 | return usage("dump", ""); 98 | 99 | default: 100 | return 1; 101 | } 102 | } 103 | 104 | static int 105 | usage(const char *name, const char *args) 106 | { 107 | logger_t logger = (logger_t)LOGGER; 108 | 109 | logger(__FILE__, __LINE__, name, 9, "usage: %s %s", name, args); 110 | return 2; 111 | } 112 | 113 | static void 114 | wdg(void) 115 | { 116 | WDT_t *pWDT = (WDT_t *)WDT; 117 | 118 | while (pWDT->LOCK == 1) { 119 | pWDT->LOCK = 0x1acce551; 120 | } 121 | pWDT->ICR = 0; 122 | pWDT->LOCK = 0; 123 | } 124 | 125 | static int 126 | dump_keys(const char* args) 127 | { 128 | static const char fmt[] = "Key 0x%02x: %s %02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x %08x %08x CRC %08x"; 129 | get_key_t get_key = (get_key_t)GET_KEY; 130 | logger_t logger = (logger_t)LOGGER; 131 | uint8_t key_data[32]; 132 | unsigned int i; 133 | 134 | for (i = 0; i < 0x80; i++) { 135 | if (get_key(i, key_data)) { 136 | uint32_t *p0 = (uint32_t*)(key_data + 16); 137 | uint32_t *p1 = (uint32_t*)(key_data + 20); 138 | uint32_t *crc = (uint32_t*)(key_data + 28); 139 | char type[5]; 140 | type[0] = key_data[24]; 141 | type[1] = key_data[25]; 142 | type[2] = key_data[26]; 143 | type[3] = key_data[27]; 144 | type[4] = '\0'; 145 | logger(__FILE__, __LINE__, __FUNCTION__, 9, fmt, i, type, 146 | key_data[0], key_data[1], key_data[2], key_data[3], 147 | key_data[4], key_data[5], key_data[6], key_data[7], 148 | key_data[8], key_data[9], key_data[10], key_data[11], 149 | key_data[12], key_data[13], key_data[14], key_data[15], 150 | *p0, *p1, *crc); 151 | } 152 | } 153 | 154 | return 0; 155 | } 156 | 157 | static void 158 | dump_dataline(uint32_t addr, const uint8_t *data) 159 | { 160 | static const char fmt[] = "%08x\t%02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x\t%s"; 161 | logger_t logger = (logger_t)LOGGER; 162 | uint32_t i, j; 163 | 164 | char text[16 + 2]; 165 | for (i = j = 0; i < 16; i++, j++) { 166 | if (i == 8) 167 | text[j++] = ' '; 168 | if (0x1f < data[i] && data[i] < 0x7f) 169 | text[j] = data[i]; 170 | else 171 | text[j] = '.'; 172 | } 173 | text[j] = '\0'; 174 | 175 | logger(__FILE__, __LINE__, __FUNCTION__, 9, fmt, addr, 176 | data[0], data[1], data[2], data[3], 177 | data[4], data[5], data[6], data[7], 178 | data[8], data[9], data[10], data[11], 179 | data[12], data[13], data[14], data[15], 180 | text); 181 | } 182 | 183 | static int 184 | dump_mem(const char *args) 185 | { 186 | sscanf_t sscanf = (sscanf_t)SSCANF; 187 | uint32_t addr; 188 | uint32_t n; 189 | 190 | if (sscanf(args, "mem %x %x", &addr, &n) != 2) { 191 | return usage("dump mem", " "); 192 | } 193 | 194 | addr &= ~(0xf); 195 | n = (n + 0xf) & ~(0xf); 196 | 197 | uint8_t *data = (uint8_t *) addr; 198 | while (n > 0) { 199 | dump_dataline(addr, data); 200 | 201 | wdg(); 202 | 203 | addr += 16; 204 | data += 16; 205 | n -= 16; 206 | } 207 | 208 | return 0; 209 | } 210 | 211 | static int 212 | dump_extflash(const char *args) 213 | { 214 | read_extflash_t read_extflash = (read_extflash_t)READ_EXTFLASH; 215 | sscanf_t sscanf = (sscanf_t)SSCANF; 216 | uint8_t data[16]; 217 | uint32_t addr; 218 | uint32_t n; 219 | 220 | if (sscanf(args, "extflash %x %x", &addr, &n) != 2) { 221 | return usage("dump extflash", " "); 222 | } 223 | 224 | addr &= ~(0xf); 225 | n = (n + 0xf) & ~(0xf); 226 | 227 | while (n > 0) { 228 | read_extflash(addr, 16, data); 229 | dump_dataline(addr, data); 230 | 231 | wdg(); 232 | 233 | addr += 16; 234 | n -= 16; 235 | } 236 | 237 | return 0; 238 | } 239 | 240 | #define ROM_API_TABLE ((uint32_t *) 0x10000180) 241 | 242 | #define ROM_API_FLASH_TABLE ((uint32_t*) (ROM_API_TABLE[10])) 243 | #define ROM_API_UART_TABLE ((uint32_t*) (ROM_API_TABLE[20])) 244 | #define ROM_API_VIMS_TABLE ((uint32_t*) (ROM_API_TABLE[22])) 245 | 246 | #define ROM_FlashSectorErase \ 247 | ((uint32_t (*)(uint32_t ui32SectorAddress)) \ 248 | ROM_API_FLASH_TABLE[5]) 249 | 250 | #define ROM_FlashProgram \ 251 | ((uint32_t (*)(uint8_t *pui8DataBuffer, uint32_t ui32Address, uint32_t ui32Count)) \ 252 | ROM_API_FLASH_TABLE[6]) 253 | 254 | #define FLASH_FCFG_B0_SSIZE0 0x40032430 255 | 256 | #define UART1 0x4000b000 257 | 258 | struct UART { 259 | volatile uint32_t DR; 260 | volatile uint32_t RSR_ECR; 261 | uint32_t unused0[4]; 262 | volatile uint32_t FR; 263 | uint32_t unused1[2]; 264 | volatile uint32_t IBRD; 265 | volatile uint32_t FBRD; 266 | volatile uint32_t LCRH; 267 | volatile uint32_t CTL; 268 | volatile uint32_t IFLS; 269 | volatile uint32_t IMSC; 270 | volatile uint32_t RIS; 271 | volatile uint32_t MIS; 272 | volatile uint32_t ICR; 273 | volatile uint32_t DMACTL; 274 | }; 275 | 276 | #define ROM_VIMSModeSet \ 277 | ((void (*)(uint32_t ui32Base, uint32_t ui32Mode)) \ 278 | ROM_API_VIMS_TABLE[1]) 279 | 280 | #define ROM_VIMSModeGet \ 281 | ((uint32_t (*)(uint32_t ui32Base)) \ 282 | ROM_API_VIMS_TABLE[2]) 283 | 284 | #define VIMS 0x40034000 285 | 286 | static uint32_t vims_enable(void) 287 | { 288 | uint32_t mode = ROM_VIMSModeGet(VIMS) & 0xff; 289 | if (mode != 0) { 290 | ROM_VIMSModeSet(VIMS, 0); 291 | while (ROM_VIMSModeGet(VIMS) != 0) 292 | /* wait */; 293 | } 294 | return mode; 295 | } 296 | 297 | static uint32_t vims_mode_set(uint32_t mode) 298 | { 299 | if (mode != 0) { 300 | ROM_VIMSModeSet(VIMS, 1); 301 | } 302 | } 303 | 304 | static int flash_sector_erase(uint32_t address) 305 | { 306 | uint32_t mode = vims_enable(); 307 | int res = ROM_FlashSectorErase(address); 308 | vims_mode_set(mode); 309 | return res; 310 | } 311 | 312 | static int flash_program(uint8_t *data, uint32_t address, size_t len) 313 | { 314 | uint32_t mode = vims_enable(); 315 | int res = ROM_FlashProgram(data, address, len); 316 | vims_mode_set(mode); 317 | return res; 318 | } 319 | 320 | #define CCFG_BASE 0x57f00 321 | #define CCFG_BIM_DATE 0x3c 322 | #define CCFG_BIM_TIME 0x48 323 | #define CCFG_TI_OPTIONS 0xe0 324 | #define CCFG_TAP_DAP_0 0xe4 325 | #define CCFG_TAP_DAP_1 0xe8 326 | 327 | static void *memcpy(void *dst, const void *src, size_t len); 328 | 329 | static int patch_ble_boot(const char* args) 330 | { 331 | uint32_t *pTIOptions = (uint32_t *)(CCFG_BASE + CCFG_TI_OPTIONS); 332 | uint32_t *pTapDap0 = (uint32_t *)(CCFG_BASE + CCFG_TAP_DAP_0); 333 | uint32_t *pTapDap1 = (uint32_t *)(CCFG_BASE + CCFG_TAP_DAP_1); 334 | uint32_t *jtagcfg = (uint32_t *)JTAGCFG; 335 | logger_t logger = (logger_t)LOGGER; 336 | uint32_t sector_size; 337 | uint32_t last_sector = 0x56000; 338 | uint32_t tmp_dst = 0x46000; 339 | uint8_t data[256]; 340 | uint32_t src_addr; 341 | uint32_t dst_addr; 342 | uint32_t total; 343 | 344 | logger(__FILE__, __LINE__, __FUNCTION__, 9, "CCFG_TI_OPTIONS: 0x%08x", *pTIOptions); 345 | logger(__FILE__, __LINE__, __FUNCTION__, 9, "CCFG_TAP_DAP_0: 0x%08x", *pTapDap0); 346 | logger(__FILE__, __LINE__, __FUNCTION__, 9, "CCFG_TAP_DAP_1: 0x%08x", *pTapDap1); 347 | logger(__FILE__, __LINE__, __FUNCTION__, 9, "JTAGCFG: 0x%08x", *jtagcfg); 348 | 349 | if ((*pTIOptions == 0xffffffc5) && (*pTapDap0 == 0xffc5c5c5) && (*pTapDap1 == 0xffc5c5c5)) { 350 | return 0; 351 | } 352 | 353 | sector_size = (*(uint32_t *)FLASH_FCFG_B0_SSIZE0 & 0x0f) << 10; 354 | logger(__FILE__, __LINE__, __FUNCTION__, 9, "Patch CCFG ... sector_size: %u", sector_size); 355 | 356 | src_addr = last_sector; 357 | dst_addr = tmp_dst; 358 | total = sector_size; 359 | 360 | flash_sector_erase(dst_addr); 361 | 362 | while (total) { 363 | memcpy(data, (void*)src_addr, sizeof(data)); 364 | 365 | if (total == sizeof(data)) { 366 | *(uint32_t *)(data + CCFG_TI_OPTIONS) = 0xffffffc5; 367 | *(uint32_t *)(data + CCFG_TAP_DAP_0) = 0xffc5c5c5; 368 | *(uint32_t *)(data + CCFG_TAP_DAP_1) = 0xffc5c5c5; 369 | strcpy(data + CCFG_BIM_DATE, "Jun 16 2025"); 370 | strcpy(data + CCFG_BIM_TIME, "13:12:42"); 371 | } 372 | 373 | flash_program(data, dst_addr, sizeof(data)); 374 | src_addr += sizeof(data); 375 | dst_addr += sizeof(data); 376 | total -= sizeof(data); 377 | } 378 | 379 | src_addr = tmp_dst; 380 | dst_addr = last_sector; 381 | total = sector_size; 382 | 383 | flash_sector_erase(dst_addr); 384 | 385 | while (total) { 386 | flash_program((uint8_t*)src_addr, dst_addr, sizeof(data)); 387 | src_addr += sizeof(data); 388 | dst_addr += sizeof(data); 389 | total -= sizeof(data); 390 | } 391 | 392 | return 0; 393 | } 394 | 395 | static void * 396 | memcpy(void *dst, const void *src, size_t len) 397 | { 398 | uint8_t *d = dst; 399 | const uint8_t *s = src; 400 | while (len--) 401 | *d++ = *s++; 402 | return dst; 403 | } 404 | 405 | static void 406 | strcpy(char *d, const char *s) 407 | { 408 | while (*s) 409 | *d++ = *s++; 410 | *d = '\0'; 411 | } 412 | 413 | static int 414 | strcmp(const char *s1, const char *s2) 415 | { 416 | while (*s1 && (*s1 == *s2)) { 417 | s1++; 418 | s2++; 419 | } 420 | return *s1 - *s2; 421 | } 422 | 423 | static int 424 | strncmp(const char *s1, const char *s2, size_t n) 425 | { 426 | while (*s1 && (*s1 == *s2) && --n) { 427 | s1++; 428 | s2++; 429 | } 430 | return *s1 - *s2; 431 | } 432 | 433 | extern void *xdcRomStatePtr; 434 | 435 | typedef uint16_t xdc_Bool; 436 | typedef xdc_Bool __T1_ti_sysbios_family_arm_m3_Hwi_Module_State__excActive; 437 | typedef xdc_Bool *ARRAY1_ti_sysbios_family_arm_m3_Hwi_Module_State__excActive; 438 | typedef ARRAY1_ti_sysbios_family_arm_m3_Hwi_Module_State__excActive __TA_ti_sysbios_family_arm_m3_Hwi_Module_State__excActive; 439 | 440 | typedef struct Hwi_Module_State { 441 | char *xdcTaskSP; 442 | __TA_ti_sysbios_family_arm_m3_Hwi_Module_State__excActive excActive; 443 | } ti_sysbios_family_arm_m3_Hwi_Module_State; 444 | 445 | #define ti_sysbios_family_arm_m3_Hwi_Module__state__V_offset 0xd0 446 | #define Hwi_module ((ti_sysbios_family_arm_m3_Hwi_Module_State *)(xdcRomStatePtr + ti_sysbios_family_arm_m3_Hwi_Module__state__V_offset)) 447 | 448 | void 449 | System_putchar(uint8_t c) 450 | { 451 | if (Hwi_module->excActive[0]) { 452 | struct UART *uart = (struct UART *)UART1; 453 | uint32_t imsc = uart->IMSC; 454 | uart->IMSC = 0; 455 | uart->CTL |= 0x100; 456 | while (uart->FR & 0x20) 457 | /* wait */; 458 | if (c == '\n') { 459 | uart->DR = '\r'; 460 | while (uart->FR & 0x20) 461 | /* wait */; 462 | } 463 | uart->DR = c; 464 | uart->IMSC = imsc; 465 | } else { 466 | system_putchar_t putc = (system_putchar_t)SYSTEM_PUTCHAR; 467 | putc(c); 468 | } 469 | } 470 | -------------------------------------------------------------------------------- /ble-patch.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | 13 | #include "mmap.h" 14 | #include "endian.h" 15 | #include "ware.h" 16 | 17 | static char *progname; 18 | 19 | static void 20 | usage(void) 21 | { 22 | fprintf(stderr, "usage: %s [-v] \n", progname); 23 | exit(1); 24 | } 25 | 26 | static const uint16_t exp_full_debug[] = { 27 | 0xf440, 28 | 0x7040, 29 | }; 30 | 31 | static const uint16_t rpl_full_debug[] = { 32 | 0xf240, 33 | 0x30ff, 34 | }; 35 | 36 | static const uint16_t exp_null_ptr[] = { 37 | 0x0000, 0x0000, 38 | }; 39 | 40 | static const uint16_t rpl_exc_dump[] = { 41 | 0xbeb5, 0x1002, // ti_sysbios_family_arm_m3_Hwi_excHandlerMax 42 | }; 43 | 44 | static const uint16_t exp_error_spin[] = { 45 | 0xf1f7, 0x1002, 46 | }; 47 | 48 | static const uint16_t rpl_error_spin[] = { 49 | 0xfb35, 0x1002, // ti_sysbios_family_arm_m3_Hwi_return 50 | }; 51 | 52 | static const uint16_t exp_system_putchar_1_4_1[] = { 53 | 0x60f9, 0x0002, 54 | }; 55 | 56 | static const uint16_t rpl_system_putchar_1_4_1[] = { 57 | (SYSTEM_PUTCHAR1 & 0xffff) | 1, SYSTEM_PUTCHAR1 >> 16, 58 | }; 59 | 60 | static const uint16_t exp_system_putchar_2_4_1[] = { 61 | 0xdbc9, 0x0002, 62 | }; 63 | 64 | static const uint16_t rpl_system_putchar_2_4_1[] = { 65 | (SYSTEM_PUTCHAR2 & 0xffff) | 1, SYSTEM_PUTCHAR2 >> 16, 66 | }; 67 | 68 | static const uint16_t exp_offset_rtos_stat_1_4_1[] = { 69 | 0xf6a1, 0x0000, 70 | }; 71 | 72 | static const uint16_t exp_offset_rtos_stat_2_4_1[] = { 73 | 0x0fa5, 0x0001, 74 | }; 75 | 76 | static const uint16_t rpl_offset_rtos_stat_1_4_1[] = { 77 | 0xc67d, 0x0002, 78 | }; 79 | 80 | static const uint16_t rpl_offset_rtos_stat_2_4_1[] = { 81 | 0x531d, 0x0003, 82 | }; 83 | 84 | static const uint16_t rpl_dump_1_4_1[] = { 85 | #include "keys1.hex" 86 | }; 87 | 88 | static const uint16_t rpl_dump_2_4_1[] = { 89 | #include "keys2.hex" 90 | }; 91 | 92 | static const char exp_date_time_1_4_1[] = { 93 | "Mar 29 2021 / 14:20:30", 94 | }; 95 | 96 | static const char exp_date_time_2_4_1[] = { 97 | "Mar 29 2021 / 14:17:30", 98 | }; 99 | 100 | static const char rpl_date_time_1_4_1[] = { 101 | "May 12 2025 / 09:03:35", 102 | }; 103 | 104 | static const char rpl_date_time_2_4_1[] = { 105 | "Jun 10 2025 / 12:02:27", 106 | }; 107 | 108 | typedef struct { 109 | const char* name; 110 | off_t offset; 111 | size_t size; 112 | const uint16_t *expect; 113 | const uint16_t *patch; 114 | } patch_t; 115 | 116 | #define N_ARRAY(a) (sizeof(a) / sizeof(a[0])) 117 | 118 | static const patch_t patch_full_debug_1_4_1 = { 119 | "enable debug", 120 | 0x1cfda, 121 | N_ARRAY(rpl_full_debug), 122 | exp_full_debug, 123 | rpl_full_debug, 124 | }; 125 | 126 | static const patch_t patch_full_debug_2_4_1 = { 127 | "enable debug", 128 | 0x22306, 129 | N_ARRAY(rpl_full_debug), 130 | exp_full_debug, 131 | rpl_full_debug, 132 | }; 133 | 134 | static const patch_t patch_exc_dump_1_4_1 = { 135 | "exception dump", 136 | 0x294d8, 137 | N_ARRAY(rpl_exc_dump), 138 | exp_null_ptr, 139 | rpl_exc_dump, 140 | }; 141 | 142 | static const patch_t patch_exc_dump_2_4_1 = { 143 | "exception dump", 144 | 0x31770, 145 | N_ARRAY(rpl_exc_dump), 146 | exp_null_ptr, 147 | rpl_exc_dump, 148 | }; 149 | 150 | static const patch_t patch_error_spin_1_4_1 = { 151 | "error spin", 152 | 0x29440, 153 | N_ARRAY(rpl_error_spin), 154 | exp_error_spin, 155 | rpl_error_spin, 156 | }; 157 | 158 | static const patch_t patch_error_spin_2_4_1 = { 159 | "error spin", 160 | 0x316d8, 161 | N_ARRAY(rpl_error_spin), 162 | exp_error_spin, 163 | rpl_error_spin, 164 | }; 165 | 166 | static const patch_t patch_system_putchar_1_4_1 = { 167 | "system putchar", 168 | 0x29604, 169 | N_ARRAY(rpl_system_putchar_1_4_1), 170 | exp_system_putchar_1_4_1, 171 | rpl_system_putchar_1_4_1, 172 | }; 173 | 174 | static const patch_t patch_system_putchar_2_4_1 = { 175 | "system putchar", 176 | 0x3189c, 177 | N_ARRAY(rpl_system_putchar_2_4_1), 178 | exp_system_putchar_2_4_1, 179 | rpl_system_putchar_2_4_1, 180 | }; 181 | 182 | static const patch_t patch_offset_rtos_stat_1_4_1 = { 183 | "offset dump", 184 | 0x2a108, 185 | N_ARRAY(rpl_offset_rtos_stat_1_4_1), 186 | exp_offset_rtos_stat_1_4_1, 187 | rpl_offset_rtos_stat_1_4_1, 188 | }; 189 | 190 | static const patch_t patch_offset_rtos_stat_2_4_1 = { 191 | "offset dump", 192 | 0x3237c, 193 | N_ARRAY(rpl_offset_rtos_stat_2_4_1), 194 | exp_offset_rtos_stat_2_4_1, 195 | rpl_offset_rtos_stat_2_4_1, 196 | }; 197 | 198 | static const patch_t patch_dump_1_4_1 = { 199 | "dump", 200 | 0x2c67c, 201 | N_ARRAY(rpl_dump_1_4_1), 202 | NULL, 203 | rpl_dump_1_4_1, 204 | }; 205 | 206 | static const patch_t patch_dump_2_4_1 = { 207 | "dump", 208 | 0x3531c, 209 | N_ARRAY(rpl_dump_2_4_1), 210 | NULL, 211 | rpl_dump_2_4_1, 212 | }; 213 | 214 | static const patch_t patch_date_time_1_4_1 = { 215 | "date/time", 216 | 0x570f, 217 | sizeof(rpl_date_time_1_4_1) / sizeof(uint16_t), 218 | (uint16_t *)exp_date_time_1_4_1, 219 | (uint16_t *)rpl_date_time_1_4_1, 220 | }; 221 | 222 | static const patch_t patch_date_time_2_4_1 = { 223 | "date/time", 224 | 0x2677, 225 | sizeof(rpl_date_time_2_4_1) / sizeof(uint16_t), 226 | (uint16_t *)exp_date_time_2_4_1, 227 | (uint16_t *)rpl_date_time_2_4_1, 228 | }; 229 | 230 | static const patch_t *patches_1_4_1[] = { 231 | &patch_offset_rtos_stat_1_4_1, 232 | &patch_full_debug_1_4_1, 233 | &patch_exc_dump_1_4_1, 234 | &patch_error_spin_1_4_1, 235 | &patch_system_putchar_1_4_1, 236 | &patch_dump_1_4_1, 237 | &patch_date_time_1_4_1, 238 | }; 239 | 240 | static const patch_t *patches_2_4_1[] = { 241 | &patch_offset_rtos_stat_2_4_1, 242 | &patch_full_debug_2_4_1, 243 | &patch_exc_dump_2_4_1, 244 | &patch_error_spin_2_4_1, 245 | &patch_system_putchar_2_4_1, 246 | &patch_dump_2_4_1, 247 | &patch_date_time_2_4_1, 248 | }; 249 | 250 | typedef struct { 251 | const char *date; 252 | const char *time; 253 | size_t n_patches; 254 | const patch_t **patches; 255 | size_t n_version_patches; 256 | const patch_t **version_patches; 257 | } patchset_t; 258 | 259 | static const patchset_t patchset_1_4_1 = { 260 | "May 12 2025", 261 | "09:03:35", 262 | N_ARRAY(patches_1_4_1), 263 | patches_1_4_1, 264 | 0, 265 | NULL, 266 | }; 267 | 268 | static const patchset_t patchset_2_4_1 = { 269 | "Jun 10 2025", 270 | "12:02:27", 271 | N_ARRAY(patches_2_4_1), 272 | patches_2_4_1, 273 | 0, 274 | NULL, 275 | }; 276 | 277 | static const uint32_t crc_poly = 0x4c11db7; 278 | static const uint32_t initial_crc = 0; 279 | 280 | static int verify_patch(const char *filename, const void *data, const patch_t *patch, int verbose) 281 | { 282 | uint32_t offset = patch->offset - BLE_WARE_OFFSET; 283 | const uint16_t* inst = data + offset; 284 | 285 | if (patch->expect) { 286 | for (size_t i = 0; i < patch->size; i++) { 287 | if (inst[i] != patch->expect[i]) { 288 | fprintf(stderr, "%s: patch \"%s\": @0x%08x: inst[%zu] 0x%04x != expected 0x%04x\n", 289 | filename, patch->name, patch->offset, i, inst[i], patch->expect[i]); 290 | return -1; 291 | } 292 | } 293 | } 294 | 295 | if (verbose) { 296 | printf("%s: verify \"%s\": @0x%08x [%u]: OK\n", progname, patch->name, patch->offset, patch->size); 297 | } 298 | return 0; 299 | } 300 | 301 | static int verify_expected(const char *filename, const void *data, const char* fake_version, const patchset_t *set, int verbose) 302 | { 303 | int expect_ok = 1; 304 | 305 | for (size_t i = 0; i < set->n_patches; i++) { 306 | if (verify_patch(filename, data, set->patches[i], verbose) != 0) { 307 | expect_ok = 0; 308 | } 309 | } 310 | 311 | if (fake_version) { 312 | for (size_t i = 0; i < set->n_version_patches; i++) { 313 | if (verify_patch(filename, data, set->version_patches[i], verbose) != 0) { 314 | expect_ok = 0; 315 | } 316 | } 317 | } 318 | 319 | return expect_ok; 320 | } 321 | 322 | static void apply_patch(void *data, const patch_t *patch, int verbose) 323 | { 324 | uint32_t offset = patch->offset - BLE_WARE_OFFSET; 325 | uint16_t* inst = data + offset; 326 | 327 | for (size_t i = 0; i < patch->size; i++) { 328 | inst[i] = patch->patch[i]; 329 | } 330 | 331 | if (verbose) { 332 | printf("%s: apply \"%s\": @0x%08x [%u]\n", progname, patch->name, patch->offset, patch->size); 333 | } 334 | } 335 | 336 | static void apply_patches(void *data, const char *fake_version, const patchset_t *set, int verbose) 337 | { 338 | for (size_t i = 0; i < set->n_patches; i++) { 339 | apply_patch(data, set->patches[i], verbose); 340 | } 341 | 342 | if (fake_version) { 343 | for (size_t i = 0; i < set->n_version_patches; i++) { 344 | apply_patch(data, set->version_patches[i], verbose); 345 | } 346 | } 347 | } 348 | 349 | static void fixup_headers(ble_ware_t *ble_ware, void *data, size_t size, size_t add_len, const char *filename) 350 | { 351 | ble_ware_seg_t seg; 352 | size_t offset = le32toh(ble_ware->hdr_len); 353 | 354 | while (offset < size + add_len) { 355 | memcpy(&seg, data + offset, sizeof(ble_ware_seg_t)); 356 | printf("%s: BLE ware seg type 0x%02x\n", filename, seg.seg_type); 357 | printf("%s: BLE ware seg len 0x%08x\n", filename, le32toh(seg.seg_len)); 358 | 359 | if (seg.seg_type == BLE_SEG_TYPE_CONTIGUOUS) { 360 | seg.seg_len = htole32(le32toh(seg.seg_len) + add_len); 361 | memcpy(data + offset, &seg, sizeof(ble_ware_seg_t)); 362 | } 363 | if (seg.seg_type == BLE_SEG_TYPE_SECURITY) { 364 | seg.seg_type = BLE_SEG_TYPE_NONCONTIGUOUS; 365 | memcpy(data + offset, &seg, sizeof(ble_ware_seg_t)); 366 | } 367 | 368 | offset += le32toh(seg.seg_len); 369 | } 370 | 371 | uint32_t length = le32toh(ble_ware->len) + add_len; 372 | ble_ware->len = htole32(length); 373 | ble_ware->img_end_addr = htole32(le32toh(ble_ware->img_end_addr) + add_len); 374 | memcpy(data, ble_ware, sizeof(ble_ware_t)); 375 | 376 | ble_ware->crc = crc32(0, data + 12, length - 12); 377 | memcpy(data, ble_ware, sizeof(ble_ware_t)); 378 | 379 | printf("%s: CRC 0x%08x\n", filename, le32toh(ble_ware->crc)); 380 | } 381 | 382 | int main(int argc, char** argv) 383 | { 384 | int verbose = 0; 385 | int opt; 386 | 387 | progname = strrchr(argv[0], '/'); 388 | if (progname) 389 | progname++; 390 | else 391 | progname = argv[0]; 392 | 393 | while ((opt = getopt(argc, argv, "v")) != -1) { 394 | switch (opt) { 395 | case 'v': 396 | verbose++; 397 | break; 398 | default: 399 | usage(); 400 | } 401 | } 402 | 403 | if (optind >= argc) { 404 | usage(); 405 | } 406 | 407 | char *filename = argv[optind]; 408 | 409 | int fd = open(filename, O_RDWR); 410 | if (fd < 0) { 411 | fprintf(stderr, "%s: open(%s): %s\n", progname, filename, strerror(errno)); 412 | exit(1); 413 | } 414 | 415 | struct stat st; 416 | if (fstat(fd, &st) < 0) { 417 | fprintf(stderr, "%s: stat(%s): %s\n", progname, filename, strerror(errno)); 418 | exit(1); 419 | } 420 | 421 | size_t add_len = sizeof(rpl_dump_1_4_1); 422 | if (add_len != sizeof(rpl_dump_2_4_1)) { 423 | fprintf(stderr, "%s: FIXME: sizeof(rpl_dump_1_4_1) and sizeof(rpl_dump_2_4_1) differ\n", progname); 424 | exit(1); 425 | } 426 | 427 | void *data = mmap(NULL, st.st_size + add_len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); 428 | if (data == (void *)-1) { 429 | fprintf(stderr, "%s: mmap(%s): %s\n", progname, filename, strerror(errno)); 430 | exit(1); 431 | } 432 | 433 | ble_ware_t ble_ware; 434 | memcpy(&ble_ware, data, sizeof(ble_ware)); 435 | 436 | if (memcmp(ble_ware.magic, BLE_WARE_MAGIC, sizeof(ble_ware.magic)) == 0) { 437 | printf("%s: BLE ware magic OK\n", filename); 438 | printf("%s: BLE ware version %08x\n", filename, le32toh(ble_ware.soft_ver)); 439 | printf("%s: BLE ware CRC 0x%08x\n", filename, le32toh(ble_ware.crc)); 440 | printf("%s: BLE ware length 0x%08x\n", filename, le32toh(ble_ware.len)); 441 | 442 | uint32_t length = le32toh(ble_ware.len); 443 | if (length > st.st_size) { 444 | printf("%s: BLE ware length 0x%08x extends beyond file size 0x%08zx\n", 445 | filename, length, st.st_size); 446 | exit(1); 447 | } 448 | 449 | uint32_t crc = crc32(0, data + 12, length - 12); 450 | 451 | printf("%s: CRC 0x%08x %s\n", filename, crc, crc == le32toh(ble_ware.crc) ? "OK" : "FAIL"); 452 | 453 | if (crc != le32toh(ble_ware.crc)) 454 | exit(1); 455 | 456 | printf("%s: BLE ware entry 0x%08x\n", filename, le32toh(ble_ware.prg_entry)); 457 | printf("%s: BLE ware hdr len 0x%08x\n", filename, le32toh(ble_ware.hdr_len)); 458 | 459 | if ((le32toh(ble_ware.crc) == 0xb79c4373) && (length == 0x0002c67c)) { 460 | if (verify_expected(filename, data, NULL, &patchset_1_4_1, verbose)) { 461 | if (add_len) { 462 | ftruncate(fd, st.st_size + add_len); 463 | } 464 | apply_patches(data, NULL, &patchset_1_4_1, verbose); 465 | 466 | fixup_headers(&ble_ware, data, st.st_size, add_len, filename); 467 | } else { 468 | fprintf(stderr, "%s: verify patchset failed\n", progname); 469 | exit(1); 470 | } 471 | } else if ((le32toh(ble_ware.crc) == 0x884a9283) && (length == 0x0003531c)) { 472 | if (verify_expected(filename, data, NULL, &patchset_2_4_1, verbose)) { 473 | if (add_len) { 474 | ftruncate(fd, st.st_size + add_len); 475 | } 476 | apply_patches(data, NULL, &patchset_2_4_1, verbose); 477 | 478 | fixup_headers(&ble_ware, data, st.st_size, add_len, filename); 479 | } else { 480 | fprintf(stderr, "%s: verify patchset failed\n", progname); 481 | exit(1); 482 | } 483 | } else { 484 | fprintf(stderr, "%s: No patchset for this version of bleware.bin\n", progname); 485 | exit(1); 486 | } 487 | } else { 488 | fprintf(stderr, "%s: Not a vanmoof ware file\n", filename); 489 | exit(1); 490 | } 491 | 492 | munmap(data, st.st_size + add_len); 493 | 494 | close(fd); 495 | 496 | return 0; 497 | } 498 | -------------------------------------------------------------------------------- /patch.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "mmap.h" 12 | #include "endian.h" 13 | #include "ware.h" 14 | 15 | static char *progname; 16 | 17 | static void 18 | usage(void) 19 | { 20 | fprintf(stderr, "usage: %s [-v] [-f ] [-m ] \n", progname); 21 | fprintf(stderr, "\nfake-version:\t..\n"); 22 | fprintf(stderr, "model:\t\t,,\n"); 23 | exit(1); 24 | } 25 | 26 | typedef struct { 27 | const char* name; 28 | off_t offset; 29 | size_t size; 30 | const uint16_t *expect; 31 | const uint16_t *patch; 32 | } patch_t; 33 | 34 | static const uint16_t exp_cmp_r3_2[] = { 35 | 0x2b02 /* cmp r3, #2 */ 36 | }; 37 | 38 | static const uint16_t rpl_cmp_r3_3[] = { 39 | 0x2b03 /* cmp r3, #3 */ 40 | }; 41 | 42 | static const uint16_t exp_cmp_r3_4[] = { 43 | 0x2b04 /* cmp r3, #4 */ 44 | }; 45 | 46 | static const uint16_t rpl_cmp_r3_5[] = { 47 | 0x2b05 /* cmp r3, #5 */ 48 | }; 49 | 50 | static const uint16_t exp_region_1_9_3[] = { 51 | 0xbf08, /* it eq */ 52 | 0xf884, 0x9109 /* strb.eq.w r9, [r4,#0x109] */ 53 | }; 54 | 55 | static const uint16_t rpl_region_1_9_3[] = { 56 | 0xbf00, /* nop */ 57 | 0xbf00, /* nop */ 58 | 0xbf00 /* nop */ 59 | }; 60 | 61 | static const uint16_t exp_power_button_1_9_3[] = { 62 | 0x3301, /* adds r3, #1 */ 63 | 0xb2db, /* uxtb r3, r3 */ 64 | 0x2b04, /* cmp r3, #4 */ 65 | 0xbf88, /* it hi */ 66 | 0x2300, /* mov.hi r3, #0 */ 67 | }; 68 | 69 | static const uint16_t rpl_power_button_1_9_3[] = { 70 | 0x4901, /* ldr r1, [pc,#4] */ 71 | 0x4788, /* blx r1 */ 72 | 0xe001, /* b.n */ 73 | 0x0029, 0x0802 /* addr 0x08020028+1 */ 74 | }; 75 | 76 | static const uint16_t exp_power_level_inc[] = { 77 | 0xffff, 0xffff, 0xffff, 0xffff, 78 | 0xffff, 0xffff, 0xffff, 0xffff, 79 | 0xffff, 0xffff, 0xffff 80 | }; 81 | 82 | static const uint16_t rpl_power_level_inc[] = { 83 | 0x3301, /* adds r3, #1 */ 84 | 0xf897, 0x2109, /* ldrb.w r2, [r7,#0x109] */ 85 | 0x2a03, /* cmp r2, #3 */ 86 | 0xd101, /* bne.n */ 87 | 0x2b05, /* cmp r3, #5 */ 88 | 0xe000, /* b.n */ 89 | 0x2b04, /* cmp r3, #4 */ 90 | 0xbf88, /* it hi */ 91 | 0x2300, /* mov.hi r3, #0 */ 92 | 0x4770 /* bx lr */ 93 | }; 94 | 95 | #ifdef DUMP 96 | 97 | static const uint16_t exp_dump[] = { 98 | 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, /* 0060 */ 99 | 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, /* 0070 */ 100 | 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, /* 0080 */ 101 | 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, /* 0090 */ 102 | 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, /* 00a0 */ 103 | 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, /* 00b0 */ 104 | 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, /* 00c0 */ 105 | 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, /* 00d0 */ 106 | 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, /* 00e0 */ 107 | 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, /* 00f0 */ 108 | 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, /* 0100 */ 109 | 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, /* 0110 */ 110 | 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, /* 0120 */ 111 | 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, /* 0130 */ 112 | 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, /* 0140 */ 113 | 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, /* 0150 */ 114 | 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, /* 0160 */ 115 | 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, /* 0170 */ 116 | 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, /* 0180 */ 117 | 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, /* 0190 */ 118 | 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, /* 01a0 */ 119 | 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, /* 01b0 */ 120 | 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, /* 01c0 */ 121 | 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, /* 01d0 */ 122 | 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, /* 01e0 */ 123 | 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, /* 01f0 */ 124 | }; 125 | 126 | static const uint16_t rpl_dump_1_9_3[] = { 127 | #include "dump.hex" 128 | }; 129 | 130 | static const uint16_t exp_help_1_9_3[] = { 131 | 0x5e05, 0x0803 132 | }; 133 | 134 | static const uint16_t rpl_help_1_9_3[] = { 135 | 0x0061, 0x0802 136 | }; 137 | 138 | static const uint16_t exp_help_text_1_9_3[] = { 139 | 0x5400, 0x6968, 0x2073, 0x6574, 0x736b, 0x0074 /* "\0This tekst\0" */ 140 | }; 141 | 142 | static const uint16_t rpl_help_text_1_9_3[] = { 143 | 0x4800, 0x6c65, 0x2f70, 0x7564, 0x706d, 0x0000 /* "\0Help/dump\0\0" */ 144 | }; 145 | 146 | 147 | #endif /* DUMP */ 148 | 149 | static const uint16_t exp_version_1_9_3[] = { 150 | 0x03f4, 0x0109 151 | }; 152 | 153 | static uint16_t rpl_version_any[] = { 154 | 0x00f4, 0x0000 155 | }; 156 | 157 | static uint16_t exp_model_06[] = { 158 | 0x2306 /* movs r3, #6 */ 159 | }; 160 | 161 | static uint16_t exp_model_06_1[] = { 162 | 0x2206 /* movs r2, #6 */ 163 | }; 164 | 165 | static uint16_t rpl_model_any[] = { 166 | 0x2300 /* movs r3, #0 */ 167 | }; 168 | 169 | static uint16_t rpl_model_06_1[] = { 170 | 0x2200 /* movs r2, #0 */ 171 | }; 172 | 173 | #define N_ARRAY(a) (sizeof(a) / sizeof(a[0])) 174 | 175 | /* Patch region 3 not allowed from BLE */ 176 | static const patch_t patch_region_ble_1_9_3 = { 177 | "Region 3 from BLE", 178 | 0x0803a876, 179 | N_ARRAY(rpl_cmp_r3_3), 180 | exp_cmp_r3_2, 181 | rpl_cmp_r3_3, 182 | }; 183 | 184 | /* Patch power level 5 not allowed from BLE */ 185 | static const patch_t patch_power_ble_1_9_3 = { 186 | "Power 5 from BLE", 187 | 0x0803a7ca, 188 | N_ARRAY(rpl_cmp_r3_5), 189 | exp_cmp_r3_4, 190 | rpl_cmp_r3_5, 191 | }; 192 | 193 | /* Patch power level 5 reset to 4 during boot */ 194 | static const patch_t patch_power_1_9_3 = { 195 | "Power 5 at startup", 196 | 0x0803ef0a, 197 | N_ARRAY(rpl_cmp_r3_5), 198 | exp_cmp_r3_4, 199 | rpl_cmp_r3_5, 200 | }; 201 | 202 | /* Patch region 3 reset to region 1 during boot */ 203 | static const patch_t patch_region_1_9_3 = { 204 | "Region 3 at startup", 205 | 0x0803ef00, 206 | N_ARRAY(rpl_region_1_9_3), 207 | exp_region_1_9_3, 208 | rpl_region_1_9_3 209 | }; 210 | 211 | /* Patch cycling of power level 5 not allowed */ 212 | static const patch_t patch_power_button_1_9_3 = { 213 | "Power 5 handle bar", 214 | 0x08027fb2, 215 | N_ARRAY(rpl_power_button_1_9_3), 216 | exp_power_button_1_9_3, 217 | rpl_power_button_1_9_3 218 | }; 219 | 220 | /* Increment function for power level cycling patch */ 221 | static const patch_t patch_power_level_inc = { 222 | "Power 5 handle bar helper", 223 | 0x08020028, 224 | N_ARRAY(rpl_power_level_inc), 225 | exp_power_level_inc, 226 | rpl_power_level_inc 227 | }; 228 | 229 | #ifdef DUMP 230 | 231 | static const patch_t patch_dump_1_9_3 = { 232 | "Dump FLASH function", 233 | 0x08020060, 234 | N_ARRAY(rpl_dump_1_9_3), 235 | exp_dump, 236 | rpl_dump_1_9_3 237 | }; 238 | 239 | static const patch_t patch_help_1_9_3 = { 240 | "Hijack help command", 241 | 0x0804e05c, 242 | N_ARRAY(rpl_help_1_9_3), 243 | exp_help_1_9_3, 244 | rpl_help_1_9_3 245 | }; 246 | 247 | static const patch_t patch_help_text_1_9_3 = { 248 | "Hijack help text", 249 | 0x0804dbb0, 250 | N_ARRAY(rpl_help_text_1_9_3), 251 | exp_help_text_1_9_3, 252 | rpl_help_text_1_9_3 253 | }; 254 | 255 | #endif /* DUMP */ 256 | 257 | static const patch_t patch_version_head_1_9_3 = { 258 | "Fixup header version", 259 | 0x08020004, 260 | N_ARRAY(rpl_version_any), 261 | exp_version_1_9_3, 262 | rpl_version_any 263 | }; 264 | 265 | static const patch_t patch_version_1_9_3 = { 266 | "Fixup mainware version", 267 | 0x0803f0f0, 268 | N_ARRAY(rpl_version_any), 269 | exp_version_1_9_3, 270 | rpl_version_any 271 | }; 272 | 273 | static const patch_t patch_model0_1_9_3 = { 274 | "Patch bike model: 0", 275 | 0x0803ebd8, 276 | N_ARRAY(rpl_model_any), 277 | exp_model_06, 278 | rpl_model_any 279 | }; 280 | 281 | static const patch_t patch_model1_1_9_3 = { 282 | "Patch bike model: 1", 283 | 0x0803ef3c, 284 | N_ARRAY(rpl_model_any), 285 | exp_model_06, 286 | rpl_model_any 287 | }; 288 | 289 | static const patch_t patch_model2_1_9_3 = { 290 | "Patch bike model: 2", 291 | 0x080377f0, 292 | N_ARRAY(rpl_model_06_1), 293 | exp_model_06_1, 294 | rpl_model_06_1, 295 | }; 296 | 297 | static const patch_t *patches_1_9_3[] = { 298 | &patch_region_ble_1_9_3, 299 | &patch_power_ble_1_9_3, 300 | &patch_power_1_9_3, 301 | &patch_region_1_9_3, 302 | &patch_power_button_1_9_3, 303 | &patch_power_level_inc, 304 | #ifdef DUMP 305 | &patch_help_1_9_3, 306 | &patch_help_text_1_9_3, 307 | &patch_dump_1_9_3, 308 | #endif /* DUMP */ 309 | }; 310 | 311 | static const patch_t *version_1_9_3[] = { 312 | &patch_version_head_1_9_3, 313 | &patch_version_1_9_3 314 | }; 315 | 316 | static const patch_t *model_1_9_3[] = { 317 | &patch_model0_1_9_3, 318 | &patch_model1_1_9_3, 319 | &patch_model2_1_9_3 320 | }; 321 | 322 | typedef struct { 323 | const char *date; 324 | const char *time; 325 | uint32_t flags; 326 | size_t n_patches; 327 | const patch_t **patches; 328 | size_t n_version_patches; 329 | const patch_t **version_patches; 330 | size_t n_model_patches; 331 | const patch_t **model_patches; 332 | } patchset_t; 333 | 334 | #define PATCHSET_FLAG_VERSION (1 << 0) 335 | #define PATCHSET_FLAG_MODEL (1 << 1) 336 | 337 | static patchset_t patchset_1_9_3 = { 338 | "Apr 30 2025", 339 | "10:30:52", 340 | 0, 341 | N_ARRAY(patches_1_9_3), 342 | patches_1_9_3, 343 | N_ARRAY(version_1_9_3), 344 | version_1_9_3, 345 | N_ARRAY(model_1_9_3), 346 | model_1_9_3, 347 | }; 348 | 349 | static void setup_version_patches(const char *fake_version, int verbose) 350 | { 351 | uint8_t major_version = 0, minor_version = 0, patch_version = 0; 352 | char *end; 353 | 354 | const char *p = fake_version; 355 | major_version = strtoul(p, &end, 10); 356 | if ((end == p) || (*end != '.')) { 357 | fprintf(stderr, "%s: can't parse version '%s'\n", progname, fake_version); 358 | exit(1); 359 | } 360 | p = end + 1; 361 | minor_version = strtoul(p, &end, 10); 362 | if ((end == p) || (*end != '.')) { 363 | fprintf(stderr, "%s: can't parse version '%s'\n", progname, fake_version); 364 | exit(1); 365 | } 366 | p = end + 1; 367 | patch_version = strtoul(p, &end, 10); 368 | if ((end == p) || (*end != '\0')) { 369 | fprintf(stderr, "%s: can't parse version '%s'\n", progname, fake_version); 370 | exit(1); 371 | } 372 | 373 | rpl_version_any[0] |= (patch_version << 8); 374 | rpl_version_any[1] = (major_version << 8) | minor_version; 375 | 376 | if (verbose) { 377 | printf("%s: patch version to %u.%u.%u\n", progname, major_version, minor_version, patch_version); 378 | } 379 | } 380 | 381 | static void setup_model_patches(const char *model, int verbose) 382 | { 383 | int model_no = 3, shifter = 1, display = 1; 384 | uint8_t model_byte; 385 | char *end; 386 | 387 | const char *p = model; 388 | model_no = strtoul(p, &end, 10); 389 | if ((end == p) || (*end != ',')) { 390 | fprintf(stderr, "%s: can't parse model '%s'\n", progname, model); 391 | exit(1); 392 | } 393 | p = end + 1; 394 | shifter = strtoul(p, &end, 10); 395 | if ((end == p) || (*end != ',')) { 396 | fprintf(stderr, "%s: can't parse model '%s'\n", progname, model); 397 | exit(1); 398 | } 399 | p = end + 1; 400 | display = strtoul(p, &end, 10); 401 | if ((end == p) || (*end != '\0')) { 402 | fprintf(stderr, "%s: can't parse model '%s'\n", progname, model); 403 | exit(1); 404 | } 405 | 406 | if (model_no != 3 && model_no != 4) { 407 | fprintf(stderr, "%s: can't parse model '%s': model must be 3 or 4\n", progname, model); 408 | exit(1); 409 | } 410 | if (shifter != 0 && shifter != 1) { 411 | fprintf(stderr, "%s: can't parse model '%s': shifter must be 0 or 1\n", progname, model); 412 | exit(1); 413 | } 414 | if (display != 0 && display != 1) { 415 | fprintf(stderr, "%s: can't parse model '%s': display must be 0 or 1\n", progname, model); 416 | exit(1); 417 | } 418 | 419 | model_byte = 0; 420 | if (model_no == 4) { 421 | model_byte |= 1; 422 | } 423 | if (shifter) { 424 | model_byte |= 2; 425 | } 426 | if (display) { 427 | model_byte |= 4; 428 | } 429 | 430 | rpl_model_any[0] |= model_byte; 431 | if (model_no == 3) { 432 | rpl_model_06_1[0] |= model_byte; 433 | } else { 434 | rpl_model_06_1[0] |= 6; 435 | } 436 | 437 | if (verbose) { 438 | printf("%s: patch model to %s with%s shifter with%s display\n", progname, 439 | model_byte & 1 ? "ES4" : "ES3", model_byte & 2 ? "" : "out", model_byte & 4 ? "" : "out"); 440 | } 441 | } 442 | 443 | static const uint32_t crc_poly = 0x4c11db7; 444 | static const uint32_t initial_crc = 0xffffffff; 445 | 446 | static uint32_t crc32_calculate(uint32_t crc, const void *data, size_t length) 447 | { 448 | const uint32_t *p = data; 449 | size_t i, b; 450 | 451 | for (i = 0; i < length; i += sizeof(uint32_t)) { 452 | crc ^= *p++; 453 | for (b = 0; b < 32; b++) { 454 | if (crc & (1 << 31)) 455 | crc = (crc << 1) ^ crc_poly; 456 | else 457 | crc <<= 1; 458 | } 459 | } 460 | 461 | return crc; 462 | } 463 | 464 | static uint32_t ware_crc(uint32_t crc, const vanmoof_ware_t *ware, const void *data, size_t length) 465 | { 466 | vanmoof_ware_t tmp; 467 | 468 | memcpy(&tmp, ware, sizeof(tmp)); 469 | tmp.crc = 0xffffffff; 470 | tmp.length = 0xffffffff; 471 | 472 | crc = crc32_calculate(crc, &tmp, sizeof(tmp)); 473 | 474 | crc = crc32_calculate(crc, data + sizeof(tmp), length - sizeof(tmp)); 475 | 476 | return crc; 477 | } 478 | 479 | static int verify_patch(const char *filename, const void *data, const patch_t *patch, int verbose) 480 | { 481 | uint32_t offset = patch->offset - MAINWARE_OFFSET; 482 | const uint16_t* inst = data + offset; 483 | 484 | for (size_t i = 0; i < patch->size; i++) { 485 | if (inst[i] != patch->expect[i]) { 486 | fprintf(stderr, "%s: patch \"%s\": @0x%08x: inst[%zu] 0x%04x != expected 0x%04x\n", 487 | filename, patch->name, patch->offset, i, inst[i], patch->expect[i]); 488 | return -1; 489 | } 490 | } 491 | 492 | if (verbose) { 493 | printf("%s: verify \"%s\": @0x%08x [%u]: OK\n", progname, patch->name, patch->offset, patch->size); 494 | } 495 | return 0; 496 | } 497 | 498 | static int verify_expected(const char *filename, const void *data, const patchset_t *set, int verbose) 499 | { 500 | int expect_ok = 1; 501 | 502 | for (size_t i = 0; i < set->n_patches; i++) { 503 | if (verify_patch(filename, data, set->patches[i], verbose) != 0) { 504 | expect_ok = 0; 505 | } 506 | } 507 | 508 | if (set->flags & PATCHSET_FLAG_VERSION) { 509 | for (size_t i = 0; i < set->n_version_patches; i++) { 510 | if (verify_patch(filename, data, set->version_patches[i], verbose) != 0) { 511 | expect_ok = 0; 512 | } 513 | } 514 | } 515 | 516 | if (set->flags & PATCHSET_FLAG_MODEL) { 517 | for (size_t i = 0; i < set->n_model_patches; i++) { 518 | if (verify_patch(filename, data, set->model_patches[i], verbose) != 0) { 519 | expect_ok = 0; 520 | } 521 | } 522 | } 523 | 524 | return expect_ok; 525 | } 526 | 527 | static void apply_patch(void *data, const patch_t *patch, int verbose) 528 | { 529 | uint32_t offset = patch->offset - MAINWARE_OFFSET; 530 | uint16_t* inst = data + offset; 531 | 532 | for (size_t i = 0; i < patch->size; i++) { 533 | inst[i] = patch->patch[i]; 534 | } 535 | 536 | if (verbose) { 537 | printf("%s: apply \"%s\": @0x%08x [%u]\n", progname, patch->name, patch->offset, patch->size); 538 | } 539 | } 540 | 541 | static void apply_patches(void *data, const patchset_t *set, int verbose) 542 | { 543 | for (size_t i = 0; i < set->n_patches; i++) { 544 | apply_patch(data, set->patches[i], verbose); 545 | } 546 | 547 | if (set->flags & PATCHSET_FLAG_VERSION) { 548 | for (size_t i = 0; i < set->n_version_patches; i++) { 549 | apply_patch(data, set->version_patches[i], verbose); 550 | } 551 | } 552 | 553 | if (set->flags & PATCHSET_FLAG_MODEL) { 554 | for (size_t i = 0; i < set->n_model_patches; i++) { 555 | apply_patch(data, set->model_patches[i], verbose); 556 | } 557 | } 558 | } 559 | 560 | int main(int argc, char** argv) 561 | { 562 | char *fake_version = NULL; 563 | char *model = NULL; 564 | uint32_t flags = 0; 565 | int verbose = 0; 566 | int opt; 567 | 568 | progname = strrchr(argv[0], '/'); 569 | if (progname) 570 | progname++; 571 | else 572 | progname = argv[0]; 573 | 574 | while ((opt = getopt(argc, argv, "f:m:v")) != -1) { 575 | switch (opt) { 576 | case 'f': 577 | fake_version = optarg; 578 | break; 579 | case 'm': 580 | model = optarg; 581 | break; 582 | case 'v': 583 | verbose++; 584 | break; 585 | default: 586 | usage(); 587 | } 588 | } 589 | 590 | if (optind >= argc) { 591 | usage(); 592 | } 593 | 594 | char *filename = argv[optind]; 595 | 596 | if (fake_version) { 597 | setup_version_patches(fake_version, verbose); 598 | flags |= PATCHSET_FLAG_VERSION; 599 | } 600 | 601 | if (model) { 602 | setup_model_patches(model, verbose); 603 | flags |= PATCHSET_FLAG_MODEL; 604 | } 605 | 606 | int fd = open(filename, O_RDWR | O_BINARY); 607 | if (fd < 0) { 608 | fprintf(stderr, "%s: open(%s): %s\n", progname, filename, strerror(errno)); 609 | exit(1); 610 | } 611 | 612 | struct stat st; 613 | if (fstat(fd, &st) < 0) { 614 | fprintf(stderr, "%s: stat(%s): %s\n", progname, filename, strerror(errno)); 615 | exit(1); 616 | } 617 | 618 | void *data = mmap(NULL, st.st_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); 619 | if (data == (void *)-1) { 620 | fprintf(stderr, "%s: mmap(%s): %s\n", progname, filename, strerror(errno)); 621 | exit(1); 622 | } 623 | 624 | close(fd); 625 | 626 | vanmoof_ware_t ware; 627 | memcpy(&ware, data, sizeof(ware)); 628 | 629 | if (le32toh(ware.magic) == WARE_MAGIC) { 630 | printf("%s: vanmoof ware magic OK\n", filename); 631 | printf("%s: vanmoof ware version %08x\n", filename, le32toh(ware.version)); 632 | printf("%s: vanmoof ware CRC 0x%08x\n", filename, le32toh(ware.crc)); 633 | printf("%s: vanmoof ware length 0x%08x\n", filename, le32toh(ware.length)); 634 | 635 | uint32_t length = le32toh(ware.length); 636 | if (length > st.st_size) { 637 | printf("%s: vanmoof ware length 0x%08x extends beyond file size 0x%08zx\n", 638 | filename, length, st.st_size); 639 | exit(1); 640 | } 641 | 642 | uint32_t crc = ware_crc(initial_crc, &ware, data, length); 643 | 644 | printf("%s: CRC 0x%08x %s\n", filename, crc, crc == le32toh(ware.crc) ? "OK" : "FAIL"); 645 | 646 | if (crc != le32toh(ware.crc)) 647 | exit(1); 648 | 649 | switch (le32toh(ware.version)) { 650 | case 0x010903f4: 651 | patchset_1_9_3.flags = flags; 652 | if ((le32toh(ware.crc) == 0x76c1ab9d) && (length == 0x0002fcc8)) { 653 | if (verify_expected(filename, data, &patchset_1_9_3, verbose)) { 654 | apply_patches(data, &patchset_1_9_3, verbose); 655 | 656 | memcpy(&ware, data, sizeof(ware)); 657 | 658 | memset(ware.date, 0xff, sizeof(ware.date)); 659 | memset(ware.time, 0xff, sizeof(ware.time)); 660 | 661 | strcpy(ware.date, patchset_1_9_3.date); 662 | strcpy(ware.time, patchset_1_9_3.time); 663 | 664 | crc = ware_crc(initial_crc, &ware, data, length); 665 | 666 | ware.crc = htole32(crc); 667 | ware.length = htole32(length); 668 | 669 | memcpy(data, &ware, sizeof(ware)); 670 | } else { 671 | fprintf(stderr, "%s: Code to patch does not match original\n", filename); 672 | exit(1); 673 | } 674 | } else { 675 | fprintf(stderr, "%s: CRC or length do not match original\n", filename); 676 | exit(1); 677 | } 678 | break; 679 | 680 | default: 681 | fprintf(stderr, "%s: No patch for this version available, yet\n", progname); 682 | exit(1); 683 | } 684 | } else { 685 | fprintf(stderr, "%s: Not a vanmoof ware file\n", filename); 686 | exit(1); 687 | } 688 | 689 | munmap(data, st.st_size); 690 | 691 | return 0; 692 | } 693 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vanmoof-tools 2 | 3 | Tools to support VanMoof S3/X3 reverse engineering efforts 4 | 5 | ## Known firmware images: 6 | 7 | - mainware.bin (MCU: ST STM32F413VGT6) 8 | - bleware.bin (MCU: TI CC2642R1F) 9 | - motorware.bin (MCU: TI TMS320F28054F) 10 | - shifterware.bin (MCU: MindMotion MM32F031F6U6) 11 | - batteryware.bin (MCU: ST STM32L072CZT6) 12 | - bmsboot.bin (MCU: see batteryware.bin) 13 | - muco-boot.bin (MCU: see mainware.bin) 14 | - bleboot.bin (MCU: see bleware.bin) 15 | - shifterboot.bin (MCU: see shifterware.bin) 16 | 17 | ## Some observations about firmware images: 18 | 19 | Most components of the bike contain a split boot loader and firmware image, the Bluetooth firmware might differ from this scheme. 20 | 21 | The boot loader starts with the typical ARM vector table and is 20Kb (0x5000 bytes) long. 22 | The last 8 bytes of the boot loader binary contain: 23 | - 4 bytes version encoded as ASCII characters 24 | - 4 bytes CRC32. 25 | 26 | The CRC is calculated using the STM32 CRC algorithm over the whole boot loader data without the last 4 bytes 27 | 28 | The firmware image starts with a magic 0xaa55aa55, followed by these header items: 29 | - 4 bytes version info 30 | - 4 bytes CRC32 31 | - 4 bytes length 32 | - 12 bytes date in ASCII 33 | - 12 bytes time in ASCII 34 | 35 | All 4 byte values are encoded little endian 36 | 37 | The CRC is calculated using the STM32 CRC algorithm over the whole image data with the header CRC and length fields both set to 0xffffffff. 38 | 39 | ## Setup / Installation 40 | 41 | You can use a Raspberry Pi, your dusty old Linux Machine or fancy new Stuff like WSL and a Linux Machine (Ubuntu or Debian is fine). 42 | 43 | ``` 44 | apt update 45 | apt install build-essential gcc-arm-none-eabi binutils-arm-none-eabi 46 | make 47 | ``` 48 | 49 | On windows, you need to install `mingw32` and the arm-none-eabi compiler toolchain, e.g. gcc 12.3.1 from [arm-eabi toolchains](https://gnutoolchains.com/arm-eabi/), then run `mingw32-make`. 50 | 51 | Sadly macos does not seem to work. (Coming soon, hopefully). 52 | 53 | ## unpack 54 | 55 | usage: `unpack ` 56 | 57 | This tool extracts the contents of a VanMoof update file, also known as PACK file. A PACK file starts with a header containing the magic "PACK", an offset to a directory structure and the length of the directory structure. The directory structure (at the end of the file) contains one or more entries containing a filename, an offset, and the length of the data. See pack.h for details of these structures. 58 | 59 | The tool will overwrite any file present in the current directory if this is contained in the PACK file. Run this in a separate directory to be shure not to loose any data. 60 | 61 | ## pack 62 | 63 | usage: `pack [ ...]` 64 | 65 | This tool packs one or more firmware files into a VanMoof update file, also known as PACK file. This is the reverse operation of the `unpack` command. 66 | 67 | ## crc32 68 | 69 | usage: `crc32 ` 70 | 71 | This tool calculates and verifies the CRC of both boot loader and firmware images. 72 | 73 | ## patch 74 | 75 | usage: `patch [-v] [-f ] [-m ] ` 76 | 77 | This tool patches a modern VanMoof mainware file, so the region OFFROAD is not reset to region US during boot. The power assistance level can be configured to 5 again. When cycling through power assistance levels using the handle bar, the bike will cycle through power level 5 as well if region is OFFROAD. 78 | 79 | The file given on the command line is overwritten with the patched version of the file, so please make a backup of your mainware before using the tool. 80 | 81 | ### Options: 82 | 83 | - `-v`: Be verbose 84 | - `-f `: Specify a different version, format: `..` 85 | - `-m `: Change default bike model, format: `,,` 86 | 87 | Currently the tool only works for mainware version 1.9.3. 88 | 89 | ## ble-patch 90 | 91 | usage: `ble-patch ` 92 | 93 | This tool patches the VanMoof bleware file, so the command `rtos-statistics` is replaced with a command `dump`. Using `dump keys` inside the bledebug console will show the stored keys. These keys are two factory default keys used during bringup of the bike in factory, your API key, and the VanMoof manufacturer key. The latter is used to encrypt firmware images (when sending updates to the APP). 94 | 95 | You can update the patched bleware on the bike by creating a pack containing this bleware and using the command `pack-upload` inside the bledebug console. You need to send the created pack using ymodem. 96 | 97 | Output looks like: 98 | 99 | ``` 100 | > dump keys 101 | Key 0x00: UKEY 52XXXXXXXXXXXXXXXXXXXXXXXXXXXX2f 00000001 000003fe CRC 81XXXX92 102 | Key 0x01: UKEY 98XXXXXXXXXXXXXXXXXXXXXXXXXXXX02 00000002 000001f4 CRC 72XXXX94 103 | Key 0x7c: M-ID 00000000000000000000000000000000 00000008 00000000 CRC 7062da7e 104 | Key 0x7d: UKEY 5f5f5f5f5f4f574e45525f5045524d53 00000000 ffffffff CRC 4f25ee68 105 | Key 0x7e: MKEY 710b2ea0dc8568b7b5e5ec0b8a39dae9 00000000 00000000 CRC a69429b6 106 | Key 0x7f: MKEY 46383841XXXXXXXXXXXXXXXX4d4f4f46 00000000 ffffffff CRC 4aXXXX7e 107 | ``` 108 | 109 | The keys 0x7d and 0x7f seem to be the factory bring-up keys, translated to ASCII they read: 110 | 111 | ``` 112 | Key 0x7d: UKEY _____OWNER_PERMS 00000000 ffffffff CRC 4f25ee68 113 | Key 0x7f: MKEY F88AXXXXXXXXMOOF 00000000 ffffffff CRC 4aXXXX7e 114 | ^- Bike MAC Address 115 | ``` 116 | 117 | This can also dump memory (i.e. ROM, internal FLASH, or external FLASH): 118 | 119 | ``` 120 | > dump mem 10000000 40000 121 | 10000000 00 20 00 11 b1 19 00 10 bf 20 00 10 c1 20 00 10 . ...... . ... .. 122 | 10000010 c3 20 00 10 c3 20 00 10 c3 20 00 10 00 00 00 00 . ... .. . ...... 123 | ... 124 | 125 | > dump extflash 5afa0 20 126 | 0005afa0 5f 5f 5f 5f 5f 4f 57 4e 45 52 5f 50 45 52 4d 53 _____OWN ER_PERMS 127 | 0005afb0 00 00 00 00 ff ff ff ff 55 4b 45 59 68 ee 25 4f ........ UKEYh.%O 128 | ``` 129 | 130 | There is a special command which shows the values of `CCFG_TI_OPTIONS` and `CCFG_TAP_DAP_*`, this command will patch to boot loader of the BLE chip to enable the JTAG debug port of the BLE chip for further debugging. The first time the command is called, it will output values like: 131 | 132 | ``` 133 | > dump ccfg 134 | CCFG_TI_OPTIONS: 0xffffff00 135 | CCFG_TAP_DAP_0: 0xff000000 136 | CCFG_TAP_DAP_1: 0xff000000 137 | JTAGCFG: 0x00000000 138 | ``` 139 | 140 | When these values are found, the last sector of flash (including the CCFG) is copied down from 0x56000 to 0x46000 while patching the CCFG values and the version string, and then copying the sector back to 0x56000. After a `reset` these new CCFG values take effect and the JTAG port is enabled: 141 | 142 | ``` 143 | > dump ccfg 144 | CCFG_TI_OPTIONS: 0xffffffc5 145 | CCFG_TAP_DAP_0: 0xffc5c5c5 146 | CCFG_TAP_DAP_1: 0xffc5c5c5 147 | JTAGCFG: 0x00000003 148 | ``` 149 | 150 | The boot loader is not touched again once these CCFG values are present. 151 | 152 | ## patch-dump 153 | 154 | This tool patches a modern VanMoof mainware as `patch` above, but adds a function to dump FLASH or memory to the console. This function is patched into the `help` command and will output FLASH or memory as hexdump. Use as `help `. 155 | 156 | The hexdump can be converted to binary using the `dump2bin.sh` script. 157 | 158 | An older version would output the whole FLASH as S-Records, the source is still provided in the repo, edit the Makefile if you want to use this function. 159 | 160 | Capture the terminal output to a logfile and clip out the S-Records to a file `vanmoof.srec`. To convert this dump to the different binaries used inside the bike, use these shell commands: 161 | 162 | ``` 163 | objcopy -I srec -O binary vanmoof.srec vanmoof.bin 164 | dd if=vanmoof.bin of=muco-boot.bin bs=4096 count=8 165 | dd if=vanmoof.bin of=vanmoof-config-a.bin bs=4096 skip=8 count=4 166 | dd if=vanmoof.bin of=vanmoof-config-b.bin bs=4096 skip=12 count=4 167 | dd if=vanmoof.bin of=shifterware.bin bs=4096 skip=16 count=16 168 | dd if=vanmoof.bin of=mainware.bin bs=4096 skip=32 count=64 169 | dd if=vanmoof.bin of=shadowware.bin bs=4096 skip=96 count=64 170 | dd if=vanmoof.bin of=motorware.bin bs=4096 skip=160 count=32 171 | dd if=vanmoof.bin of=batteryware.bin bs=4096 skip=192 count=32 172 | dd if=vanmoof.bin of=bmsboot.bin bs=4096 skip=224 count=32 173 | ``` 174 | 175 | ## Offsets in smart controller internal flash: 176 | 177 | ``` 178 | 0x08000000: stm32 boot loader 179 | 0x08008000: bike config A 180 | 0x0800c000: bike config B 181 | 0x08010000: shifterware image 182 | 0x08020000: mainware image 183 | 0x08060000: shadow image 184 | 0x080a0000: motorware image 185 | 0x080c0000: batteryware image 186 | 0x080e0000: bmsboot image 187 | ``` 188 | 189 | ## Offsets in smart controller internal SRAM: 190 | 191 | ``` 192 | 0x20000a00: Bike state/config 193 | from FLASH at 0x8008000 or 0x800c000, 0xc0 bytes 194 | + 0x0f4: Sound bitmask [1] low 195 | + 0x0f8: Sound bitmask [1] medium 196 | + 0x0fc: Sound bitmask [1] high 197 | + 0x100: Backup code 198 | + 0x102: Lux low 199 | + 0x105: Volume low 200 | + 0x106: Volume medium 201 | + 0x107: Volume high 202 | + 0x108: Shift mode: AUTO/MANUAL 203 | + 0x109: Region 204 | + 0x10a: Unit system 205 | + 0x10b: Wheel motor type 206 | + 0x10c: Light mode 207 | + 0x10e: Shift up EU (3 x uint16_t) 208 | + 0x114: Shift up US (3 x uint16_t) 209 | + 0x11a: Shift up JP (3 x uint16_t) 210 | + 0x120: Shift up OFFROAD (3 x uint16_t) 211 | + 0x126: Shift down EU (3 x uint16_t) 212 | + 0x12c: Shift down US (3 x uint16_t) 213 | + 0x132: Shift down JP (3 x uint16_t) 214 | + 0x138: Shift down OFFROAD (3 x uint16_t) 215 | + 0x140: Mainware version 216 | + 0x144: Region lock 217 | + 0x145: Model: Bit0: 0=ES3, 1=ES4; Bit1: 1=E-Shifter, Bit2: 1=Display 218 | + 0x146: Custom soc 219 | + 0x147: HW revision 220 | + 0x1c0: CRC32 over FLASH data 221 | 222 | Motor support settings (from mainware): 223 | + 0x2c6: Motor percent power level 0 224 | + 0x2c8: Motor speed limit power level 0 225 | + 0x2ca: Motor percent power level 1 226 | + 0x2cc: Motor speed limit power level 1 227 | + 0x2ce: Motor percent power level 2 228 | + 0x2d0: Motor speed limit power level 2 229 | + 0x2d2: Motor percent power level 3 230 | + 0x2d4: Motor speed limit power level 3 231 | + 0x2d6: Motor percent power level 4 232 | + 0x2d8: Motor speed limit power level 4 233 | + 0x2d6: Motor percent power level 5 234 | + 0x2d8: Motor speed limit power level 5 235 | 236 | from EEPROM, 0x3c bytes 237 | + 0x310: Alarm state 238 | + 0x311: Play lock sound 239 | + 0x312: Remote locked 240 | + 0x313: Logging APP/Serial 241 | + 0x314: Shipping 242 | + 0x315: Cached BMS soc 243 | + 0x316: Power level + Boost 244 | + 0x317: Alarm enable/disable 245 | + 0x318: Horn file index 246 | + 0x31c: Odometer: km * 10 247 | + 0x320: Timestamp bell button 248 | + 0x324: Timestamp boost button 249 | + 0x328: Timestamp GSM check 250 | + 0x32c: Firmware update order (6 bytes) 251 | + 0x332: BMS soc override 252 | + 0x333: BLE sleep request 253 | + 0x334: Shifter retries 254 | + 0x336: Shifter firmware version 255 | + 0x338: Shifter total shifts 256 | + 0x33c: GSM tracking heartbeat 257 | + 0x340: Kicklock state 258 | + 0x341: Battery state 259 | + 0x342: BMS firmware version 260 | + 0x344: Wake counter 261 | + 0x348: CRC32 over EEPROM data 262 | 263 | Bike state 264 | + 0x34c: BLE debug flag 265 | + 0x34d: GSM debug flag 266 | + 0x34e: Shift debug flag 267 | + 0x34f: BMS debug flag 268 | + 0x358: Loop count actual 269 | + 0x35c: Loop count min 270 | + 0x360: Loop count max 271 | + 0x364: Motor error 272 | + 0x366: Motor speed 273 | + 0x368: Motor I 274 | + 0x36a: Motor m_tmp 275 | + 0x36c: Motor d_tmp 276 | + 0x36e: Motor wlsp 277 | + 0x370: Motor Ubat 278 | + 0x372: Motor Pdsp 279 | + 0x374: Motor Pdtrg 280 | + 0x376: Motor io 281 | + 0x388: Motor firmware version 282 | + 0x38c: BLE firmware version 283 | + 0x390: BLE MAC address (6 bytes) 284 | + 0x396: BMS ESN (18 bytes) 285 | + 0x3a8: Debug password (16 bytes) 286 | + 0x3ba: Lipo version 287 | + 0x3c0: Error flags (2 x uint32_t) 288 | + 0x3cc: Speed 1 289 | + 0x3ce: Speed 2 290 | + 0x3d1: Power level (init from 0x316 & 0x7f) 291 | + 0x3d2: Power level (copy, init from 0x316 & 0x7f) 292 | + 0x3d3: Ride change 293 | + 0x3e0: Lipo state 294 | + 0x3e4: Powerbank soc 295 | + 0x3e6: Powerbank serial number (4 bytes) 296 | + 0x3ea: Powerbank serial version (3 bytes) 297 | + 0x3ed: Powerbank soh 298 | + 0x3ee: Powerbank noc 299 | + 0x3f1: Powerbank present 300 | + 0x3f8: GSM type pointer 301 | 302 | Battery state 303 | + 0x402: Type 304 | + 0x406: Error flags 305 | + 0x408: Temperature (°C/100) 306 | + 0x40a: Voltage (mV) 307 | + 0x40c: State of charge (%) 308 | + 0x40e: Current (mA) 309 | + 0x412: Discharging flag 310 | + 0x414: Testmode flag 311 | + 0x416: HW version 312 | + 0x418: SW version 313 | + 0x41a: Serial number (14 bytes) 314 | + 0x428: Manufacture date (3 bytes) 315 | + 0x42c: cap_nominal 316 | + 0x42e: cap_full 317 | + 0x430: cap_remain 318 | + 0x432: health 319 | + 0x434: cycle_count 320 | + 0x438: cell_voltage (10 x mV) 321 | + 0x44c: tp1 322 | + 0x44e: tp2 323 | + 0x450: mos_tmp 324 | + 0x454: u_max 325 | + 0x456: u_min 326 | + 0x45a: Boot loader version 327 | + 0x462: fsr 328 | + 0x490: dotp 329 | + 0x492: dutp 330 | + 0x494: cotp 331 | + 0x496: cutp 332 | + 0x498: docp1 333 | + 0x49a: docp2 334 | + 0x49c: cocp1 335 | + 0x49e: cocp2 336 | + 0x4a0: ovp1 337 | + 0x4a2: ovp2 338 | + 0x4a4: uvp1 339 | + 0x4a6: uvp2 340 | + 0x4a8: pdocp 341 | + 0x4aa: pdscp 342 | + 0x4ac: motp 343 | + 0xac scp 344 | ``` 345 | `[1]` [bitmask](vanmoof_sx3_sound_bitmask.md) 346 | 347 | ## Offsets in BLE controller internal ROM: 348 | 349 | ``` 350 | 10000000: TI ROM boot loader 351 | 10007000: TI ROM ble5stack 352 | 1002b400: TI ROM tirtos7 353 | 354 | 50001000: FCFG1 355 | 500012e8: FCFG1::MAC_BLE_0 356 | 500012ec: FCFG1::MAC_BLE_1 357 | 358 | 50003000: CCFG (mirrored from FLASH 56000) 359 | ``` 360 | 361 | ## Offsets in BLE controller internal FLASH: 362 | 363 | ``` 364 | 00000000: bleware.bin 365 | 366 | 00056000: boot loader 367 | 00057f38: boot loader version: "BVERApr 23 2020" 368 | 00057f48: boot loader version: "14:10:12" 369 | 370 | 00057fa8: CCFG (Customer configuration) Mirrored at CCFG 50004fa8 371 | 00057fec: CCFG::IMAGE_VALID_CONF -> 56000 (boot loader entry) 372 | ``` 373 | 374 | ## Offsets in BLE controller internal SRAM: 375 | 376 | ``` 377 | 2000a3dc: Memory location of UKEY, when used in BLE protocol (1.4.1) 378 | 2000a3fc: Memory location of MKEY, when used in BLE protocol (1.4.1) 379 | 380 | 2000cec8: Memory location of UKEY, when used in BLE protocol (2.4.1) 381 | 2000cee8: Memory location of MKEY, when used in BLE protocol (2.4.1) 382 | ``` 383 | 384 | ## update.py 385 | 386 | A simple cheasy update tool to send firmware packed with `pack` to the bike. This needs [pymoof](https://github.com/quantsini/pymoof) to run. 387 | 388 | You need to insert your bikes API key and manufacturer key before using the tool. 389 | 390 | Use as a reference for the firmware update over BLE. 391 | 392 | ## read_logs.py 393 | 394 | A simple cheasy tool to read the internal debug logs from the bike using BLE. This needs [pymoof](https://github.com/quantsini/pymoof) to run. 395 | 396 | You need to insert your bikes API key before using the tool. 397 | 398 | Use as a reference howto read logs over BLE. 399 | 400 | ## Internal communication 401 | 402 | Main MCU communicates with the other MCUs via: 403 | 404 | | UART | TX | RX | Function | Protocol | Comments | 405 | | :-- | :-- | :-- | :-- | :-- | :-- | 406 | | USART1 | PA9 | PA10 | Alternative debug | console | maybe accessed through debug header TC1? | 407 | | USART2 | PA2 | PA3 | GSM uBlox G350 | AT commands | passthrough in `gsmdebug` mode | 408 | | USART3 | PD8 | PD9 | Shifter MCU | [Modbus](https://en.wikipedia.org/wiki/Modbus) || 409 | | UART4 | PA0 | PA1 | Battery MCU | [Modbus](https://en.wikipedia.org/wiki/Modbus) || 410 | | UART5 | PB13 | PB12 | BLE MCU control | SSP | [SLIP](https://en.wikipedia.org/wiki/Serial_Line_Internet_Protocol) encoded packets | 411 | | USART6 | PC6 | PC7 | Motor MCU | SSP | [SLIP](https://en.wikipedia.org/wiki/Serial_Line_Internet_Protocol) encoded packets | 412 | | UART7 | PE8 | PE7 | Main MCU debug | console | port behind rear light | 413 | | UART8 | PE1 | PE0 | BLE MCU debug | console | passthrough in `bledebug` mode | 414 | 415 | The [SLIP](https://en.wikipedia.org/wiki/Serial_Line_Internet_Protocol) encoded packets contain one byte sender address, a command byte, a sequence byte, 416 | a little endian 16 bit word which is an offset or a function code, a 16 bit data length word, data, and a 16 bit CRC, which is the same as the [Modbus](https://en.wikipedia.org/wiki/Modbus) CRC. 417 | 418 | The command byte is 06 for READ, 07 for WRITE, and 05 for ACK packets. The CRC is calculated over the packet without the C0 framing characters. 419 | 420 | ``` 421 | C0 01 06 56 1A 01 33 F8 C0 MCU -> BLE READ req 56: 0x011a 422 | C0 02 05 56 53 6E C0 BLE -> MCU ACK 56 423 | 424 | C0 02 07 79 1A 01 06 00 F8 8A 5E XX XX XX YY YY C0 BLE -> MCU WRITE req 79: 0x011a: 0x0006 bytes: 0xF8 8A 5E XX XX XX 425 | C0 01 05 79 E2 B2 C0 MCU -> BLE ACK 79 426 | ``` 427 | 428 | The MCU handles both packet streams from the BLE and the Motor MCU inside the same packet handler, so the offsets/function codes of the BLE and the Motor need to be disjunct. 429 | 430 | 431 | ## BLE service `6acc5505-e631-4069-944d-b8ca7598ad50` 432 | 433 | The bluetooth service `@5505` contains backoffice messages, these are used to configure UKEYs or MKEYs on the bike, for example for bike sharing or workshop access. These messages are encrypted using the bikes MKEY. The messages are prefixed with a two byte nonce, the byte `0x01` and an offset byte. These 4 bytes are not encrypted. The message continues with n * 16 bytes MKEY encrypted data. 434 | 435 | | Nonce | Const 1 | Offset | Encrypted backoffice message | 436 | | :-- | :-- | :-- | :-- | 437 | | a12d | 01 | 00 | acc3d0f327c70f5a4755185bcb27c40df508b19df62e7551127abe79c9c822326adef001d97d51b45f8c58a7d2cc0cc0 | 438 | 439 | The decrypted message is build up like this (format 1): 440 | 441 | | M-ID | Cmd | Len | UKEY data | Index | Perms | Modbus CRC | Padding | 442 | | :-- | :-- | :-- | :-- | :-- | :-- | :-- | :-- | 443 | | 00000008 | 0001 | 18 | 98d29703b832207ed7c67b34edfadc02 | 00000002 | 000001f4 | 6845 | 0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f | 444 | 445 | Message format 2: 446 | 447 | | M-ID | Cmd | Len | Index | Modbus CRC | Padding | 448 | | :-- | :-- | :-- | :-- | :-- | :-- | 449 | | 00000004 | 0003 | 04 | 00000002 | 2577 | 0505050505 | 450 | 451 | Message format 3: 452 | 453 | | M-ID | Cmd | Len | State | Modbus CRC | Padding | 454 | | :-- | :-- | :-- | :-- | :-- | :-- | 455 | | 00000007 | 0005 | 01 | 00 | 5ae4 | 0808080808080808 | 456 | 457 | Message format 4: 458 | 459 | | M-ID | Cmd | Len | Modbus CRC | Padding | 460 | | :-- | :-- | :-- | :-- | :-- | 461 | | 00000005 | 0006 | 00 | 6c18 | 090909090909090909 | 462 | 463 | Message format 5: 464 | 465 | | M-ID | Cmd | Len | UKEY data | Index | Perms | ... | UKEY data | Index | Perms | Modbus CRC | Padding | 466 | | :-- | :-- | :-- | :-- | :-- | :-- | :-- | :-- | :-- | :-- | :-- | :-- | 467 | | 00000008 | 0007 | 48 | 98d29703b832207ed7c67b34edfadc02 | 00000002 | 000001f4 | ... | cb27c40df508b19df62e7551127abe79 | 00000004 | 000001f4 | xxxx | 07070707070707 | 468 | 469 | Message format 6: 470 | 471 | | M-ID | Cmd | Len | Unknown1 | Unknown2 | Modbus CRC | Padding | 472 | | :-- | :-- | :-- | :-- | :-- | :-- | :-- | 473 | | 00000004 | 0008 | 02+n | xxxx | yy .. yy (n bytes) | zzzz | .. | 474 | 475 | The possible Cmd values are: 476 | 477 | | Cmd | Message (format) | 478 | | :-- | :-- | 479 | | 0001 | Update UKEY (1) | 480 | | 0002 | Update MKEY (1) | 481 | | 0003 | Erase key by index (2) | 482 | | 0004 | Nothing (always succeed) | 483 | | 0005 | Set Module State `@5562` (3) | 484 | | 0006 | Erase all keys (4) | 485 | | 0007 | Update multiple keys (5) | 486 | | 0008 | Unknown, read keys? (6) | 487 | | 0009 | Unknown, FMNA related | 488 | | 000a | Unknown, FMNA related | 489 | 490 | ## Debug console 491 | 492 | The `Login:` prompt on the debug console knows two passwords, one fixed password hardcoded in the firmware `vEVjGF!paYsM2EBV8SoDT8*T0eB&#T6xevaoxCaO` and one password containing the last three bytes of the bikes MAC address followed by the word "DeBug", as output by `printf("%02X%02X%02XDeBug", MAC[3], MAC[4], MAC[5])`. 493 | 494 | The debug console has a `help` command. The BLE chip and the GSM modem can also be accessed from the debug port, by using the commands `bledebug` and `gsmdebug` respectively. 495 | 496 | ``` 497 | Login: *********** 498 | Welcome to ES3 499 | 500 | help 501 | Available commands: 502 | help This tekst 503 | reboot reboot CPU 504 | login Login shell 505 | logout Logout shell 506 | ver Software version 507 | distance Manual set dst 508 | gear set gear 509 | region Region 0..3 510 | model model 511 | blereset hard reset BLE 512 | bledebug redirect uart8 513 | show Parameters 514 | motorupdate Update F2806 CPU 515 | vollow Audio volume 516 | volmid Audio volume 517 | volhigh Audio volume 518 | speed override speed 519 | loop main loop time 520 | shipping Shipping mode 521 | factory-shipping Factory shipping mode (ignores BMS) 522 | logprn Print log 523 | logclr Clear log 6 524 | logapp 1/ 0 525 | powerchange 1/ 0 526 | factory Load factory defaults 527 | battery Show battery 528 | batware Battery update 529 | batboot BatteryBL update 530 | batreset Battery reset 531 | shiftware Battery update 532 | shifterstatus Show shifter 533 | shiftdebug Show Modbus 534 | shiftresetcounter Reset shift counter 535 | motorstatus 536 | gsminfo Info from Ublox 537 | gsmstart start GSM function 538 | gsmdebug redirect uart2 539 | bmsdebug Show Modbus 540 | sound sample,volume,times 541 | adc read adc 542 | bwritereg Modbus Bat write register 543 | bwritedata Modbus Bat write data 544 | breadreg Modbus Bat read register 545 | swritereg Modbus Shift write register 546 | swritedata Modbus Shift write data 547 | sreadreg Modbus Shift read register 548 | stc read lipo monitor 549 | stcreset 550 | setoad test 551 | setgear save muco shifter 552 | soc overrule soc 553 | customsoc sound soc 554 | hwrev hardware revision 555 | error set errorcode 556 | 557 | ver 558 | ES3.0 Main 1.09.03 (10:30:52 Apr 30 2025) 559 | ES3 boot 1.9 560 | Motorware S.0.00.22 561 | BMSWare BL:007 FW:1.17 RSOC:100 Cycles:42 HW:3.10 ESN:XXXXXXXXXXXXXX 562 | Shifterware 0.237 stored: 0.237 563 | BLEWare 1.4.01 564 | GSMWare 08.90 565 | CMD_BLE_MAC F8:8A:5E:XX:XX:XX 566 | ``` 567 | 568 | The BLE console also has a `help` command and one can return to the MCU console with the command `exit`. 569 | 570 | ``` 571 | Login: *********** 572 | Welcome to ES3 573 | bledebug 574 | Connect to UART8 575 | 576 | > help 577 | The following commands are available: 578 | 579 | firmware-update - update a new image of firmware to the external flash 580 | extflash-verify - verify the current flashchip 581 | log-count - get log-count statistic 582 | log-dump - print blocks starting at address 583 | log-flush - flush all log-entries 584 | log-inject - Create fake-logs 585 | audio-play - play audio bound to the specified index 586 | audio-stop - stop playing the current audio file 587 | audio-dump - dump all audio files in external memory 588 | audio-upload - upload audio binary using Y-Modem at the address linked to the specified index 589 | audio-volume-set-all - set audio level of all audio-clips (0-3) 590 | pack-upload - upload a PACK file by Y-Modem 591 | pack-list - list the contents of a PACK file 592 | pack-delete - delete a PACK file 593 | pack-process - process pack files in external flash memory 594 | ble-info - dump current BLE connection info / statistics 595 | ble-disconnect - force a disconnect of all connected devices 596 | ble-erase-all-bonds - erase all bonds 597 | shutdown - shutdown the system 598 | rtos-statistics - dump memory stats every 500ms 599 | rtos-nvm-compact - Compact the non-volatile storage 600 | dump - dump keys/memory/extflash 601 | info/ver - show basic firmware info 602 | exit - exit from shell 603 | help - show all monitor commands 604 | 605 | > info 606 | BLE MAC Address: "f8:8a:5e:xx:xx:xx" 607 | 608 | Device name ................ : ES3-F88A5EXXXXXX 609 | Firmware version ........... : 1.04.01 610 | Compile date / time ........ : May 12 2025 / 09:03:35 611 | BIM firmware version ....... : 1.00.00 612 | BIM compile date / time .... : Apr 23 2020 / 14:10:12 613 | reset type ................. : pin reset 614 | systick .................... : -117259002 615 | 616 | > exit 617 | ``` 618 | 619 | The GSM console talks extended ublox [AT](https://en.wikipedia.org/wiki/Hayes_AT_command_set) commands. It can be exited with the sequence `[14~`. 620 | 621 | ``` 622 | Login: *********** 623 | Welcome to ES3 624 | gsmdebug 625 | Modem powering on.. 626 | 627 | ATI 628 | SARA-G350-02S-01 629 | 630 | OK 631 | AT+UGSRV? 632 | +UGSRV: "ublox1.vanmoof.com","ublox1.vanmoof.com","PBNjh0V46Eev8CcfS4LPJg",14,4,1,65,0,15 633 | 634 | OK 635 | AT+UPSDA=0,3 636 | OK 637 | AT+UPSND=0,8 638 | +UPSND: 0,8,1 639 | 640 | OK 641 | AT+ULOCIND=1 642 | OK 643 | AT+ULOC=2,2,1,180,1,10 644 | OK 645 | 646 | +UULOCIND: 0,0 647 | 648 | +UULOCIND: 1,0 649 | 650 | +UULOCIND: 2,0 651 | 652 | +UULOCIND: 3,0 653 | 654 | +UULOC: 16/05/2025,08:27:54.000,,,0,601,0,0,0,2,0,0,0 655 | ``` 656 | 657 | 658 | ## Ideas 659 | 660 | I see a crash of the controller when sending firmware update packets > 256 bytes, so there might be the chance to use this as an exploit to read out the MKEY over bluetooth. We need the MKEY to be able to: 661 | - decode the update packages received from Vanmoof 662 | - send our own update package (patched with features like offroad) to the bike 663 | 664 | 665 | ## External resources 666 | 667 | - [Wiring harness](https://www.moofrepair.nl/wiring-harness/) 668 | - [Debug console](https://www.reddit.com/r/vanmoofbicycle/comments/17744l7/comment/k4z0wyi/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button) 669 | - [Hardware info](https://github.com/ciborg971/VanmoofX3RE/tree/master/OG) 670 | - [Hardware info](https://github.com/dtngx/VMBattery) 671 | - [pymoof](https://github.com/quantsini/pymoof) 672 | 673 | 674 | ## Special thanks! 675 | 676 | - [Tobias](https://github.com/Knight1) 677 | - [Quinten](https://github.com/quintenadema) 678 | - [Max](https://github.com/MPeek1995) 679 | 680 | 681 | ## Fun facts 682 | 683 | The magic numbers used by VanMoof have been used previously by others, this gives some funny output when using the unix `file` command: 684 | 685 | ``` 686 | $ file packfile.bin 687 | packfile.bin: Quake I or II world or extension, 340 entries 688 | 689 | $ file mainware.bin 690 | mainware.bin: BIOS (ia32) ROM Ext. (85*512) 691 | ``` 692 | --------------------------------------------------------------------------------