Sonntag, 13. Oktober 2013

Ergometer (KETTLER FX1) serial protocol

I have an exercise bicycle ergometer with a serial port which I want to connect to my computer in order to use my own customized and opensource software to monitor data, like rpm, puls and power time and distance and control power.

Protokol

Hardware

On modern computers you will usually find no serial port, so you need an USB<->serial converter. Make sure to buy a good one, which also works on linux systems. According to many web-sources an converter with FTDI chipsets are the best. I bought a digitus usb-serial adaptor for ca. 11.00 EUR.

For a wired connection you need a RS-232 extension cable with a male and female D-sub-9 connector. NOT a Null modem cable! Also a three-wire connection with TxD (pin 3), RxD (pin 2) and GND (pin 5) is sufficient.

In my first step I needed to discover the protocol for ergometer data communication. For this task I used some software serial sniffer and an oscilloscope. Finally I discovered following serial connection specifications:
  • Bitrate: 9600 (According to user Unknown, Kettler World Tours changes it into 57600 when using Software)
  • Data bits: 8
  • Stop bits: 1
  • No handshake


Protocol

The next step is to discover the  protocol.  First of all I found out that it is possible to communicate with ergometer using a normal terminal for example Cutecom on Linux or Hyperterminal on windows sending strings ending with "\r\n" to the ergometer and getting "ERROR\r\n" response. 
Notation: I use a escape sequences \r, \n and \t to describe special characters for carriage return, new line and tab.

Playing with different commands I was unable to get more information, so I decided to use brute-force search methods to get the protocol specification.I wrote a small program which sends all possible strings consisting of one or two ascii symbols from "a" to "z" and ending with "\r\n" until I get a response different from "ERROR".

I was able to retrieve following commands*:
Notation: [request without "\r\n"] -> [response without "\r\n"]. User midway112 found out, that the command work only when used in uppercase. That means,for example, use "CA" instead of "ca".
  • ca -> 041
  • cd -> ACK
  • cm -> ACK
  • cp -> ACK
  • id -> FX1S
  • pd -> 000\t000\t000\t000\t025\t0000\t00:00\t000
  • pe -> 000\t000\t000\t000\t025\t0000\t00:00\t000
  • pt -> 000\t000\t000\t000\t025\t0000\t00:00\t000
  • pw -> 000\t000\t000\t000\t025\t0000\t00:00\t000
  • rd -> [long output]
  • rp -> [long output]
  • st -> 000\t000\t000\t000\t025\t0000\t00:00\t000
  • ve -> 165
  • vs -> 00 
  • PS -> [response unknwon] set speed on treadmil Task9 (Remark from User Eva).
  • PI -> [response unknwon] set titl on treadmil Task9 (Remark from User Eva).
The next part of the hacking was the funniest way of protocol analysis in my life, since it was some kind of circular training:
  1. Make an exercise on ergometer remember the values.
  2. Go to the computer check the output.
  3. Go to the step 1.
Below I will give a more precise explanation for some commands. The precise meaning of each letter is just a wild guess, since I don't have access to the original protocol specs.

Some command can be used with an argument, which is maximal a 3- or 4- digit number. The argument and the command are separated by one blank character. No leading zeros are required. Missing argument considered to be 0 and large arguments are stripped.

In order to use cp, pd, pe, pt and pw commands one must first call cd or cm commands first.
cd: Maybe put the ergometer  into manual mode ? After the call a serial port socket appears on the ergometer screen. 

cm: Seems to be the same as cd.

cp: Reset (?).

id: Get device type, which in my case is KETTLER FX1.

pd: put distance. The argument is the distance in 0.1km. The values are between 0 and 999.

pt: put time. The argument is a time. The values are between 0 and 9959. First two digits describe minutes last two digits describes seconds. If the number of seconds is greater than 59 it will be reduced to 59

pp: put power. The argument is power in Watt. The values are between 25 and 400 in 5 steps.
The values less than 25 are converted to 25. The values greater than 400 are converted to 400. The values are round down until the becomes multiple of 5.

rp: read programs. Output consists of the ergometer standard programs written as a sequence of 2-digits or 3-digits blocks. The format of each program is [number of minutes] [watt in the first minute] [watt in the second minute ]...

st: It is an important command for monitoring purpose. It can mean "current status" or "current state".
It does not need any arguments and its output consists of 8 fields separated by tab-characters, where each fields means [pulse in Hz][rpm][speed in 0.1 km/h ][distance in 0.1 km][requested power][energy in kJ][time in minutes:seconds][actual power (?)]
The fictional response 088\t072\t324\t009\t150\t0024\t10:02\t140 means:  pulse 88 beats per minute, 72 RPM, speed 32.4 km/h, distance 0.9 km, requested 150 Watt, burned energy 24kJ, time 10 minutes 02 seconds, actual power 140 Watt.

Aknowledgement:

I thank the Unknown, Eva and midway112 for their remakrs and testing.

35 Kommentare:

  1. For your information: The Kettler ErgoRace II uses default also 9600 Baudrate, but Kettler World Tours changes it into 57600 when using the software..

    AntwortenLöschen
    Antworten
    1. Thank you for the comment. Did you try to use the same commands as in this article in ErgoRace II?

      Löschen
    2. @Unknown. I added your remarkt to the original post.

      Löschen
  2. very useful article, but how to assign the distance or speed, or any other value is not specified.

    AntwortenLöschen
    Antworten
    1. Thank you. For the distance it could be pd123 for 12.3 km. But I do not think that it is possible to assign the speed.

      I can image that settings speed makes sense in a treadmill but how it should work for stationary bicycle ;)?

      Löschen
    2. Dieser Kommentar wurde vom Autor entfernt.

      Löschen
    3. Now I try to understand how to operate in the same way with the help of bluetooth. While no results.

      Löschen
    4. Dieser Kommentar wurde vom Autor entfernt.

      Löschen
    5. I figured:
      CM - a command mode
      Only after it is possible to send commands in the form of
      PS10 - set the speed to the value of 1.0
      PI20 - set the tilt angle is set to 2.0

      PS! I forgot to say that this model treadmill Trask 9

      Löschen
    6. Thank you for the comments.

      It apears to work very similar to my Kettler bycicle. My Kettler requires "cd"/"cm" commands before using the "p*-commands" too. Maybe I had to stress it better in my article.

      PI -- cound mean "put inclination".

      How do you want to connect it with bluetooth?

      I built a test device to transmit data from the serial serial to bluetooth several years ago. But now I am thinking to rebuild it with more nice and cheap modern circuits.

      Löschen
    7. I tried to connect it to the bluetooth using the Arduino and HC-05, but came across a problem, Arduino itself is not sent to the port of TRACK-9 commands. I do not know how to win. I think the use of 'USB Host Shield v2', but do not know the code for this program.

      Löschen
    8. Dieser Kommentar wurde vom Autor entfernt.

      Löschen
    9. I faced once a similar problem because I used a device with TTL-Serial. That is 0/3.3V. In contrast, for RS-232 in Ergometer you need more: -5V/5V. I used an MAX232 ic and it worked.

      May be I will a HC-05 to test wireless communication with Android and other SPP-BT devices.

      I did some experiments with SparkFun ESP32 Thing. It has nice specification, it is small but important sofware is missing. In particular there no SPP-BT support. I was not able to connect it to my Android devices.

      Löschen
    10. @Eva I added your remarks to the original post.

      Löschen
  3. Dieser Kommentar wurde vom Autor entfernt.

    AntwortenLöschen
  4. Dieser Kommentar wurde vom Autor entfernt.

    AntwortenLöschen
  5. Dieser Kommentar wurde vom Autor entfernt.

    AntwortenLöschen
  6. Thanks for writing this!

    I am working on a project to store the training data on my Synology NAS (php, mysql) with an ESP32 NODEMCU development board.

    I found out after a while that only UPPECASE commands work. ST, ID etc. (maybe you want to update the post?)

    Also I found some strange behavior of my MAX3232: It would not send any data if I connected the Kettler CTR3 / CX3 RS232 first and then power up the ESP32.
    I added a 4.7k resistor in the out line of the RS232 side of the max - now it works.

    Also strange: The first command I send always returns ERROR, no matter what I send. All succeeding commands work as described.

    Hope that may help others who try the same.

    Cheers,

    Henrik

    AntwortenLöschen
    Antworten
    1. Hi Hendrik,
      could you link your solution / Nodemcu. Im very interested and would like to log via Nodemcu and upload the data to mysql
      Thanks

      Löschen
    2. > Also strange: The first command I send always returns ERROR

      Did you tried to use some kind of terminal, to send data from a desktop computer? It helps to distinguish between Hardware and Software problems.

      Löschen
    3. Dieser Kommentar wurde vom Autor entfernt.

      Löschen
    4. @midway11 I added your remarks to the original post.

      Löschen
    5. > Thanks for writing this!
      :). I am glad that this information is still useful.
      > I found out after a while that only UPPECASE commands work. ST, ID etc. (maybe you want to update the post?)

      I cannot test it now with my device, but I will add your update. Thak you for sharing this information!

      Löschen
  7. Dieser Kommentar wurde vom Autor entfernt.

    AntwortenLöschen
  8. Dusty
    I did not publish anything yet but I can share what I have. Let me know how to contact you

    AntwortenLöschen
  9. Thanks for your answer, can you please contact me at "mydustydustATgmail.com"

    AntwortenLöschen
    Antworten
    1. Sorry, I just saw, your posts :)

      It would be better if you publish your results first and then other developers could join.

      Löschen
  10. Thanks to the former work i was able to make a small programm for my ctr3.
    Just one small correction: the hartbeat is in beat per minute.

    Here is my vb.net code. You just need a Form with a Serialport, a timer (I have it on 1000 ms) and 8 labels for the live values. for the graf you need a chart with two series of lines. And last but not least a save botton.

    AntwortenLöschen
    Antworten
    1. Hello,

      am not it geek, but am owner of Kettler AXIOM (usb and BT connections possible).
      Would like to have an app, that at the same time records all training data and stores it to my PC (or via PC to the cloud) in database (favourable) or excel file.
      I have HR stripe attached to AXIOM Kettler.

      I have Windows 10 Pro 64 it.

      I am keen to purchase such an app (PayPal etc).

      Awaiting response.

      Löschen
    2. Dieser Kommentar wurde vom Autor entfernt.

      Löschen
    3. Hi @EasyCoder,
      I am glad that you could communicate with your Kettler Device.
      thank you for you code and for the correction - 88 Beats per seconds was too much 😅.

      Löschen
    4. @ Bee Man.
      I do not development apps for Kettler anymore but if someone will do it, here are some my hints. Probably both: USB and BT will use some sort of virtual serial Port on a Windows System. For this kind of communication the ports settings like bitrate, data bits, stop bits, and handshake, are not important. Only the port name is important.

      If Bluetooth uses serial profile then it is also possible to connect an Android Phone directly to it with the Android Bluetooth Sockets API: https://developer.android.com/reference/android/bluetooth/BluetoothSocket

      Löschen
  11. Public Class Form1
    Dim FlagDatenEmpfangen As Boolean = True
    Dim StopUhr As New Stopwatch
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
    SerialPort1.BaudRate = 9600
    SerialPort1.DataBits = 8
    SerialPort1.PortName = "COM3"
    SerialPort1.StopBits = IO.Ports.StopBits.One
    SerialPort1.Parity = IO.Ports.Parity.None
    SerialPort1.NewLine = vbLf

    Try
    SerialPort1.Open()
    Timer1.Start()
    StopUhr.Start()
    Catch
    MessageBox.Show("Fehler: SerialPort konnte nicht geöffnet werden.")
    End Try
    End Sub

    Dim ReceivedText As String
    Dim Values As String()
    Dim Messwerte As New List(Of String)

    Dim Index As Integer
    Private Sub SerialPort1_DataReceived(sender As Object, e As IO.Ports.SerialDataReceivedEventArgs) Handles SerialPort1.DataReceived
    If FlagDatenEmpfangen Then

    ReceivedText = SerialPort1.ReadLine

    DelegateTextBox1(ReceivedText)

    ReceivedText = ReceivedText.Replace(vbCr, "")

    Messwerte.Add(StopUhr.ElapsedMilliseconds & vbTab & ReceivedText)

    If ReceivedText.Length = 34 Then
    Values = ReceivedText.Split(vbTab)

    DelegateLblHerzschlag(CInt(Values(0)))
    DelegateSerie1(CInt(Values(0)))
    DelegateLblDrehzahl(CInt(Values(1)))
    DelegateLblGeschwindigkeit(CInt(Values(2)) / 10)
    DelegateLblDistanz(CInt(Values(3)) / 10)
    DelegateLblLeistungSoll(CInt(Values(4)))
    DelegateLblVerbrauchteEnergie(Math.Round(CInt(Values(5)) / 4.1868, 0))
    DelegateLbLZeit(Values(6))
    DelegateLblLeistungIst(CInt(Values(7)))
    DelegateSerie2(CInt(Values(7)))
    End If
    End If
    End Sub
    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    SerialPort1.Write(TextBox2.Text & vbCr)
    End Sub

    #Region "Delegates"
    Delegate Sub TextBox1Delegate(text As String)
    Sub DelegateTextBox1(ByVal text As String)
    If TextBox1.InvokeRequired Then
    Dim d As New TextBox1Delegate(AddressOf DelegateTextBox1)
    Invoke(d, New Object() {text})
    Else
    TextBox1.Text = text & vbLf & TextBox1.Text
    End If
    End Sub

    Delegate Sub Serie1Delegate(value As Double)
    Sub DelegateSerie1(ByVal value As Double)
    If Chart1.InvokeRequired Then
    Dim d As New Serie1Delegate(AddressOf DelegateSerie1)
    Invoke(d, New Object() {value})
    Else
    Chart1.Series(0).Points.AddY(value)
    End If
    End Sub

    Delegate Sub Serie2Delegate(value As Double)
    Sub DelegateSerie2(ByVal value As Double)
    If Chart1.InvokeRequired Then
    Dim d As New Serie2Delegate(AddressOf DelegateSerie2)
    Invoke(d, New Object() {value})
    Else
    Chart1.Series(1).Points.AddY(value)
    End If
    End Sub

    Delegate Sub LblHerzschlagDelegate(text As String)
    Sub DelegateLblHerzschlag(ByVal text As String)
    If LblHerzschlag.InvokeRequired Then
    Dim d As New LblHerzschlagDelegate(AddressOf DelegateLblHerzschlag)
    Invoke(d, New Object() {text})
    Else
    LblHerzschlag.Text = "Herzschlag: " & text & " bpm"
    End If
    End Sub

    Delegate Sub LblDrehzahlDelegate(text As String)
    Sub DelegateLblDrehzahl(ByVal text As String)
    If LblDrehzahl.InvokeRequired Then
    Dim d As New LblDrehzahlDelegate(AddressOf DelegateLblDrehzahl)
    Invoke(d, New Object() {text})
    Else
    LblDrehzahl.Text = "Drehzahl: " & text & " rpm"
    End If
    End Sub

    AntwortenLöschen
  12. Delegate Sub LblGeschwindigkeitDelegate(text As String)
    Sub DelegateLblGeschwindigkeit(ByVal text As String)
    If LblHerzschlag.InvokeRequired Then
    Dim d As New LblGeschwindigkeitDelegate(AddressOf DelegateLblGeschwindigkeit)
    Invoke(d, New Object() {text})
    Else
    LblGeschwindigkeit.Text = "Geschwindigkeit: " & text & " km/h"
    End If
    End Sub

    Delegate Sub LblDistanzDelegate(text As String)
    Sub DelegateLblDistanz(ByVal text As String)
    If LblDistanz.InvokeRequired Then
    Dim d As New LblDistanzDelegate(AddressOf DelegateLblDistanz)
    Invoke(d, New Object() {text})
    Else
    LblDistanz.Text = "Distanz: " & text & " km"
    End If
    End Sub

    Delegate Sub LblLeistungSollDelegate(text As String)
    Sub DelegateLblLeistungSoll(ByVal text As String)
    If LblLeistungSoll.InvokeRequired Then
    Dim d As New LblLeistungSollDelegate(AddressOf DelegateLblLeistungSoll)
    Invoke(d, New Object() {text})
    Else
    LblLeistungSoll.Text = "Leistung Soll: " & text & " W"
    End If
    End Sub

    Delegate Sub LblVerbrauchteEnergieDelegate(text As String)
    Sub DelegateLblVerbrauchteEnergie(ByVal text As String)
    If LblVerbrauchteEnergie.InvokeRequired Then
    Dim d As New LblVerbrauchteEnergieDelegate(AddressOf DelegateLblVerbrauchteEnergie)
    Invoke(d, New Object() {text})
    Else
    LblVerbrauchteEnergie.Text = "Verbrauchte Energie: " & text & " kcal"
    End If
    End Sub

    Delegate Sub LbLZeitDelegate(text As String)
    Sub DelegateLbLZeit(ByVal text As String)
    If LbLZeit.InvokeRequired Then
    Dim d As New LbLZeitDelegate(AddressOf DelegateLbLZeit)
    Invoke(d, New Object() {text})
    Else
    LbLZeit.Text = "Zeit: " & text
    End If
    End Sub

    Delegate Sub LblLeistungIstDelegate(text As String)
    Sub DelegateLblLeistungIst(ByVal text As String)
    If LblLeistungIst.InvokeRequired Then
    Dim d As New LblLeistungIstDelegate(AddressOf DelegateLblLeistungIst)
    Invoke(d, New Object() {text})
    Else
    LblLeistungIst.Text = "Leistung Ist: " & text & " W"
    End If
    End Sub
    #End Region
    Private Sub Form1_FormClosing(sender As Object, e As FormClosingEventArgs) Handles MyBase.FormClosing
    Timer1.Stop()
    FlagDatenEmpfangen = False

    SerialPort1.Write("id" & vbCr)
    End Sub

    Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick
    SerialPort1.Write("st" & vbCr)
    End Sub

    AntwortenLöschen
  13. Dim WerTrainiert As String
    Private Sub RdbPerson1_CheckedChanged(sender As Object, e As EventArgs) Handles RdbPerson1.CheckedChanged
    If RdbPerson1.Checked Then
    WerTrainiert = "Person1"
    End If
    End Sub

    Private Sub RdbPerson2_CheckedChanged(sender As Object, e As EventArgs) Handles RdbPerson2.CheckedChanged
    If RdbPerson2.Checked Then
    WerTrainiert = "Person2"
    End If
    End Sub

    Private Sub CmdSpeichern_Click(sender As Object, e As EventArgs) Handles CmdSpeichern.Click
    Speichern()
    End Sub

    Sub Speichern()
    Dim FileName As String = WerTrainiert & Format(Date.Now, "yyyyMMdd_HHmmss") & ".txt"
    Dim fs As New IO.FileStream(My.Computer.FileSystem.SpecialDirectories.Desktop & "\" & FileName, IO.FileMode.OpenOrCreate)
    Dim sw As New IO.StreamWriter(fs)

    sw.WriteLine("Timer [ms]" & vbTab & "Herzschlag [bpm]" & vbTab & "Drehzahl [rpm]" & vbTab & "Geschwindigkeit [0.1 km/h]" & vbTab & "Distanz [0.1 km]" & vbTab & "Leistung Soll [W]" & vbTab & "Verbrauchte Energie [kJ]" & vbTab & "Trainingszeit [mm:ss]" & vbTab & "Leistung Ist [W]")

    For Each Linie As String In Messwerte
    sw.WriteLine(Linie)
    Next

    sw.Close()
    End Sub
    End Class

    AntwortenLöschen