Fardriver controller serial protocol reverse engineering

Hello! Has anyone figured out the CRC16 calculation in the latest versions of the controller? I found this information: Poly: 0x8005, Init: 0x7F3C, RefIn: true, RefOut: true, XorOut: false. But it doesn't work for my controller. Here is one of the parcels, you can check:
AA 80 85 07 11 20 00 00 00 00 00 00 00 00 1F FF
AA 81 74 00 79 00 00 00 00 00 00 00 00 00 A2 5D
AA 82 00 00 00 00 00 00 00 00 00 00 00 00 C5 2A
 
Last edited:
Making some progress with reading the UART from Fardriver controller. I am using an ESP32-S3, Visual Studio Code with PlatformIO extension and the ESP-IDF. I think i will have a working software at the end of summer.

20240623_215403944_iOS.jpg
 
Hello! Has anyone figured out the CRC16 calculation in the latest versions of the controller? I found this information: Poly: 0x8005, Init: 0x7F3C, RefIn: true, RefOut: true, XorOut: false. But it doesn't work for my controller. Here is one of the parcels, you can check:
AA 80 85 07 11 20 00 00 00 00 00 00 00 00 1F FF
AA 81 74 00 79 00 00 00 00 00 00 00 00 00 A2 5D
AA 82 00 00 00 00 00 00 00 00 00 00 00 00 C5 2A

This github repository has the implementation of the CRC in one of its files with some comments in russian. -> google translate.


But i think it´s only needed for sending data to the controller not for reading the data from it for a display.
 
The data there is outdated, I checked. The Chinese changed the algorithm again.
I just need a CRC for sending.
But I picked up the data!
 
I've started documenting my findings on the serial protocol in a Github repo - I've been able to decode most of the status messages received from the controller, and have started to figure out how to interact with the controller through sending commands, etc. I'd like to fill the fardriver.hpp with comments about what each variable does/how it works. The two CRC methods are described there are well.
 
Someone messaged me asking about details on what I've found, so I thought I'd describe the decoding algorithm a bit here, in addition to the readme I've been working on: fardriver-controllers/README.md at main · jackhumbert/fardriver-controllers - I also have the full decoding implemented in a 010 Editor template file: fardriver-controllers/fardriver.bt at main · jackhumbert/fardriver-controllers - it can decode a binary file of the data received.

The 16 byte status messages look like this (the c++ version is here):

Code:
0xAA <0x80 + index> <data[12]> <crc[2]>

Where the index (after removing the highest two bits) can be converted to a flash address via this lookup table:

Code:
const uint8_t flash_read_addr[55] = {
  0xE2, 0xE8, 0xEE, 0x00, 0x06, 0x0C, 0x12, 
  0xE2, 0xE8, 0xEE, 0x18, 0x1E, 0x24, 0x2A, 
  0xE2, 0xE8, 0xEE, 0x30, 0x5D, 0x63, 0x69, 
  0xE2, 0xE8, 0xEE, 0x7C, 0x82, 0x88, 0x8E, 
  0xE2, 0xE8, 0xEE, 0x94, 0x9A, 0xA0, 0xA6, 
  0xE2, 0xE8, 0xEE, 0xAC, 0xB2, 0xB8, 0xBE, 
  0xE2, 0xE8, 0xEE, 0xC4, 0xCA, 0xD0,
  0xE2, 0xE8, 0xEE, 0xD6, 0xDC, 0xF4, 0xFA
};

Based on that address, the data section can be mapped to a struct in the fardriver.hpp file, named via the address in hex: fardriver-controllers/fardriver.hpp at main · jackhumbert/fardriver-controllers - I tried to use a lot of the names from the app, but am still working on describing things/adding notes.

I'm also working on a template for HEB files that works the same way as the one mentioned earlier (HEB files are just pure data though, and also include CAN info): fardriver-controllers/HEB.bt at main · jackhumbert/fardriver-controllers
 
Hey,
Here's what I have been able to figure out about the serial communications protocol for the Fardriver controller.
I have only looked at the data FROM the controller, as I wanted to use it for a custom display.
My thread with my EKSR build that use that display, is here.
If you have anything to add to this and/or can help decode this further, please let me know.

Questions welcome, of course.

Here is a short Python program you can run to visualize the messages on the serial link and what bytes are being changed:
(change the serial port in the last line to suit your setup)

import serial
import datetime, threading, time

alist = [b'\x00' * 32] * 32

def open_serial_port(port, baudrate):
try:
global ser
ser = serial.Serial(port, baudrate, timeout=1)
print(f"Connected to {port} at {baudrate} baud.")
return ser
except Exception as e:
print(f"Error opening serial port: {e}")
return None

def clear_screen():
print('\x1B[2J')

def goto_line(line):
print('\x1B[' + str(line) + ';1H')

def set_color(inverse):
if (inverse):
print('\x1B[7m', end='')
else:
print('\x1B[0m', end='')
return

def read_from_serial(ser):
while ser.in_waiting:
try:
global alist
# read 16 bytes total : 0xAA, address, 12 bytes data and 2 bytes checksum
data = ser.read(16)
index = data[1]
if (index < 24):
goto_line(index + 2)
set_color(False)
print('{: >2}'.format(index) + ' : ', end='')
old = alist[index]
for i in range(2,14):
if (old != data):
set_color(True)
else:
set_color(False)
print(format(data,'02x') + ' ', end='')
set_color(False)
print('')
alist[index] = data


except Exception as e:
print(f"\nError reading from serial: {e}")


def write_to_serial(ser):
try:
data = input("Enter data to send: ")
ser.write(data.encode('utf-8'))
except Exception as e:
print(f"Error writing to serial: {e}")

def foo():
global ser
if (foo.first):
keep_alive = [ 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0 ] # size 8
else:
keep_alive = [ 0xAA, 0x13, 0xec, 0x07, 0x01, 0xF1, 0xA2, 0x5D ] # size 8
foo.first = False
ser.write(keep_alive);
#print (datetime.datetime.now())
threading.Timer(2, foo).start()

foo.first = True

def run_serial_terminal(port, baudrate):
ser = open_serial_port(port, baudrate)
clear_screen()
goto_line(1)
print(' 0 1 2 3 4 5 6 7 8 9 10 11')
print('\x1B[?25l')

timerThread = threading.Thread(target=foo)
timerThread.daemon = True
timerThread.start()

if ser:
try:
while True:
read_from_serial(ser)
time.sleep(0.1) # Adjust the polling rate if needed
except KeyboardInterrupt:
print("\nClosing serial connection.")
finally:
ser.close()
else:
print("Unable to open serial port.")

if __name__ == "__main__":
run_serial_terminal("/dev/tty.usbserial-834440", 19200)
 

Attachments

  • FarDriver serial messages.pdf
    157.3 KB · Views: 79
Someone messaged me asking about details on what I've found, so I thought I'd describe the decoding algorithm a bit here, in addition to the readme I've been working on: fardriver-controllers/README.md at main · jackhumbert/fardriver-controllers - I also have the full decoding implemented in a 010 Editor template file: fardriver-controllers/fardriver.bt at main · jackhumbert/fardriver-controllers - it can decode a binary file of the data received.

The 16 byte status messages look like this (the c++ version is here):

Code:
0xAA <0x80 + index> <data[12]> <crc[2]>

Where the index (after removing the highest two bits) can be converted to a flash address via this lookup table:

Code:
const uint8_t flash_read_addr[55] = {
  0xE2, 0xE8, 0xEE, 0x00, 0x06, 0x0C, 0x12,
  0xE2, 0xE8, 0xEE, 0x18, 0x1E, 0x24, 0x2A,
  0xE2, 0xE8, 0xEE, 0x30, 0x5D, 0x63, 0x69,
  0xE2, 0xE8, 0xEE, 0x7C, 0x82, 0x88, 0x8E,
  0xE2, 0xE8, 0xEE, 0x94, 0x9A, 0xA0, 0xA6,
  0xE2, 0xE8, 0xEE, 0xAC, 0xB2, 0xB8, 0xBE,
  0xE2, 0xE8, 0xEE, 0xC4, 0xCA, 0xD0,
  0xE2, 0xE8, 0xEE, 0xD6, 0xDC, 0xF4, 0xFA
};

Based on that address, the data section can be mapped to a struct in the fardriver.hpp file, named via the address in hex: fardriver-controllers/fardriver.hpp at main · jackhumbert/fardriver-controllers - I tried to use a lot of the names from the app, but am still working on describing things/adding notes.

I'm also working on a template for HEB files that works the same way as the one mentioned earlier (HEB files are just pure data though, and also include CAN info): fardriver-controllers/HEB.bt at main · jackhumbert/fardriver-controllers
Wow! Really nice work man! This is going to help my project a lot!
 
Someone messaged me asking about details on what I've found, so I thought I'd describe the decoding algorithm a bit here, in addition to the readme I've been working on: fardriver-controllers/README.md at main · jackhumbert/fardriver-controllers - I also have the full decoding implemented in a 010 Editor template file: fardriver-controllers/fardriver.bt at main · jackhumbert/fardriver-controllers - it can decode a binary file of the data received.

The 16 byte status messages look like this (the c++ version is here):

Code:
0xAA <0x80 + index> <data[12]> <crc[2]>

Where the index (after removing the highest two bits) can be converted to a flash address via this lookup table:

Code:
const uint8_t flash_read_addr[55] = {
  0xE2, 0xE8, 0xEE, 0x00, 0x06, 0x0C, 0x12,
  0xE2, 0xE8, 0xEE, 0x18, 0x1E, 0x24, 0x2A,
  0xE2, 0xE8, 0xEE, 0x30, 0x5D, 0x63, 0x69,
  0xE2, 0xE8, 0xEE, 0x7C, 0x82, 0x88, 0x8E,
  0xE2, 0xE8, 0xEE, 0x94, 0x9A, 0xA0, 0xA6,
  0xE2, 0xE8, 0xEE, 0xAC, 0xB2, 0xB8, 0xBE,
  0xE2, 0xE8, 0xEE, 0xC4, 0xCA, 0xD0,
  0xE2, 0xE8, 0xEE, 0xD6, 0xDC, 0xF4, 0xFA
};

Based on that address, the data section can be mapped to a struct in the fardriver.hpp file, named via the address in hex: fardriver-controllers/fardriver.hpp at main · jackhumbert/fardriver-controllers - I tried to use a lot of the names from the app, but am still working on describing things/adding notes.

I'm also working on a template for HEB files that works the same way as the one mentioned earlier (HEB files are just pure data though, and also include CAN info): fardriver-controllers/HEB.bt at main · jackhumbert/fardriver-controllers
Have you been able to figure out the login process via Bluetooth? I'm connecting to the the controller via BLE through my Mac and the controller seems to only send me dummy default values.
 
Have you been able to figure out the login process via Bluetooth? I'm connecting to the the controller via BLE through my Mac and the controller seems to only send me dummy default values.
Nevermind, messed up Little Endian with Big Endian!
 
Nevermind, messed up Little Endian with Big Endian!
Hi @Dexion I saw you made an app "GD Fardriver Speed & Trip Info". Did you manage to connect to the fardriver's ble dongle?
I've been getting data from the fardrivers for more than an year now but via the SIF (one line) output using an esp32 and seding it to my stock motorcycle display and to an extra 4" stats display. However the protocol is quite limited and I was thinking of switching to serial to get more stable (no EMI) and more complete data. However if I were to do it clasically I guess that would require a 4 channel relay module to switch the serial input from esp32 to the bluetooth dongle and disconnect the 5v from the dongle when using esp32. If it were to work via BLE that would be amazing as no hardware mods would be necessary, all just software.

On my ebike with votol I use serial to get controller data and pass it to my displays and classic bluetooth serial to passtrhrough the connection when I want to change controller settings.

Also on my BMS', both JK and JBD I get my data via BLE.

Aside from what's in the well put document here GitHub - jackhumbert/fardriver-controllers: Information on Nanjing Fardriver Controllers , is there anything else I should take into account when connecting via ble? Do I need to login for read only data?

Thanks in advance.
 
Hi @Dexion I saw you made an app "GD Fardriver Speed & Trip Info". Did you manage to connect to the fardriver's ble dongle?
I've been getting data from the fardrivers for more than an year now but via the SIF (one line) output using an esp32 and seding it to my stock motorcycle display and to an extra 4" stats display. However the protocol is quite limited and I was thinking of switching to serial to get more stable (no EMI) and more complete data. However if I were to do it clasically I guess that would require a 4 channel relay module to switch the serial input from esp32 to the bluetooth dongle and disconnect the 5v from the dongle when using esp32. If it were to work via BLE that would be amazing as no hardware mods would be necessary, all just software.

On my ebike with votol I use serial to get controller data and pass it to my displays and classic bluetooth serial to passtrhrough the connection when I want to change controller settings.

Also on my BMS', both JK and JBD I get my data via BLE.

Aside from what's in the well put document here GitHub - jackhumbert/fardriver-controllers: Information on Nanjing Fardriver Controllers , is there anything else I should take into account when connecting via ble? Do I need to login for read only data?

Thanks in advance.
I'm weaker on the hardware side, but I do know no login is necessary for read only data via BLE.
 
I am able to get data via ble and decode the packages.
The useful data rate is about 5.2 frames / s (for frame types EE, E8 and E2) and that is exactly what I got with SIF.
Note to anyone else wanting to connect via BLE is that the fardriver changes its address, so you need to scan it by name each time you want to connect to it. I used nRF Connect to get the service and characteristics.
I did not need to request the packages, the notifications just arrive upon subscribing to them, at least on my nd72680.
I only tested it lightly to see if it works, but I will implement it on my motorcycle soon.
Many thanks to @jackhumbert for putting together all that information and to everyone else involved.
 
I implemented this on my motorcycle, I'm able to get all the relevant data, except the cruise control flag. I'll probably need to put the bike on a stand and inspect the bits that change once cruise is enabled. I expect it to be hidden somewhere in E2 .
 
Any tips appreciated.


Have a newer 961800. I made a serial to USB cable. I can connect to the fardriver PC app.

I used pulse view and a logic decoder to sniff the lines. And applied the Uart protocol and set the channels.

My goal here is to simply make a logging device that will log to a CSV file for review.

Attached is the Pulseview sniff.

It seems the data is serial 19200, however still get framing errors, slightly out of my element here.

But between this forum, AI, and youtube I am giving it an honest effort.

I also created a python script that would record data, and send a wake up call.


#!/usr/bin/env python3
# FD_loggerrev1.05.py
# Universal “ID+Words” sniffer/logger for FarDriver live telemetry

import serial, csv, time, threading, os, sys, signal

# ——— CONFIG ———
PORT = "COM4" # your COM port
BAUD_RATE = 19200 # must match controller
HEADER = 0xAA
PACKET_SIZE = 16
WAKE2 = bytes([0xAA,0x13,0xEC,0x07,0x01,0xF1,0xA2,0x5D])
POLL_SEC = 1.0 # how often to poke for new data

ser = None
timer = None

def next_log_number():
base, ext = "FD LOG", ".csv"
n = 1
while os.path.exists(f"{base} {n}{ext}"):
n += 1
return n

def keep_awake():
"""Send WAKE2 to solicit live frames, then reschedule."""
global timer
try:
ser.write(WAKE2)
print(f"[TX WAKE] {WAKE2.hex(' ').upper()}")
except Exception as e:
print(f"[ERROR] WAKE2 failed: {e}")
timer = threading.Timer(POLL_SEC, keep_awake)
timer.start()

def shutdown(sig=None, frame=None):
"""Stop the timer, close serial, exit."""
if timer:
timer.cancel()
if ser and ser.is_open:
ser.close()
print("\n[!] Logger stopped.")
sys.exit(0)

def main():
global ser, timer
signal.signal(signal.SIGINT, shutdown)

# allocate file names
idx = next_log_number()
csv_fn = f"FD LOG {idx}.csv"
raw_fn = f"FD LOG {idx}_raw.txt"
print(f"[+] CSV → {csv_fn}")
print(f"[+] RAW → {raw_fn}\n")

# open COM port
try:
ser = serial.Serial(PORT, BAUD_RATE, timeout=1)
except Exception as e:
print(f"[ERROR] opening {PORT}@{BAUD_RATE}: {e}")
sys.exit(1)

# start polling
keep_awake()

# set up files
with open(raw_fn, "w") as rawf, open(csv_fn, "w", newline="") as csvf:
csvw = csv.writer(csvf)
# header: timestamp,id,w0..w5
csvw.writerow(["timestamp_s", "id"] + [f"w{i}" for i in range(6)])
t0 = time.time()

try:
while True:
# sync on header byte
b = ser.read(1)
if not b or b[0] != HEADER:
continue
pkt = b + ser.read(PACKET_SIZE - 1)
if len(pkt) != PACKET_SIZE:
continue

# raw dump
line = " ".join(f"{c:02X}" for c in pkt)
rawf.write(line + "\n")
rawf.flush()
print(f"[RX RAW] {line}")

# unpack id & words
pid = pkt[1]
data = pkt[2:14]
words = struct.unpack("<6H", data)

# write CSV row
ts = time.time() - t0
csvw.writerow([f"{ts:.3f}", f"0x{pid:02X}"] + list(words))
csvf.flush()

except Exception as e:
print(f"[ERROR] main loop: {e}")
finally:
shutdown()

if __name__ == "__main__":
main()




This would record data, but it always seemed to recording data offset, and not correctly.

Willing to compensate anyone for there time.
 

Attachments

  • logic enabled.rar
    159.9 KB · Views: 0
THe most likely problem is noise in the signal itself, or the wrong level for the serial connection at one transciever vs the other. Either could cause loss of or mistranslation of data.

Some serial transceivers use 3.3v, some 5v. You should match the transceiver type at each end, so that your USB-serial cable has the same voltage signal as the controller's serial I/O.

Generally,the higher the voltage range of the signal, the greater the noise immunity, as noise is usually small scale voltage variations.

(an early RS232 spec called for a +/- 25v signal range, for greater immunity to noise over long cable runs (it's more complicated than that, but...)
 
THe most likely problem is noise in the signal itself, or the wrong level for the serial connection at one transciever vs the other. Either could cause loss of or mistranslation of data.

Some serial transceivers use 3.3v, some 5v. You should match the transceiver type at each end, so that your USB-serial cable has the same voltage signal as the controller's serial I/O.

Generally,the higher the voltage range of the signal, the greater the noise immunity, as noise is usually small scale voltage variations.

(an early RS232 spec called for a +/- 25v signal range, for greater immunity to noise over long cable runs (it's more complicated than that, but...)


Thank you, I will look into this.

I have a OEM FD PC cable, I will use that instead, was trying to leave that intact and look into other fixes for a clean signal.

Thank you.
 
Got most things working, Phase amps, voltage, line amps, throttle volts.

Struggling with RPM and DNR / 1-2-3.

Also, For anyone wondering... There IS A FACTORY RESET bit you can command. By mistake.......... Which involves importing tune again and redoing auto learn. Trust me :p
 
Back
Top