├── web ├── __init__.py ├── static │ ├── favicon.png │ └── style.css ├── app.py └── templates │ └── home.html ├── .gitignore ├── xiaotea ├── __init__.py ├── enc.py ├── dec.py └── xiaotea.py ├── bins ├── DRV130.bin ├── DRV134.bin ├── DRV138.bin ├── DRV140.bin ├── DRV141.bin ├── DRV142.bin └── DRV143.bin └── patcher.py /web/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | -------------------------------------------------------------------------------- /xiaotea/__init__.py: -------------------------------------------------------------------------------- 1 | from .xiaotea import XiaoTea -------------------------------------------------------------------------------- /bins/DRV130.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotoX/xiaomi-m365-firmware-patcher/HEAD/bins/DRV130.bin -------------------------------------------------------------------------------- /bins/DRV134.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotoX/xiaomi-m365-firmware-patcher/HEAD/bins/DRV134.bin -------------------------------------------------------------------------------- /bins/DRV138.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotoX/xiaomi-m365-firmware-patcher/HEAD/bins/DRV138.bin -------------------------------------------------------------------------------- /bins/DRV140.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotoX/xiaomi-m365-firmware-patcher/HEAD/bins/DRV140.bin -------------------------------------------------------------------------------- /bins/DRV141.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotoX/xiaomi-m365-firmware-patcher/HEAD/bins/DRV141.bin -------------------------------------------------------------------------------- /bins/DRV142.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotoX/xiaomi-m365-firmware-patcher/HEAD/bins/DRV142.bin -------------------------------------------------------------------------------- /bins/DRV143.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotoX/xiaomi-m365-firmware-patcher/HEAD/bins/DRV143.bin -------------------------------------------------------------------------------- /web/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotoX/xiaomi-m365-firmware-patcher/HEAD/web/static/favicon.png -------------------------------------------------------------------------------- /xiaotea/enc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from sys import argv, exit 3 | from os.path import getsize 4 | from xiaotea import XiaoTea 5 | 6 | if len(argv) != 3: 7 | exit('Usage: ' + argv[0] + ' ') 8 | 9 | cry = XiaoTea() 10 | 11 | hfi = open(argv[1], 'rb') 12 | hfo = open(argv[2], 'wb') 13 | 14 | hfo.write(cry.encrypt(hfi.read())) 15 | 16 | hfo.close() 17 | hfi.close() 18 | -------------------------------------------------------------------------------- /web/static/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | .header { 5 | background-color: #f1f1f1; 6 | padding: 15px; 7 | } 8 | .header > h1 { 9 | margin-bottom: 0.1em 10 | } 11 | .content { 12 | background-color: #f1f1f1; 13 | text-align: left; 14 | padding: 10px; 15 | margin-top: 7px; 16 | } 17 | @media only screen and (max-width:500px) { 18 | /* For mobile phones: */ 19 | .content ul { 20 | padding-left: 10px; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /xiaotea/dec.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from sys import argv, exit 3 | from os.path import getsize 4 | from xiaotea import XiaoTea 5 | 6 | if len(argv) != 3: 7 | exit('Usage: ' + argv[0] + ' ') 8 | 9 | fsize = getsize(argv[1]) 10 | 11 | if fsize % 8: 12 | exit('Wrong input file size !') 13 | 14 | cry = XiaoTea() 15 | 16 | hfi = open(argv[1], 'rb') 17 | hfo = open(argv[2], 'wb') 18 | 19 | hfo.write(cry.decrypt(hfi.read())) 20 | 21 | hfo.close() 22 | hfi.close() 23 | -------------------------------------------------------------------------------- /xiaotea/xiaotea.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Taken from https://electro.club/f/50300 and modified a bit 3 | from struct import pack, unpack 4 | 5 | UPDKEY = b'\xFE\x80\x1C\xB2\xD1\xEF\x41\xA6\xA4\x17\x31\xF5\xA0\x68\x24\xF0' 6 | 7 | def tea_encrypt_ecb(block, key): 8 | y, z = unpack('> 5) + k[1]))) & 0xFFFFFFFF 15 | z = (z + (((y << 4) + k[2]) ^ (y + s) ^ ((y >> 5) + k[3]))) & 0xFFFFFFFF 16 | return pack('> 5) + k[3]))) & 0xFFFFFFFF 25 | y = (y - (((z << 4) + k[0]) ^ (z + s) ^ ((z >> 5) + k[1]))) & 0xFFFFFFFF 26 | s = (s - 0x9E3779B9) & 0xFFFFFFFF 27 | return pack('> 16) & 0xFFFF) | ((s & 0xFFFF) << 16)) ^ 0xFFFFFFFF 40 | 41 | def pad(data): 42 | # The data which will be encrypted must be 8 byte aligned! 43 | # We also have to write a checksum to the last 4 bytes. 44 | # Zero pad for 4-byte aligning first: 45 | sz = len(data) 46 | if sz % 4: 47 | o = (4 - (sz % 4)) 48 | data += b'\x00' * o 49 | sz += o 50 | 51 | # If we're 8-byte aligned now then add 4 zero pad bytes 52 | if (sz % 8) == 0: 53 | data += b'\x00\x00\x00\x00' 54 | 55 | # so we can add our 4 checksum bytes and be 8-byte aligned 56 | return data + pack('= 0 and kers_min_speed <= 100 52 | patcher.kers_min_speed(kers_min_speed) 53 | 54 | speed_params = flask.request.args.get('speed_params', None) 55 | if speed_params: 56 | speed_normal_kmh = int(flask.request.args.get('speed_normal_kmh', None)) 57 | assert speed_normal_kmh >= 0 and speed_normal_kmh <= 100 58 | speed_normal_phase = int(flask.request.args.get('speed_normal_phase', None)) 59 | assert speed_normal_phase >= 0 and speed_normal_phase <= 65535 60 | speed_normal_battery = int(flask.request.args.get('speed_normal_battery', None)) 61 | assert speed_normal_battery >= 0 and speed_normal_battery <= 65535 62 | speed_eco_kmh = int(flask.request.args.get('speed_eco_kmh', None)) 63 | assert speed_eco_kmh >= 0 and speed_eco_kmh <= 100 64 | speed_eco_phase = int(flask.request.args.get('speed_eco_phase', None)) 65 | assert speed_eco_phase >= 0 and speed_eco_phase <= 65535 66 | speed_eco_battery = int(flask.request.args.get('speed_eco_battery', None)) 67 | assert speed_eco_battery >= 0 and speed_eco_battery <= 65535 68 | patcher.speed_params(speed_normal_kmh, speed_normal_phase, speed_normal_battery, speed_eco_kmh, speed_eco_phase, speed_eco_battery) 69 | 70 | brake_params = flask.request.args.get('brake_params', None) 71 | if brake_params: 72 | brake_limit = int(flask.request.args.get('brake_limit', None)) 73 | assert brake_limit >= 1 and brake_limit <= 130 74 | brake_i_min = int(flask.request.args.get('brake_i_min', None)) 75 | assert brake_i_min >= 0 and brake_i_min <= 65535 76 | brake_i_max = int(flask.request.args.get('brake_i_max', None)) 77 | assert brake_i_max >= brake_i_min and brake_i_max <= 65535 78 | patcher.brake_params(brake_limit, brake_i_min, brake_i_max) 79 | 80 | motor_start_speed = flask.request.args.get('motor_start_speed', None) 81 | if motor_start_speed is not None: 82 | motor_start_speed = float(motor_start_speed) 83 | assert motor_start_speed >= 0 and motor_start_speed <= 100 84 | patcher.motor_start_speed(motor_start_speed) 85 | 86 | cruise_control_delay = flask.request.args.get('cruise_control_delay', None) 87 | if cruise_control_delay is not None: 88 | cruise_control_delay = float(cruise_control_delay) 89 | assert cruise_control_delay >= 0.1 and cruise_control_delay <= 20.0 90 | patcher.cruise_control_delay(cruise_control_delay) 91 | 92 | cruise_control_nobeep = flask.request.args.get('cruise_control_nobeep', None) 93 | if cruise_control_nobeep: 94 | patcher.cruise_control_nobeep() 95 | 96 | instant_eco_switch = flask.request.args.get('instant_eco_switch', None) 97 | if instant_eco_switch: 98 | patcher.instant_eco_switch() 99 | 100 | boot_with_eco = flask.request.args.get('boot_with_eco', None) 101 | if boot_with_eco: 102 | patcher.boot_with_eco() 103 | 104 | voltage_limit = flask.request.args.get('voltage_limit', None) 105 | if voltage_limit is not None: 106 | voltage_limit = float(voltage_limit) 107 | assert voltage_limit >= 43.01 and voltage_limit <= 100.00 108 | patcher.voltage_limit(voltage_limit) 109 | 110 | russian_throttle = flask.request.args.get('russian_throttle', None) 111 | if russian_throttle: 112 | patcher.russian_throttle() 113 | 114 | remove_hard_speed_limit = flask.request.args.get('remove_hard_speed_limit', None) 115 | if remove_hard_speed_limit: 116 | patcher.remove_hard_speed_limit() 117 | 118 | remove_charging_mode = flask.request.args.get('remove_charging_mode', None) 119 | if remove_charging_mode: 120 | patcher.remove_charging_mode() 121 | 122 | stay_on_locked = flask.request.args.get('stay_on_locked', None) 123 | if stay_on_locked: 124 | patcher.stay_on_locked() 125 | 126 | bms_uart_76800 = flask.request.args.get('bms_uart_76800', None) 127 | if bms_uart_76800: 128 | patcher.bms_uart_76800() 129 | 130 | wheel_speed_const = flask.request.args.get('wheel_speed_const', None) 131 | if wheel_speed_const: 132 | wheel_speed_const = int(wheel_speed_const) 133 | assert wheel_speed_const >= 200 and wheel_speed_const <= 500 134 | patcher.wheel_speed_const(wheel_speed_const) 135 | 136 | # make zip file for firmware 137 | zip_buffer = io.BytesIO() 138 | zip_file = zipfile.ZipFile(zip_buffer, 'a', zipfile.ZIP_DEFLATED, False) 139 | 140 | zip_file.writestr('FIRM.bin', patcher.data) 141 | md5 = hashlib.md5() 142 | md5.update(patcher.data) 143 | 144 | patcher.encrypt() 145 | zip_file.writestr('FIRM.bin.enc', patcher.data) 146 | md5e = hashlib.md5() 147 | md5e.update(patcher.data) 148 | 149 | info_txt = 'dev: M365;\nnam: {};\nenc: B;\ntyp: DRV;\nmd5: {};\nmd5e: {};\n'.format( 150 | version, md5.hexdigest(), md5e.hexdigest()) 151 | 152 | zip_file.writestr('info.txt', info_txt.encode()) 153 | zip_file.comment = flask.request.url.encode() 154 | zip_file.close() 155 | zip_buffer.seek(0) 156 | content = zip_buffer.getvalue() 157 | zip_buffer.close() 158 | 159 | resp = flask.Response(content) 160 | filename = version + '-' + str(int(time.time())) + '.zip' 161 | resp.headers['Content-Type'] = 'application/zip' 162 | resp.headers['Content-Disposition'] = 'inline; filename="{0}"'.format(filename) 163 | resp.headers['Content-Length'] = len(content) 164 | 165 | return resp 166 | 167 | if __name__ == '__main__': 168 | app.run('0.0.0.0') 169 | -------------------------------------------------------------------------------- /web/templates/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Xiaomi Mijia M365 Custom Firmware Toolkit 11 | 12 | 13 | 14 | 15 |
16 |

Xiaomi Mijia M365 Custom Firmware Toolkit

17 | Spanish 18 | French 19 | German 20 | Russian 21 |
22 | 23 |
24 |

25 | Configure your own custom firmware by adjusting the options below.
26 | There are safety checks in place to ensure your scooter will not be bricked.
27 | Be aware that a higher motor power will shorten the lifetime of your battery and could damage your motor.
28 | By default nothing will be patched, enable patches with the "Patch?" checkbox next to them. 29 |

30 | 31 |

32 | Presets: 33 | 34 | 35 | 36 | 37 |

38 | 39 | 40 | 41 |
42 |
43 |
    44 |
  • 45 | 55 |

    1.3.8 is recommended over 1.4.0, it provides a much smoother riding experience.

    56 |
  • 57 | 58 |
  • 59 | 61 | 62 |

    63 | The scooter will start braking by itself when it goes above this speed and you're not accelerating.
    64 | If you want "KERS OFF" then just put this to 40 km/h. 65 | You will still have recuperative braking when using the brake lever.
    66 |

    67 |
  • 68 | 69 |
  • 70 | 72 |

    73 |
    74 |
    75 | 76 |

    77 |

    78 |
    79 |
    80 | 81 |

    82 |

    83 | For Smoothness: Stock (10S) battery: Max 31 km/h, 12S battery: Max 36 km/h
    84 | To convert the old power constant to mA, use (51575/constant)*17000
    85 | Motor phases are PWM controlled and instantaneous current exceeds the average. As a rough guide, use 2.0-2.5x battery current. 86 |

    87 |
  • 88 | 89 |
  • 90 | 92 |

    93 |
    94 |
    95 | 96 |

    97 |
  • 98 | 99 |
  • 100 | 102 | 103 |

    Minimum speed in km/h before the engine will start.

    104 |
  • 105 | 106 |
  • 107 | 109 | 110 |
    111 |

    How many seconds it takes for cruise control to kick in.

    112 |
  • 113 | 114 |
  • 115 | 117 |
    118 |

    Disables beep sound when cruise control enables.

    119 |
  • 120 | 121 |
  • 122 | 124 |

    125 | Switch instantly between Eco and Normal mode while driving.
    126 | Warning! This can be very sudden and cause injury! 127 |

    128 |
  • 129 | 130 |
  • 131 | 133 |

    The scooter will always start in Eco mode.

    134 |
  • 135 | 136 |
  • 137 | 139 |

    140 | Changes the throttle algorithm from speed-control to power-control.
    141 | Ignores speed control params. 142 |

    143 |
  • 144 | 145 |
  • 146 | 148 | 149 |

    If you want to connect a custom battery with a higher voltage.

    150 |
  • 151 | 152 |
  • 153 | 155 |

    Removes code which checks that the speed is below 34.78km/h (12000/345).

    156 |
  • 157 | 158 |
  • 159 | 161 |

    Fixes issue of scooter going into charge mode with an external battery connected in parallel.

    162 |
  • 163 | 164 |
  • 165 | 167 |

    Disables auto shutdown when the scooter is locked so it stays on forever.

    168 |
  • 169 | 170 |
  • 171 | 173 |

    Only if you use the compatible open source BMS!

    174 |
  • 175 | 176 |
  • 177 | 179 | 180 |

    For 10" wheels use 315, don't change otherwise.

    181 |
  • 182 |
183 |
184 | 185 |

186 | Make sure to double check all of your entered values before submitting!
187 | I AM RESPONSIBLE FOR THE ENTERED VALUES: 188 | 189 | 190 | 191 |

192 |
193 | 194 |

195 | ⚠ NEW The tool now makes .zip files with both encrypted and unencrypted firmware and an info.txt inside.
196 | ⚠ NEW Use the following app made by CamiAlfa to flash your modified firmware: m365 DownG
197 | It will automatically chose the correct firmware for your scooter. Don't try to flash the .zip file with the old patched app! 198 |

199 | 200 |
201 |

202 | Source code on GitHub: https://github.com/BotoX/xiaomi-m365-firmware-patcher 203 |
204 | Donate: https://paypal.me/BotoXbz 205 |

206 |
207 | 208 | 464 | 465 | 466 | -------------------------------------------------------------------------------- /patcher.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from binascii import hexlify 3 | import struct 4 | import keystone 5 | from xiaotea import XiaoTea 6 | 7 | # https://web.eecs.umich.edu/~prabal/teaching/eecs373-f10/readings/ARMv7-M_ARM.pdf 8 | MOVW_T3_IMM = [*[None]*5, 11, *[None]*6, 15, 14, 13, 12, None, 10, 9, 8, *[None]*4, 7, 6, 5, 4, 3, 2, 1, 0] 9 | MOVS_T1_IMM = [*[None]*8, 7, 6, 5, 4, 3, 2, 1, 0] 10 | 11 | def PatchImm(data, ofs, size, imm, signature): 12 | assert size % 2 == 0, 'size must be power of 2!' 13 | assert len(signature) == size * 8, 'signature must be exactly size * 8 long!' 14 | imm = int.from_bytes(imm, 'little') 15 | sfmt = '<' + 'H' * (size // 2) 16 | 17 | sigs = [signature[i:i + 16][::-1] for i in range(0, len(signature), 16)] 18 | orig = data[ofs:ofs+size] 19 | words = struct.unpack(sfmt, orig) 20 | 21 | patched = [] 22 | for i, word in enumerate(words): 23 | for j in range(16): 24 | imm_bitofs = sigs[i][j] 25 | if imm_bitofs is None: 26 | continue 27 | 28 | imm_mask = 1 << imm_bitofs 29 | word_mask = 1 << j 30 | 31 | if imm & imm_mask: 32 | word |= word_mask 33 | else: 34 | word &= ~word_mask 35 | patched.append(word) 36 | 37 | packed = struct.pack(sfmt, *patched) 38 | data[ofs:ofs+size] = packed 39 | return (orig, packed) 40 | 41 | class SignatureException(Exception): 42 | pass 43 | 44 | def FindPattern(data, signature, mask=None, start=None, maxit=None): 45 | sig_len = len(signature) 46 | if start is None: 47 | start = 0 48 | stop = len(data) - len(signature) 49 | if maxit is not None: 50 | stop = start + maxit 51 | 52 | if mask: 53 | assert sig_len == len(mask), 'mask must be as long as the signature!' 54 | for i in range(sig_len): 55 | signature[i] &= mask[i] 56 | 57 | for i in range(start, stop): 58 | matches = 0 59 | 60 | while signature[matches] is None or signature[matches] == (data[i + matches] & (mask[matches] if mask else 0xFF)): 61 | matches += 1 62 | if matches == sig_len: 63 | return i 64 | 65 | raise SignatureException('Pattern not found!') 66 | 67 | 68 | class FirmwarePatcher(): 69 | def __init__(self, data): 70 | self.data = bytearray(data) 71 | self.ks = keystone.Ks(keystone.KS_ARCH_ARM, keystone.KS_MODE_THUMB) 72 | 73 | def encrypt(self): 74 | cry = XiaoTea() 75 | self.data = cry.encrypt(self.data) 76 | 77 | def kers_min_speed(self, kmh): 78 | val = struct.pack('= 1 and limit <= 130 148 | min = int(min) 149 | assert min >= 0 and min < 65536 150 | max = int(max) 151 | assert max >= min and max < 65536 152 | 153 | sig = [0x73, 0x29, 0x00, 0xDD, 0x73, 0x21, 0x45, 0xF2, 0xF0, 0x53, 0x59, 0x43, 0x73, 0x23, 0x91, 0xFB, 0xF3, 0xF1, None, 0x6C, 0x51, 0x1A, 0xA1, 0xF5, 0xFA, 0x51] 154 | ofs = FindPattern(self.data, sig) 155 | 156 | pre = self.data[ofs:ofs+2] 157 | post = bytes(self.ks.asm('CMP R1, #{:n}'.format(limit))[0]) 158 | self.data[ofs:ofs+2] = post 159 | ret.append((ofs, pre, post)) 160 | ofs += 2 161 | 162 | ofs += 2 163 | pre = self.data[ofs:ofs+2] 164 | post = bytes(self.ks.asm('MOVS R1, #{:n}'.format(limit))[0]) 165 | self.data[ofs:ofs+2] = post 166 | ret.append((ofs, pre, post)) 167 | ofs += 2 168 | 169 | pre = self.data[ofs:ofs+4] 170 | post = bytes(self.ks.asm('MOVW R3, #{:n}'.format(max - min))[0]) 171 | self.data[ofs:ofs+4] = post 172 | ret.append((ofs, pre, post)) 173 | ofs += 4 174 | 175 | ofs += 2 176 | pre = self.data[ofs:ofs+2] 177 | post = bytes(self.ks.asm('MOVS R3, #{:n}'.format(limit))[0]) 178 | self.data[ofs:ofs+2] = post 179 | ret.append((ofs, pre, post)) 180 | ofs += 2 181 | 182 | ofs += 8 183 | pre = self.data[ofs:ofs+4] 184 | post = bytes(self.ks.asm('SUB.W R1, R1, #{:n}'.format(min & 0xFF00))[0]) 185 | self.data[ofs:ofs+4] = post 186 | ret.append((ofs, pre, post)) 187 | 188 | return ret 189 | 190 | def voltage_limit(self, volts): 191 | val = struct.pack('> 6) & 0x1F 398 | 399 | # STR (T1) Rt, [Rn, #imm5] 400 | addr2_ofs2 = (struct.unpack('> 6) & 0x1F 401 | addr2_ofs2 *= 4 # ZeroExtend '00' 402 | 403 | # STRH (T1) Rt, [Rn, #imm5] 404 | addr4_ofs1 = (struct.unpack('> 6) & 0x1F 405 | addr4_ofs1 *= 2 # ZeroExtend '0' 406 | 407 | ret[0]['addrs'] = { 408 | '1': [hex(addr1), hex(addr1 + addr1_ofs1)], 409 | '2': [hex(addr2), hex(addr2 + addr2_ofs1), hex(addr2 + addr2_ofs2)], 410 | '3': [hex(addr3)], 411 | '4': [hex(addr4), hex(addr4 + addr4_ofs1)], 412 | '5': [hex(addr5)] 413 | } 414 | 415 | asm = f''' 416 | LDR R3, ={hex(addr2 + addr2_ofs1)} 417 | LDRB R3, [R3] 418 | CBNZ R3, loc_ret 419 | AND R2, R3, #0xFF 420 | LDR R3, ={hex(addr3)} 421 | LDR R1, [R3] 422 | CMP R1, #0 423 | BLT loc_1 424 | PUSH {{R4, R5}} 425 | LDR R1, ={hex(addr4 + addr4_ofs1)} 426 | LDR R5, ={hex(addr2 + addr2_ofs2)} 427 | MOVS R4, #1 428 | SUBS R0, #0x32 429 | STR R2, [R5] 430 | STRH R4, [R1] 431 | BMI loc_3 432 | LDR R2, ={hex(eco_addr)} 433 | CMP R0, #0x7D 434 | LDRB R2, [R2] 435 | IT GE 436 | MOVGE R0, #0x7D 437 | CMP R2, R4 438 | BEQ loc_2 439 | MOVS R3, #0x96 440 | MUL R3, R3, R0 441 | LDR R2, ={hex(addr5)} 442 | STR R3, [R2] 443 | 444 | loc_popret: 445 | POP {{R4, R5}} 446 | 447 | loc_ret: 448 | BX LR 449 | 450 | loc_1: 451 | LDR R1, ={hex(addr5)} 452 | ADD.W R3, R3, #0x1580 453 | ADDS R3, #0x12 454 | STR R2, [R1] 455 | STRH R2, [R3] 456 | BX LR 457 | 458 | loc_2: 459 | MOVW R4, #0x1AF4 460 | MOVS R2, #0x64 461 | MUL R2, R2, R0 462 | LDR R1, ={hex(addr5)} 463 | STR R2, [R1] 464 | LDR R2, [R3] 465 | CMP R2, R4 466 | BLE loc_popret 467 | LDR R3, [R3] 468 | LDR R2, [R1] 469 | SUB.W R3, R3, #0x1AE0 470 | SUBS R3, #0x14 471 | ADD.W R3, R3, R3, LSL#2 472 | SUB.W R3, R2, R3, LSL#1 473 | STR R3, [R1] 474 | B loc_popret 475 | 476 | loc_3: 477 | LDR R3, ={hex(addr5)} 478 | MVN R2, #0x9 479 | STR R2, [R3] 480 | B loc_popret 481 | ''' 482 | 483 | res = self.ks.asm(asm) 484 | assert len(res[0]) <= len(sig), 'new code larger than old code, this won\'t work' 485 | assert len(res[0]) == 164, 'hardcoded size safety check, if you haven\'t changed the ASM then something is wrong' 486 | 487 | # pad with zero for no apparent reason 488 | padded = bytes(res[0]).ljust(len(sig), b'\x00') 489 | 490 | ret[0]['len_sig'] = len(sig) 491 | ret[0]['len_res'] = len(res[0]) 492 | ret[0]['res_inst'] = res[1] 493 | 494 | self.data[ofs:ofs+len(padded)] = bytes(padded) 495 | 496 | # additional russian change 497 | sig = [0x07, 0xD0, 0x0B, 0xE0, 0x00, 0xEB, 0x40, 0x00, 0x40, 0x00, 0x05, 0xE0] 498 | mask= [0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF] 499 | ofs = FindPattern(self.data, sig, mask) + 8 500 | pre = self.data[ofs:ofs+2] 501 | post = bytes(self.ks.asm('NOP')[0]) 502 | self.data[ofs:ofs+2] = post 503 | ret.append((ofs, pre, post)) 504 | 505 | return ret 506 | 507 | 508 | def eprint(*args, **kwargs): 509 | print(*args, file=sys.stderr, **kwargs) 510 | 511 | if __name__ == "__main__": 512 | import sys 513 | if len(sys.argv) != 3: 514 | eprint("Usage: {0} ".format(sys.argv[0])) 515 | exit(1) 516 | 517 | with open(sys.argv[1], 'rb') as fp: 518 | data = fp.read() 519 | 520 | cfw = FirmwarePatcher(data) 521 | 522 | cfw.kers_min_speed(45) 523 | cfw.speed_params(31, 50000, 30000, 26, 40000, 20000) 524 | cfw.brake_params(115, 8000, 50000) 525 | cfw.voltage_limit(52) 526 | cfw.motor_start_speed(3) 527 | cfw.instant_eco_switch() 528 | #cfw.boot_with_eco() 529 | #cfw.cruise_control_delay(5) 530 | #cfw.cruise_control_nobeep() 531 | cfw.remove_hard_speed_limit() 532 | #cfw.remove_charging_mode() 533 | #cfw.stay_on_locked() 534 | #cfw.bms_uart_76800() 535 | #cfw.russian_throttle() 536 | #cfw.wheel_speed_const(315) 537 | 538 | # Don't flash encrypted firmware to scooter running firmware < 1.4.1 539 | #cfw.encrypt() 540 | 541 | with open(sys.argv[2], 'wb') as fp: 542 | fp.write(cfw.data) 543 | --------------------------------------------------------------------------------