├── 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 | Open serial port
150 | Close serial port
151 |
152 |
153 |
154 |
155 | Load File:
156 |
157 |
158 |
159 |
160 | Save File:
161 | Download
162 |
163 |
164 |
165 |
166 |
167 |
168 | Read All
169 | Write All
170 |
171 |
Info
172 |
173 |
174 |
175 | Manufacturer
176 | Model
177 | HW Version
178 | FW Version
179 | Voltage
180 | Max Current (A)
181 |
182 |
183 |
184 |
185 |
186 |
Basic
187 |
188 |
189 | Read
190 | Write
191 |
192 |
193 |
227 |
228 |
229 |
230 |
231 |
Pedal Assist
232 |
233 |
234 | Read
235 | Write
236 |
237 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
Throttle
267 |
268 |
269 | Read
270 | Write
271 |
272 |
285 |
286 |
287 |
288 |
289 |
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 |
--------------------------------------------------------------------------------