├── README.md ├── LICENSE ├── BafangWebCfg.html └── app.js /README.md: -------------------------------------------------------------------------------- 1 | # BafangWebConfig 2 | Cross-platform browser based configuration tool for Bafang BBSxx e-bike motors, runs on Mac, Linux or Windows. It requires a browser with support for Web Serial API, and should work on any recent version of Chrome, Opera or Edge. 3 | 4 | ## Run online 5 | 6 | [Click here](https://devnotes.kymatica.com/BafangWebConfig/BafangWebCfg.html) to start the tool. 7 | 8 | ## Install & run locally 9 | 10 | Download or clone [the repository](https://github.com/lijon/BafangWebConfig) and open BafangWebCfg.html in your browser. 11 | 12 | ## Notes 13 | 14 | USE AT YOUR OWN RISK! Bad settings can fry your motor controller. 15 | 16 | If you find any bugs, please [report them here](https://github.com/lijon/BafangWebConfig/issues). 17 | 18 | Thanks to Stefan Penov (https://penoff.me/2016/01/13/e-bike-conversion-software/) 19 | and Philipp Sandhaus (https://github.com/philippsandhaus/bafang-python) for their 20 | prior work on deciphering the original Bafang Config Tool source code and 21 | communication protocol. 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jonatan Liljedahl 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /BafangWebCfg.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Bafang Web Config 6 | 145 | 146 | 147 |

Bafang BBSxx Configuration

148 |
149 | 150 | 151 | 152 |
153 |
154 |
155 | 158 |
159 |
160 | 163 |
164 |
165 |
166 | 167 |
168 | 169 | 170 |
171 |

Info

172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 |
Manufacturer
Model
HW Version
FW Version
Voltage
Max Current (A)
183 |
184 | 185 |
186 |

Basic

187 | 188 |
189 | 190 | 191 |
192 | 193 | 194 | 195 | 196 | 197 | 198 | 216 | 221 | 222 | 223 | 224 | 225 | 226 |
Low Battery Protect (V)
Current Limit (A)
Wheel Diameter
Speedmeter Model
Speedmeter Signals
AssistCurrent (%)Speed (%)
227 | 228 |
229 | 230 |
231 |

Pedal Assist

232 | 233 |
234 | 235 | 236 |
237 | 238 | 239 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 254 | 255 | 256 | 257 | 258 | 259 |
Pedal Type
Designated Assist (0-9)
Speed Limit (km/h)
Start Current (%)
Slow-Start Mode (1-8)
Startup Degree (Signal No.)
Work Mode (pedal/wheel*10)
Time of Stop (x 10 ms)
Current Decay (1-8)
Stop Decay (x 10 ms)
Keep Current (%)
260 |
261 | 262 | 264 | 265 |
266 |

Throttle

267 | 268 |
269 | 270 | 271 |
272 | 273 | 274 | 275 | 276 | 280 | 281 | 282 | 283 | 284 |
Start Voltage (x 100 mV)
End Voltage (x 100 mV)
Mode
Designated Assist (0-9)
Speed Limit (km/h)
Start Current (%)
285 |
286 | 287 |

288 | 
289 |   
By Jonatan Liljedahl - www.kymatica.com
290 | 291 | 292 | 627 | 628 | 629 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const BAUD_RATE = 1200; 3 | // const BAUD_RATE = 9600; 4 | const BLK_GEN = 0x51; 5 | const BLK_BAS = 0x52; 6 | const BLK_PAS = 0x53; 7 | const BLK_THR = 0x54; 8 | const CMD_READ = 0x11; 9 | const CMD_WRITE = 0x16; 10 | const blockNumBytes = { 11 | [BLK_GEN]: 19, 12 | [BLK_BAS]: 27, 13 | [BLK_PAS]: 14, 14 | [BLK_THR]: 9, 15 | }; 16 | const blockKeys = { 17 | [BLK_GEN]: "info", 18 | [BLK_BAS]: "basic", 19 | [BLK_PAS]: "pedal", 20 | [BLK_THR]: "throttle", 21 | }; 22 | 23 | class BafangConfig { 24 | constructor() { 25 | this.byteCount = 0; 26 | this.buffer = null; 27 | this.lastCmd = 0; 28 | this.data = {}; 29 | this.resultBlock = ""; 30 | this.resultKey = ""; 31 | this.readWriteAll = false; 32 | } 33 | 34 | // callbacks 35 | onRead = function(blk) {}; 36 | onWrite = function(blk,ok) {}; 37 | onSerialConnect = function(port) {}; 38 | 39 | parseGenData(buf) { 40 | let voltage = buf[16] > 4 ? "24V-60V" : ["24V","36V","48V","60V","24V-48V"][buf[16]]; 41 | return { 42 | manufacturer: String.fromCharCode.apply(null, buf.slice(2,2+4)), 43 | model: String.fromCharCode.apply(null, buf.slice(6,6+4)), 44 | hw_ver: String.fromCharCode.apply(null, buf.slice(10,12)).split("").join("."), 45 | fw_ver: String.fromCharCode.apply(null, buf.slice(12,16)).split("").join("."), 46 | voltage: voltage, 47 | max_current: buf[17], 48 | }; 49 | } 50 | speedModels = ["External","Internal","Motorphase","Unknown"]; 51 | parseBasData(buf) { 52 | let smm = buf[25] >> 6; 53 | if(smm==3) smm=2; // According to original code! 54 | let data = { 55 | low_battery_protect: buf[2], 56 | current_limit: buf[3], 57 | wheel_size: buf[24]==0x37 ? "700C" : (Math.ceil(buf[24]/2) + '"'), 58 | speedmeter_model: this.speedModels[smm], 59 | speedmeter_signals: buf[25] & 63, 60 | }; 61 | for(let i=0;i<10;i++) { 62 | data["assist"+i+"_current"] = buf[4+i]; 63 | data["assist"+i+"_speed"] = buf[14+i]; 64 | } 65 | return data; 66 | } 67 | makeBasData(data) { 68 | let buf = [ 69 | data["low_battery_protect"], 70 | data["current_limit"], 71 | ]; 72 | for(let i=0;i<10;i++) 73 | buf.push(data["assist"+i+"_current"]); 74 | for(let i=0;i<10;i++) 75 | buf.push(data["assist"+i+"_speed"]); 76 | buf.push(data["wheel_size"] == "700C" ? 0x37 : parseInt(data["wheel_size"])*2); 77 | let spd_sigs = parseInt(data["speedmeter_signals"]) & 63; 78 | let spd_model = Math.max(0,this.speedModels.indexOf(data["speedmeter_model"])); 79 | if(spd_model == 2) spd_model = 3; // According to original code! 80 | buf.push(spd_model << 6 | spd_sigs); 81 | return buf; 82 | } 83 | pedalTypes = ["None","DH-Sensor-12","BB-Sensor-32","DoubleSignal-24"]; 84 | parsePasData(buf) { 85 | return { 86 | pedal_type: this.pedalTypes[buf[2]], 87 | designated_assist: buf[3]==0xff?"Display":buf[3], 88 | speed_limit: buf[4]==0xff?"Display":buf[4], 89 | start_current: buf[5], 90 | slow_start_mode: buf[6], 91 | startup_degree: buf[7], 92 | work_mode: buf[8]==0xff?"Undetermined":buf[8], 93 | time_of_stop: buf[9], 94 | current_decay: buf[10], 95 | stop_decay: buf[11], 96 | keep_current: buf[12], 97 | }; 98 | } 99 | makePasData(data) { 100 | return [ 101 | Math.max(0,this.pedalTypes.indexOf(data["pedal_type"])), 102 | data["designated_assist"] == "Display" ? 0xff : data["designated_assist"], 103 | data["speed_limit"] == "Display" ? 0xff : data["speed_limit"], 104 | data["start_current"], 105 | data["slow_start_mode"], 106 | data["startup_degree"], 107 | data["work_mode"] == "Undetermined" ? 0xff : data["work_mode"], 108 | data["time_of_stop"], 109 | data["current_decay"], 110 | data["stop_decay"], 111 | data["keep_current"] 112 | ]; 113 | } 114 | parseThrData(buf) { 115 | return { 116 | start_voltage: buf[2], 117 | end_voltage: buf[3], 118 | mode: ["Speed","Current"][buf[4]], 119 | designated_assist: buf[5]==0xff?"Display":buf[5], 120 | speed_limit: buf[6]==0xff?"Display":buf[6], 121 | start_current: buf[7], 122 | }; 123 | } 124 | makeThrData(data) { 125 | return [ 126 | data["start_voltage"], 127 | data["end_voltage"], 128 | data["mode"]=="Speed"?0:1, 129 | data["designated_assist"]=="Display"?0xff:data["designated_assist"], 130 | data["speed_limit"]=="Display"?0xff:data["speed_limit"], 131 | data["start_current"], 132 | ]; 133 | } 134 | parseData(buf) { 135 | const blk = buf[0]; 136 | console.log("Reading block:",blockKeys[blk]); 137 | switch(blk) { 138 | case BLK_GEN: return this.parseGenData(buf); 139 | case BLK_BAS: return this.parseBasData(buf); 140 | case BLK_PAS: return this.parsePasData(buf); 141 | case BLK_THR: return this.parseThrData(buf); 142 | } 143 | console.log("parseData: Unknown block",blk); 144 | return null; 145 | } 146 | 147 | parseBasCode(code) { 148 | let lvl=0; 149 | switch(code) { 150 | case 0: return ['Basic: Low Battery Protection out of range!', "low_battery_protect"]; 151 | break; 152 | case 1: return ['Basic: Current Limit out of range!', "current_limit"]; 153 | break; 154 | case 2: //0 155 | case 4: //1 156 | case 6: //2 157 | case 8: //3 158 | case 10: //4 159 | case 12: //5 160 | case 14: //6 161 | case 16: //7 162 | case 18: //8 163 | case 20: //9 164 | lvl = (code-2)/2; 165 | return ['Basic: Current Limit for Assist '+lvl+' out of range!',"assist"+lvl+"_current"]; 166 | break; 167 | case 3: 168 | case 5: 169 | case 7: 170 | case 9: 171 | case 11: 172 | case 13: 173 | case 15: 174 | case 17: 175 | case 19: 176 | case 21: 177 | lvl = (code-3)/2; 178 | return ['Basic: Speed Limit for Assist '+lvl+' out of range!', "assist"+lvl+"_speed"]; 179 | break; 180 | case 22: return ['Basic: Wheel Diameter out of range!', "wheel_size"]; 181 | break; 182 | case 23: return ['Basic: Speed Meter Signals out of range!', "speedmeter_signals"]; 183 | break; 184 | case 24: return null; 185 | } 186 | return ['Unknown result code: '+code,null]; 187 | } 188 | parsePasCode(code) { 189 | switch(code) { 190 | case 0: return ['Pedal: Pedal Sensor Type error!', "pedal_type"]; 191 | break; 192 | case 1: return ['Pedal: Designated Assist Level error!', "designated_assist"]; 193 | break; 194 | case 2: return ['Pedal: Speed Limit error!', "speed_limit"]; 195 | break; 196 | case 3: return ['Pedal: Current out of range!', "current_limit"]; 197 | break; 198 | case 4: return ['Pedal: Slow-start Mode error!', "slow_start_mode"]; 199 | break; 200 | case 5: return ['Pedal: Start Degree out of range!', "startup_degree"]; 201 | break; 202 | case 6: return ['Pedal: Work Mode error!', "work_mode"]; 203 | break; 204 | case 7: return ['Pedal: Time of Stop out of range!', "time_of_stop"]; 205 | break; 206 | case 8: return ['Pedal: Current Decay out of range!', "current_decay"]; 207 | break; 208 | case 9: return ['Pedal: Stop Decay out of range!', "stop_decay"]; 209 | break; 210 | case 10: return ['Pedal: Keep Current out of range!', "keep_current"]; 211 | break; 212 | case 11: return null; 213 | } 214 | return ['Unknown result code: '+code,null]; 215 | } 216 | parseThrCode(code) { 217 | switch(code) { 218 | case 0: return ['Throttle: Start Voltage out of range!', "start_voltage"]; 219 | break; 220 | case 1: return ['Throttle: End Voltage out of range!', "end_voltage"]; 221 | break; 222 | case 2: return ['Throttle: Mode error!', "mode"]; 223 | break; 224 | case 3: return ['Throttle: Designated Assist error!', "designated_assist"]; 225 | break; 226 | case 4: return ['Throttle: Speed Limit error!', "speed_limit"]; 227 | break; 228 | case 5: return ['Throttle: Start Current out of range!', "start_current"]; 229 | break; 230 | case 6: return null; 231 | } 232 | return ['Unknown result code: '+code,null]; 233 | } 234 | parseResultCode(buf) { 235 | const blk = buf[0]; 236 | const code = buf[1]; 237 | console.log("Write result for",blockKeys[blk],"=",code); 238 | this.resultBlock = blockKeys[blk]; 239 | switch(blk) { 240 | case BLK_BAS: return this.parseBasCode(code); 241 | case BLK_PAS: return this.parsePasCode(code); 242 | case BLK_THR: return this.parseThrCode(code); 243 | } 244 | console.log("parseResultCode: Unknown block",blk); 245 | return null; 246 | } 247 | prepareWriteData(blk, buf) { 248 | let data = [CMD_WRITE, blk, buf.length]; 249 | data = data.concat(buf); 250 | this.addVerification(data); 251 | console.log("Prepare to write:",data); 252 | return data; 253 | } 254 | bytesForBlock(blk) { 255 | let key = blockKeys[blk]; 256 | let buf = null; 257 | switch(blk) { 258 | case BLK_BAS: buf = this.makeBasData(this.data[key]); 259 | break; 260 | case BLK_PAS: buf = this.makePasData(this.data[key]); 261 | break; 262 | case BLK_THR: buf = this.makeThrData(this.data[key]); 263 | break; 264 | default: 265 | console.log("bytesForBlock: Unknown block",blk); 266 | return null; 267 | } 268 | return buf.map((e)=>{return parseInt(e)}); 269 | } 270 | async writeBlock(blk) { 271 | if(this.parseTable(blk)) { 272 | this.logMsg(blk,"Writing..."); 273 | let data = this.bytesForBlock(blk); 274 | data = this.prepareWriteData(blk, data); 275 | this.expectBytes(2, CMD_WRITE); 276 | return this.write(data); 277 | } 278 | } 279 | writeAllBlocks() { 280 | this.clearMessages(); 281 | this.readWriteAll = true; 282 | this.writeBlock(BLK_BAS); 283 | } 284 | 285 | processResponse(buf) { 286 | const blk = buf[0]; 287 | const key = blockKeys[blk]; 288 | if(!key) { 289 | this.logError(BLK_GEN, "Unexpected block in response, ignoring"); 290 | } else if(this.lastCmd == CMD_READ) { 291 | this.data[key] = this.parseData(buf); 292 | this.onRead(blk); 293 | this.logMsg(blk, "Read successful"); 294 | if(this.readWriteAll && blk < BLK_THR) 295 | this.readBlock(blk+1); 296 | if(blk == BLK_THR) 297 | this.readWriteAll = false; 298 | // Verify that our byte generation code works. 299 | // Note this can fail on wheelsize since two values equals the same size.. 300 | /*let org = buf.slice(2,-1); 301 | let xxx = this.bytesForBlock(blk); 302 | if(xxx.length === org.length && org.every((v,i) => v===xxx[i])) { 303 | console.log("Internal byte generation check successful"); 304 | } else { 305 | console.log("Internal byte generation check failed!"); 306 | console.log("READ",org); 307 | console.log("WRITE",xxx); 308 | }*/ 309 | } else if(this.lastCmd == CMD_WRITE) { 310 | const res = this.parseResultCode(buf); 311 | if(!res) { 312 | this.logMsg(blk, "Write successful"); 313 | this.onWrite(blk, null); 314 | if(this.readWriteAll && blk < BLK_THR) 315 | this.writeBlock(blk+1); 316 | if(blk == BLK_THR) 317 | this.readWriteAll = false; 318 | } else { 319 | this.logError(blk, res[0]); 320 | this.onWrite(blk, res[1]); 321 | } 322 | } 323 | } 324 | async listen() { 325 | while (this.port.readable) { 326 | const reader = this.port.readable.getReader(); 327 | this.reader = reader; 328 | try { 329 | while (true) { 330 | const { value, done } = await reader.read(); 331 | if (done) { 332 | reader.releaseLock(); 333 | console.log("DONE"); 334 | return; 335 | } 336 | console.log("read "+value); 337 | 338 | /* 339 | // DUMMY TEST WITH LOOP BACK DEVICE 340 | if(this.byteCount > 0 && this.buffer) { 341 | this.byteCount = 0; 342 | // GEN 343 | //this.buffer = [0x51,0x10,0x48,0x5A,0x58,0x54,0x53,0x5A,0x5A,0x36,0x32,0x32,0x32,0x30,0x31,0x31,0x01,0x14,0x1B]; 344 | // BAS 345 | if(this.lastCmd == CMD_READ) 346 | this.buffer = [0x52, 0x18, 0x1F, 0x0F, 0x00, 0x1C, 0x25, 0x2E, 0x37, 0x40, 0x49, 0x52, 0x5B, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x35, 0x42, 0xDF]; 347 | else 348 | this.buffer = [0x52, 50]; 349 | // PAS 350 | // this.buffer = [0x53, 0x0B, 0x03, 0xFF, 0xFF, 0x64, 0x06, 0x14, 0x0A, 0x19, 0x08, 0x14, 0x14, 0x27]; 351 | // THR 352 | // this.buffer = [0x54, 0x06, 0x0B, 0x23, 0x00, 0x03, 0x11, 0x14, 0xAC]; 353 | 354 | this.processResponse(this.buffer); 355 | } 356 | */ 357 | 358 | for (const a of value) { 359 | if(this.byteCount > 0 && this.buffer) { 360 | this.byteCount--; 361 | console.log("pushing 0x"+a.toString(16)+", "+this.byteCount+" bytes left"); 362 | this.buffer.push(a); 363 | if(this.byteCount == 0) { 364 | console.log("Got all "+this.buffer.length+" bytes"); 365 | this.processResponse(this.buffer); 366 | } 367 | } else { 368 | console.log("ignoring byte: 0x"+a.toString(16)); 369 | } 370 | } 371 | } 372 | } catch (error) { 373 | console.log(error); 374 | reader.releaseLock(); 375 | this.writer.releaseLock(); 376 | this.port.close(); 377 | this.onSerialConnect(false); 378 | } 379 | } 380 | } 381 | logError(blk, ...msg) { 382 | const key = blockKeys[blk]; 383 | const node = document.querySelector('#'+key+'.error-display'); 384 | node.style.color = "red"; 385 | node.innerText = msg.join(' '); 386 | console.error(key,"ERROR:",msg.join(' ')); 387 | } 388 | logMsg(blk, ...msg) { 389 | const key = blockKeys[blk]; 390 | const node = document.querySelector('#'+key+'.error-display'); 391 | node.style.color = "green"; 392 | node.innerText = msg.join(' '); 393 | console.log(key,"LOG:",msg.join(' ')); 394 | } 395 | async close() { 396 | await this.reader.cancel(); 397 | await this.writer.releaseLock(); 398 | await this.port.close(); 399 | this.onSerialConnect(false); 400 | } 401 | async init() { 402 | if ('serial' in navigator) { 403 | try { 404 | this.port = await navigator.serial.requestPort(); 405 | console.log(this.port); 406 | await this.port.open({ baudRate: BAUD_RATE }); 407 | // this.reader = port.readable.getReader(); 408 | this.writer = this.port.writable.getWriter(); 409 | /*let signals = await port.getSignals();*/ 410 | 411 | this.listen(); 412 | this.onSerialConnect(true); 413 | this.connectDevice(); 414 | } 415 | catch (err) { 416 | console.log(err); 417 | if(err.name != "NotFoundError") 418 | this.logError(BLK_GEN,'Could not open serial port:',err); 419 | } 420 | } 421 | else { 422 | this.logError(BLK_GEN, "Web Serial disabled or not supported by your browser. Try a recent version of Chrome, Opera or Edge.") 423 | } 424 | } 425 | async write(data) { 426 | return await this.writer.write(Uint8Array.from(data)); 427 | } 428 | verificationByte(data) { 429 | let x = data.slice(1).reduce((tot,val) => { 430 | return tot + val; 431 | }); 432 | return (x % 256); 433 | } 434 | addVerification(data) { 435 | data.push(this.verificationByte(data)); 436 | } 437 | expectBytes(len, cmd) { 438 | console.log("expecting "+len+" bytes..."); 439 | if(this.byteCount > 0) 440 | console.warn("Previous read not finished"); 441 | this.lastCmd = cmd; 442 | this.byteCount = len; 443 | this.buffer = new Array(); 444 | } 445 | async connectDevice() { 446 | let data = [CMD_READ, BLK_GEN, 4, 0xb0]; 447 | this.addVerification(data); 448 | // console.log(data); 449 | this.expectBytes(blockNumBytes[BLK_GEN], CMD_READ); 450 | // await new Promise(r => setTimeout(r, 3000)); // test 451 | return this.write(data); 452 | } 453 | async readBlock(blk) { 454 | let data = [CMD_READ, blk]; 455 | this.logMsg(blk,"Reading..."); 456 | this.expectBytes(blockNumBytes[blk], CMD_READ); 457 | return this.write(data); 458 | } 459 | clearMessages() { 460 | let nodes = document.querySelectorAll('.error-display'); 461 | for(let e of nodes) 462 | e.innerText = ""; 463 | } 464 | readAllBlocks() { 465 | this.clearMessages(); 466 | this.readWriteAll = true; 467 | this.readBlock(BLK_BAS); 468 | } 469 | parseINIString(data) { 470 | const regex = { 471 | section: /^\s*\[\s*([^\]]*)\s*\]\s*$/, 472 | param: /^\s*([^=]+?)\s*=\s*(.*?)\s*$/, 473 | comment: /^\s*;.*$/ 474 | }; 475 | var value = {}; 476 | const lines = data.split(/[\r\n]+/); 477 | var section = null; 478 | lines.forEach(function(line) { 479 | if(regex.comment.test(line)) { 480 | return; 481 | } else if(regex.param.test(line)) { 482 | const match = line.match(regex.param); 483 | if(section) { 484 | value[section][match[1]] = parseInt(match[2]); 485 | } else { 486 | value[match[1]] = parseInt(match[2]); 487 | } 488 | } else if(regex.section.test(line)) { 489 | const match = line.match(regex.section); 490 | value[match[1]] = {}; 491 | section = match[1]; 492 | } else if(line.length == 0 && section) { 493 | section = null; 494 | }; 495 | }); 496 | return value; 497 | } 498 | convertIni(txt) { 499 | const ini = this.parseINIString(txt); 500 | const bas = ini["Basic"]; 501 | const pas = ini["Pedal Assist"]; 502 | const thr = ini["Throttle Handle"]; 503 | let data = { 504 | "basic": { 505 | low_battery_protect: bas["LBP"], 506 | current_limit: bas["LC"], 507 | speedmeter_model: this.speedModels[bas["SMM"]], 508 | speedmeter_signals: bas["SMS"], 509 | }, 510 | "pedal": { 511 | pedal_type: this.pedalTypes[pas["PT"]], 512 | designated_assist: pas["DA"]==0?"Display":(pas["DA"]-1), 513 | speed_limit: pas["SL"]==0?"Display":(pas["SL"]+14), 514 | start_current: pas["SC"], 515 | slow_start_mode: pas["SSM"]+1, 516 | startup_degree: pas["SDN"], 517 | work_mode: pas["WM"]==0?"Undetermined":(pas["WM"]+9), 518 | time_of_stop: pas["TS"], 519 | current_decay: pas["CD"], 520 | stop_decay: pas["SD"], 521 | keep_current: pas["KC"], 522 | }, 523 | "throttle": { 524 | start_voltage: thr["SV"], 525 | end_voltage: thr["EV"], 526 | mode: ["Speed","Current"][thr["MODE"]], 527 | designated_assist: thr["DA"]==0?"Display":(thr["DA"]-1), 528 | speed_limit: thr["SL"]==0?"Display":(thr["SL"]+14), 529 | start_current: thr["SC"], 530 | } 531 | }; 532 | for(let i=0;i<10;i++) { 533 | data["basic"]["assist"+i+"_current"] = bas["ALC"+i]; 534 | data["basic"]["assist"+i+"_speed"] = bas["ALBP"+i]; 535 | } 536 | let whl = bas["WD"]; 537 | if(whl==12) { 538 | whl = "700C"; 539 | } else if(whl>12) { 540 | whl = (whl+15)+'"'; 541 | } else { 542 | whl = (whl+16)+'"'; 543 | } 544 | data["basic"]["wheel_size"] = whl; 545 | return data; 546 | } 547 | readFile(f) { 548 | this.clearMessages(); 549 | let fr = new FileReader(); 550 | let ext = f.name.split('.').pop().toUpperCase(); 551 | fr.onload = (e) => { 552 | let oldInfo = this.data["info"]; 553 | let txt = e.target.result; 554 | if(ext == "EL") { 555 | this.data = this.convertIni(txt); 556 | } else { 557 | this.data = JSON.parse(txt); 558 | } 559 | if(oldInfo) this.data["info"] = oldInfo; 560 | // convert number strings to numbers 561 | for (let blk in this.data) { 562 | for (let key in this.data[blk]) { 563 | let val = this.data[blk][key]; 564 | let num = parseInt(val); 565 | if(num==val) this.data[blk][key] = num; 566 | } 567 | } 568 | for (let blk of [BLK_BAS, BLK_PAS, BLK_THR]) { 569 | this.onRead(blk); 570 | } 571 | }; 572 | 573 | fr.readAsText(f); 574 | } 575 | timestamp() { 576 | let d = new Date(); 577 | return d.getFullYear() + "-" 578 | + ("0"+(d.getMonth()+1)).slice(-2) + "-" 579 | + ("0" + d.getDate()).slice(-2) + "_" 580 | + ("0" + d.getHours()).slice(-2) + "-" 581 | + ("0" + d.getMinutes()).slice(-2) + "-" 582 | + ("0" + d.getSeconds()).slice(-2); 583 | } 584 | saveFile() { 585 | for (let blk of [BLK_BAS, BLK_PAS, BLK_THR]) 586 | if(!this.parseTable(blk)) 587 | return; 588 | //console.log(JSON.stringify(this.data, null, 2)); 589 | const a = document.createElement("a"); 590 | a.href = URL.createObjectURL(new Blob([JSON.stringify(this.data, null, 2)], { 591 | type: "application/json" 592 | })); 593 | let now = new Date(); 594 | a.setAttribute("download", "bafang_profile_"+this.timestamp()+".json"); // TODO append date 595 | //a.setAttribute("target", "_blank"); // this had no SaveAs enabled in chrome 596 | document.body.appendChild(a); 597 | a.click(); 598 | document.body.removeChild(a); 599 | } 600 | } 601 | --------------------------------------------------------------------------------