S-LCD to S12S controller communication protocol hacked

flangefrog said:
Also what is L1?
lcd8 manual:
"L Parameter Setting
1. L1 parameters are applicable to the automatic under-voltage controller. The
default of the factory is 0.
1.1 When L1=0, the automatic under-voltage controller can automatically select
the under-voltage value according to the battery voltage.
1.2 When L1=1, the under-voltage value of the automatic under-voltage controller
is forced to be 20V.
1.3 When L1 = 2, the under-voltage value of the automatic under-voltage
controller is forced to be 30V.
1.4 When L1 = 3, the under-voltage value of the automatic under-voltage
controller is forced to be 40V."
My LCD does not allow editing L2 ... L4. Perhaps they are also in the protocol.

Does anyone know which bit is responsible for the reverse of the motor (for 2-speed wheel motors)?
 
casainho said:
Can you please please help me to understand:

"B3,B4: speed, wheel rotation period, ms; period(ms)=B3*256+B4;"
"P1 = motor gear reduction ratio×number of rotor magnet pieces, just rounding if there’s any decimal."

Considering: 1 eRPM at each 2 rotor magnets;

B3B4 = P1 * (eRPM / 2) ??

casainho, were you every able to figure this one out?
 
This is a good thread. I have two KT-LCD5 displays and each has a version number printed in the back. For the "V1.0 8H" model, obelix's original documentation worked. For the "V1.0 7J" model, byte B11 = 0x1e (not 0x32), and the checksum B5 is computed by XORing with 0x0b (not 2).

Thanks obelix! :bigthumb:
 
ceylonaire: I believe you need rpm in the denominator, because B3B4 is the period in ms.
Code:
rpm = 2 * eRPM / P1
B3B4 = 60000.0 / rpm

This C function works on a PC to make the LCD display the speed corresponding to a specific wheel RPM (send_buffer is like Bx):
Code:
int generate_message(unsigned char* send_buffer, double rpm) {
    send_buffer[0] = 0x41;
    send_buffer[1] = 0x10;
    send_buffer[2] = 36; //battery voltage
    
    /* speed, wheel rotation period, ms; period(ms)=B3*256+B4; */
    double period_ms = 60000.0/rpm;
    send_buffer[3] = (int)(period_ms/256);
    send_buffer[4] = period_ms - send_buffer[3]*256;
    
    // error code
    send_buffer[5] = 0x0;
    
    send_buffer[6] = 0x0;
    
    // show throttle
    send_buffer[7] = 0x2;
    
    send_buffer[8] = 13;
    send_buffer[9] = 0x0;
    send_buffer[10] = 0x0;
    send_buffer[11] = 0x0;
    
    /* CRC.  Byte 6 is the xor of all other bytes. */
    unsigned crc = 0;
    
    for(unsigned i=1; i < 12; i++)
        crc ^= send_buffer[i];
    
    send_buffer[6] = crc;
    return 0;
}
 
obelix662000 said:
flangefrog said:
Great work. When were the packets sent between the devices? Were they sent every 100ms or so, or for lcd to controller communication were they just sent when a button was pressed? Any special packets sent on start-up?
Continiously, about 10 times a second in both directions. Actually this is not important, everything works fine with any rate since LCD an S12s rememeber last state.

Why do you say it is not important? If I set the P and C parameters of the kt controller with the display, and after that I disconect the display, but I bypass the V+ and blue wire (control wire) so the Kunteng controller keeps working. Does the Kunteng keeps the setting I did before?

Also if after that I disconnect the battery to charge it and plug it back again for a new ride, does the kunteng keeps my first seting?

I ask this because I dont want to have the display at my handlebar or any other place, so if I can do what I asked you before it woudl be awesome.

Dont know where I read something that if you unplug the display then the controller turn to a low power mode even if you bypass the red and blue wires.
 
Arduino simulation for the LCD1 display of the S06P controller
I will use the S06P controller for my Maxun e-kit, see:
https://www.avdweb.nl/solar-bike/electronics/bluetooth-wattmeter-extension-board-for-ebike-motor-controller

Does anyone has an Arduino sketch that simulates the LCD1 (or LCD3 etc) display?
 
avandalen said:
Arduino simulation for the LCD1 display of the S06P controller
I will use the S06P controller for my Maxun e-kit, see:
https://www.avdweb.nl/solar-bike/electronics/bluetooth-wattmeter-extension-board-for-ebike-motor-controller

Does anyone has an Arduino sketch that simulates the LCD1 (or LCD3 etc) display?
Maybe can be good for you to invest on our EasyDIY OpenSource EBike display, that currently supports TSDZ2 (UART) and Bafang M500/M600 (CAN BUS) mid drive motors. This display implements Bluetooth and ANT+LEV Ebike wireless standard and integrates well with Garmin GPS cycling computers displays or Garmin watches, so you can see the EBike data on your watch for instance.

The idea is that if you want a colorful, big display, with map navigation, with user heart rate sensors connection as also other sensors like electronic gears, you use the Garmin GPS cycling displays - that is an extra to this display and communicates with it using ANT +LEV Ebike wireless standard.

See more here: https://opensourceebike.github.io/

Bafang M500/M600:
04.png


TSDZ2:
display-1.jpg
 
avandalen said:
Does anyone has an Arduino sketch that simulates the LCD1 (or LCD3 etc) display?

The protocol for exchanging data for the display itself has been documented here, but the Kunteng displays program the controller also. That hasn't been decoded. And I don't see a reason why anyone would want to do that. It's basically reinventing the wheel.
 
Hi casainho
>>>> Maybe can be good for you to invest on our EasyDIY OpenSource EBike display, that currently supports TSDZ2 (UART)

Unfortunately, my lightweight TSDZ2 requires an external motor controller: the S06P. But your OpenSource EBike display has so many features … It would be a good idea when I make my e-kit communication compatible with the OpenSource EBike display.
 
avandalen said:
Hi casainho
>>>> Maybe can be good for you to invest on our EasyDIY OpenSource EBike display, that currently supports TSDZ2 (UART)

Unfortunately, my lightweight TSDZ2 requires an external motor controller: the S06P. But your OpenSource EBike display has so many features … It would be a good idea when I make my e-kit communication compatible with the OpenSource EBike display.
An OpenSource display like this one represents freedom, you do not have to reinvent the wheel so you can quickly have your own unique display. Other option as using a commercial display as that Kuntengs, means you are very limited and dependent on the manufacturer, that can change it or stop selling it at anytime.
 
Hi guys! I'm currently working on arduino replacement of lcd3 display and writing firmware for diy color display (use 240x240 st7789 cheap display and esp8266). Currently i need to find bit which corresponding with L2 parameter. Perhaps somebody can send me hex strings with L2 = 0 and L2 =1. L1 and L3 i find in https://github.com/romelec/KT_LCD8_override/blob/master/doc/LCD8_to_S12SN-1.txt manual.

Also i dissassemble ktsmartbicycle app and write js code for generating hex from parameters but in result i got something different
For example bit 12 doesn't exist and other differences. Perhaps it use old protocol. WARNING!!! Chineese code is insane!
Code:
var dimension = 20;
var maxspeed = 60 - 10;
var P1 = 86;
var P2 = 0;
var P3 = 1;
var P4 = 0;
var P5 = 1;
var C1 = 6;
var C2 = 0;
var C3 = 5;
var C4 = 4;
var C5 = 4;
var C7 = 1;
var C12 = 2;
var C13 = 1;
var C14 = 2;
var percent = 50;
var hb_speed = 50; //handle bar max speed
var cadence = 12;            


var p2 = "0x" + (P2.toString(16)).toUpperCase();
        var p3 = "0x" + (P3.toString(16)).toUpperCase();
        var p4 = "0x" + (P4.toString(16)).toUpperCase();           
       

arrayData = new Array(13    );
    function b2b4(dimension, p2, p3, p4, maxspeed) {
        
        var dimension;
        var _dimension;
        var dimensionHex;
        var code;
        if (dimension < 10) {
            code = 0;
            dimensionHex = "0x" + (dimension.toString(16)).toUpperCase();
            dimension = dimensionHex;
        }
        else {
            code = 1;
            _dimension = dimension - 10;
            dimensionHex = "0x" + (_dimension.toString(16)).toUpperCase();
            dimension = dimensionHex;
        };     
        var limitSpeed;
        if (maxspeed <= 31) {
            limitSpeed = 0x0;
            var speed = maxspeed;
            var speedHexStr = "0x" + (speed.toString(16)).toUpperCase();
            var speedHex = void 0;
            speedHex = speedHexStr << 3;
            arrayData[2] = speedHex + dimension * 1; //限速+轮径
            if (dimension.code == 1) {
                var expand = 0x80;
                arrayData[4] = (p2 * 1) + (p3 << 3) + (p4 << 4) + limitSpeed + expand; // p2+p3+p4+限速+轮径拓展
            }
            else {
                var expand = 0x0;
                arrayData[4] = (p2 * 1) + (p3 << 3) + (p4 << 4) + limitSpeed + expand; // p2+p3+p4+限速+轮径拓展
            }
            // code = 0 轮径不需要扩展  code = 1 需要扩展
            // SETTINGS_MAX_SPEED < 31 限速不需要扩展
            //this.arrayData[4] = 0x28;
        }
        if (maxspeed >= 32 && maxspeed <= 63) {
            limitSpeed = 0x20;
            var speed = maxspeed - 32;
            if (speed == 0) {
                arrayData[2] = 0x5; //限速+轮径
            }
            else {
                var speedHexStr = "0x" + (speed.toString(16)).toUpperCase();
                var speedHex = void 0;
                speedHex = speedHexStr << 3;
                arrayData[2] = speedHex + dimension * 1; //限速+轮径
                if (dimension.code == 1) {
                    var expand = 0x80;
                    arrayData[4] = (p2 * 1) + (p3 << 3) + (p4 << 4) + limitSpeed + expand; // p2+p3+p4+限速+轮径拓展
                }
                else {
                    var expand = 0x0;
                    arrayData[4] = (p2 * 1) + (p3 << 3) + (p4 << 4) + limitSpeed + expand; // p2+p3+p4+限速+轮径拓展
                }
                // code = 0 轮径不需要扩展
                // SETTINGS_MAX_SPEED < 31 限速不需要扩展
            }
        }
        
    };

    function p1() {
        if (P1) {
            var p1HexStr = P1.toString(16).toUpperCase();
            var p1Hex = p1HexStr * 1;
            arrayData[3] = p1Hex;
            
        }
    };
    
    function p5() {
        if (P5) {
            var p5HexStr = P5.toString(16).toUpperCase();
            var p5Hex = p5HexStr * 1;
            arrayData[0] = p5Hex;
            
        }
    };
    
    function c1c2() {
            var c1HexStr = C1.toString(16).toUpperCase();
            var c1Hex = c1HexStr * 1;
            var c2HexStr = C2.toString(16).toUpperCase();
            var c2Hex = c2HexStr * 1;
            arrayData[6] = (c1Hex << 3) + (c2Hex);
    };

    function c3() {
        
        if (C3) {
            C3 = C3 * 1;
            if (C3 <= 5) {
                switch (C3) {
                    case 1:
                        arrayData[1] = gear = 0x01;
                        break;
                    case 2:
                        arrayData[1] = gear = 0x02;
                        break;
                    case 3:
                        arrayData[1] = gear = 0x03;
                        break;
                    case 4:
                        arrayData[1] = gear = 0x04;
                        break;
                    case 5:
                        arrayData[1] = gear = 0x05;
                        break;
                    default:
                        arrayData[1] = gear = 0x05;
                        break;
                }
            }
            }
          
    };
    
    function c5c14() {
        if (C5 && C14) {
            var c5HexStr = C5.toString(16).toUpperCase();
            var c5Hex = c5HexStr * 1;
            var c14HexStr = C14.toString(16).toUpperCase();
            var c14Hex = c14HexStr * 1;
            arrayData[7] = (c14Hex << 5) + (c5Hex);
            
        }
    };
    
    function c4c7c12() {
        if (C4 && C7 && C12) {
            var c4HexStr = C4.toString(16).toUpperCase();
            var c4Hex = c4HexStr * 1;
            var c7HexStr = C7.toString(16).toUpperCase();
            var c7Hex = c7HexStr * 1;
            var c12HexStr = C12.toString(16).toUpperCase();
            var c12Hex = c12HexStr * 1;
            arrayData[8] = (c4Hex << 5) + (c7Hex << 3) + c12Hex;
            
        }
    };
    
    function perc() {
        if (percent) {
            var percentHexStr = percent.toString(16).toUpperCase();
            var percentHex = percentHexStr * 1;
            arrayData[11] = percentHex;
            
        }
    };
    
    function handlebar() {
        if (hb_speed) {
            var HexStr = hb_speed.toString(16).toUpperCase();
            var Hex = HexStr * 1;
            arrayData[9] = Hex;
        }
    };
    
   function c13() {
        if (C13) {
            var c13HexStr = C13.toString(16).toUpperCase();
            var c13Hex = c13HexStr * 1;
            arrayData[10] = c13Hex << 2;
        }
    };

    b2b4(dimension, p2, p3, p4, maxspeed);
    p1();
    p5();
    c1c2();
    c3();
    c5c14();
    c4c7c12();
    perc();
    handlebar();
    c13();
    arrayData[5] = arrayData[1] ^ arrayData[2] ^ arrayData[3] ^ arrayData[4] ^ arrayData[6] ^ arrayData[7] ^ arrayData[8] ^ arrayData[9] ^ arrayData[10] ^ arrayData[11];
    console.log(arrayData.toString(16))

Thank's in advance!
 
Hi guys! I'm currently working on arduino replacement of lcd3 display and writing firmware for diy color display (use 240x240 st7789 cheap display and esp8266). Currently i need to find bit which corresponding with L2 parameter. Perhaps somebody can send me hex strings with L2 = 0 and L2 =1. L1 and L3 i find in KT_LCD8_override/LCD8_to_S12SN-1.txt at master · romelec/KT_LCD8_override manual.

Also i dissassemble ktsmartbicycle app and write js code for generating hex from parameters but in result i got something different
For example bit 12 doesn't exist and other differences. Perhaps it use old protocol. WARNING!!! Chineese code is insane!
Code:
var dimension = 20;
var maxspeed = 60 - 10;
var P1 = 86;
var P2 = 0;
var P3 = 1;
var P4 = 0;
var P5 = 1;
var C1 = 6;
var C2 = 0;
var C3 = 5;
var C4 = 4;
var C5 = 4;
var C7 = 1;
var C12 = 2;
var C13 = 1;
var C14 = 2;
var percent = 50;
var hb_speed = 50; //handle bar max speed
var cadence = 12;           


var p2 = "0x" + (P2.toString(16)).toUpperCase();
        var p3 = "0x" + (P3.toString(16)).toUpperCase();
        var p4 = "0x" + (P4.toString(16)).toUpperCase();          
      

arrayData = new Array(13    );
    function b2b4(dimension, p2, p3, p4, maxspeed) {
       
        var dimension;
        var _dimension;
        var dimensionHex;
        var code;
        if (dimension < 10) {
            code = 0;
            dimensionHex = "0x" + (dimension.toString(16)).toUpperCase();
            dimension = dimensionHex;
        }
        else {
            code = 1;
            _dimension = dimension - 10;
            dimensionHex = "0x" + (_dimension.toString(16)).toUpperCase();
            dimension = dimensionHex;
        };    
        var limitSpeed;
        if (maxspeed <= 31) {
            limitSpeed = 0x0;
            var speed = maxspeed;
            var speedHexStr = "0x" + (speed.toString(16)).toUpperCase();
            var speedHex = void 0;
            speedHex = speedHexStr << 3;
            arrayData[2] = speedHex + dimension * 1; //限速+轮径
            if (dimension.code == 1) {
                var expand = 0x80;
                arrayData[4] = (p2 * 1) + (p3 << 3) + (p4 << 4) + limitSpeed + expand; // p2+p3+p4+限速+轮径拓展
            }
            else {
                var expand = 0x0;
                arrayData[4] = (p2 * 1) + (p3 << 3) + (p4 << 4) + limitSpeed + expand; // p2+p3+p4+限速+轮径拓展
            }
            // code = 0 轮径不需要扩展  code = 1 需要扩展
            // SETTINGS_MAX_SPEED < 31 限速不需要扩展
            //this.arrayData[4] = 0x28;
        }
        if (maxspeed >= 32 && maxspeed <= 63) {
            limitSpeed = 0x20;
            var speed = maxspeed - 32;
            if (speed == 0) {
                arrayData[2] = 0x5; //限速+轮径
            }
            else {
                var speedHexStr = "0x" + (speed.toString(16)).toUpperCase();
                var speedHex = void 0;
                speedHex = speedHexStr << 3;
                arrayData[2] = speedHex + dimension * 1; //限速+轮径
                if (dimension.code == 1) {
                    var expand = 0x80;
                    arrayData[4] = (p2 * 1) + (p3 << 3) + (p4 << 4) + limitSpeed + expand; // p2+p3+p4+限速+轮径拓展
                }
                else {
                    var expand = 0x0;
                    arrayData[4] = (p2 * 1) + (p3 << 3) + (p4 << 4) + limitSpeed + expand; // p2+p3+p4+限速+轮径拓展
                }
                // code = 0 轮径不需要扩展
                // SETTINGS_MAX_SPEED < 31 限速不需要扩展
            }
        }
       
    };

    function p1() {
        if (P1) {
            var p1HexStr = P1.toString(16).toUpperCase();
            var p1Hex = p1HexStr * 1;
            arrayData[3] = p1Hex;
           
        }
    };
   
    function p5() {
        if (P5) {
            var p5HexStr = P5.toString(16).toUpperCase();
            var p5Hex = p5HexStr * 1;
            arrayData[0] = p5Hex;
           
        }
    };
   
    function c1c2() {
            var c1HexStr = C1.toString(16).toUpperCase();
            var c1Hex = c1HexStr * 1;
            var c2HexStr = C2.toString(16).toUpperCase();
            var c2Hex = c2HexStr * 1;
            arrayData[6] = (c1Hex << 3) + (c2Hex);
    };

    function c3() {
       
        if (C3) {
            C3 = C3 * 1;
            if (C3 <= 5) {
                switch (C3) {
                    case 1:
                        arrayData[1] = gear = 0x01;
                        break;
                    case 2:
                        arrayData[1] = gear = 0x02;
                        break;
                    case 3:
                        arrayData[1] = gear = 0x03;
                        break;
                    case 4:
                        arrayData[1] = gear = 0x04;
                        break;
                    case 5:
                        arrayData[1] = gear = 0x05;
                        break;
                    default:
                        arrayData[1] = gear = 0x05;
                        break;
                }
            }
            }
         
    };
   
    function c5c14() {
        if (C5 && C14) {
            var c5HexStr = C5.toString(16).toUpperCase();
            var c5Hex = c5HexStr * 1;
            var c14HexStr = C14.toString(16).toUpperCase();
            var c14Hex = c14HexStr * 1;
            arrayData[7] = (c14Hex << 5) + (c5Hex);
           
        }
    };
   
    function c4c7c12() {
        if (C4 && C7 && C12) {
            var c4HexStr = C4.toString(16).toUpperCase();
            var c4Hex = c4HexStr * 1;
            var c7HexStr = C7.toString(16).toUpperCase();
            var c7Hex = c7HexStr * 1;
            var c12HexStr = C12.toString(16).toUpperCase();
            var c12Hex = c12HexStr * 1;
            arrayData[8] = (c4Hex << 5) + (c7Hex << 3) + c12Hex;
           
        }
    };
   
    function perc() {
        if (percent) {
            var percentHexStr = percent.toString(16).toUpperCase();
            var percentHex = percentHexStr * 1;
            arrayData[11] = percentHex;
           
        }
    };
   
    function handlebar() {
        if (hb_speed) {
            var HexStr = hb_speed.toString(16).toUpperCase();
            var Hex = HexStr * 1;
            arrayData[9] = Hex;
        }
    };
   
   function c13() {
        if (C13) {
            var c13HexStr = C13.toString(16).toUpperCase();
            var c13Hex = c13HexStr * 1;
            arrayData[10] = c13Hex << 2;
        }
    };

    b2b4(dimension, p2, p3, p4, maxspeed);
    p1();
    p5();
    c1c2();
    c3();
    c5c14();
    c4c7c12();
    perc();
    handlebar();
    c13();
    arrayData[5] = arrayData[1] ^ arrayData[2] ^ arrayData[3] ^ arrayData[4] ^ arrayData[6] ^ arrayData[7] ^ arrayData[8] ^ arrayData[9] ^ arrayData[10] ^ arrayData[11];
    console.log(arrayData.toString(16))

Thank's in advance!
Hey! Did you manage to get this? If not, I can try to attach an Arduino to my bike. I'm also interested in the firmware. :)
 
Wrote a library for receiving data from the controller. Requires testing.
Perhaps the Arduino IDE needs to remove the first line of the library. I am using software serial and ESP8266.

#include "Arduino.h"
#include "KTLCD3_DISPLAY.h"
#define RX 0
#define TX 2
KTLCD3_DISPLAY bicycle(RX, TX);
void setup(){
Serial.begin(74880);
}
void loop(){
if (bicycle.dataAvailable()) {
bicycle.getData();
bicycle.data.print();
}
}

1689275880515.png
Variables:
1689276867560.png

1689276177471.png
This is what the html page looks like. It remains to write the code for passing the settings and finalize some details:
1689277350268.png
 

Attachments

  • KTLCD3_DISPLAY.zip
    2 KB · Views: 24
  • 1689276746414.png
    1689276746414.png
    18.6 KB · Views: 18
Last edited:
LCD>controller CRC = (xor B1,B2,B3,B4,B6,B7,B8,B9,B10,B11)
controller>LCD CRC = (xor B1,B2,B3,B4,B5,B7,B8,B9,B10,B11)
 
I got it and working, however that data I got from my LCD5, I had to do "xor 9" for having a correct CRC:
Code:
    // validation of the package data
    ui8_crc = 0;
    for (ui8_i = 0; ui8_i <= 12; ui8_i++)
    {
      if (ui8_i == 7) continue; // don't xor B5 (B7 in our case)
      ui8_crc ^= ui8_rx_buffer[ui8_i];
    }

    if ((ui8_crc ^ 9) == ui8_rx_buffer [7]) // see if CRC is ok
    {

[youtube]b5erBqISRE0[/youtube]
I can add that for my LCD8H I need to do xor 3 for the correct CRC
 
I can add that for my LCD8H I need to do xor 3 for the correct CRC
Is the CRC I suggested not working?
LCD>controller CRC = (xor B1,B2,B3,B4,B6,B7,B8,B9,B10,B11)
controller>LCD CRC = (xor B1,B2,B3,B4,B5,B7,B8,B9,B10,B11)
 
In my latest code the right last XOR number is searched automatically:
Can you test the CRC I provided? (Without additional multipliers)
ui8_crc = 0;
for (ui8_j = 1; ui8_j <= 11; ui8_j++) {

if (ui8_j == 5) continue; // don't xor B5
ui8_crc ^= ui8_rx_buffer[ui8_j];
}
 
Last edited:
?! you always need an additional XOR. Like several people reported already above....
 
?! you always need an additional XOR. Like several people reported already above....
I won't say for all cases, but from what I was able to analyze, my CRC works without a multiplier. Most compensate with a multiplier for the presence of B0, B12 in their formula.
 
Back
Top