ftmsemu / ftmsemu.github.io

12 stars 3 forks source link

OpenSource Code #1

Open joaodullius opened 2 years ago

joaodullius commented 2 years ago

Hello,

I came to your repository searching on FTMS implementation. You wrote excelent articles? Have you also released in open source your code? I was not able to find it in your repository.

nategreco commented 1 year ago

Same here - An excellent article @ftmsemu ! I was wondering if you had plans to share your source code?

If not, that's OK - I understand it is a lot of work to share to public. But I did have some follow-up questions:

nategreco commented 1 year ago

^ @OevreFlataeker - In case you are the original developer

OevreFlataeker commented 1 year ago

Sorry for not getting back to you @joaodullius - I've actually really overlooked there was this issue opened!

Thanks for your feedback. I have not (yet) open sourced it for various reasons. It's not about keeping it back, but more in the sense of keeping a low profile. If you need something in particular or have a question I might be able to answer it here.

@nategreco : ad 1) I implemented CSCS, CPS and FTMS in parallel and also advertised them. It turned out Zwift subscribes to all the notifications it could get hold of. As far as I can see it only uses the FTMS implementation though (when there are multiple available). That's why I decided in the last version to no longer advertise CSCS (and CPS AFAIR ;-) ). In the end it's internally the same data source ofc, which just gets repackaged in an answer packet according to the spec.

ad 2) I did not implement ANT+. I implemented FE-C over BLE, which is a "proxy" for the ANT+ FE-C protocol. This was really hard to get right but it works decently and I can for example even process the "special commands" the Neo2T can understand (Roadfeel). In the latter versions I mainly concentrated only on improving "FE-C over BLE". The solution is still based on an nRF52832

I've added a couple of more additions.... like proper steering support and gear change display in Zwift and enhanced OLED display output ... ;-) Also I can now do firmware updates OTA which is really convenient. The device is now also powered directly by the trainer via a DC-DC converter to convert down the 20V of the trainer to 3.3V of the board. It works decent also with all the other apps I tried (Rouvy, Tacx training, ...)

Due to HW problems with the trainer itself (it's getting quite dated...) I however replaced the whole setup this summer with an original Neo 2T I could get hold of at a good price ;-) So I am not actively improving my solution anymore right now but am ofc open to questions.

I might open-source the stuff. It's just I am reluctant cause I am not sure about the steering support module ;-)

evanscastonguay commented 1 year ago

Hi I am wondering if there is any been any taught on open source the code? I am asking because I am interested in the specific code you used for "simulating" power trainer and having access to your code would help on this. Let me know if this is something that you would be ready to share.

OevreFlataeker commented 1 year ago

Hi, thanks for the inquiry. Seems there is indeed a need for my ugly code :-D I'll go through the code during the Xmas holidays and see what needs to be done to publish it, ok?

evanscastonguay commented 1 year ago

thank you. Note I find another interresting project that target similar feature. I am trying to streamline the different algorithm to see if we could add feature. For example in their implementation they use an HR monitor to estimate cycling power. https://www.smartspin2k.com/

nategreco commented 1 year ago

Gosh @OevreFlataeker, I did the same thing and just saw your response now (too many github emails, I ignore them all).

Thanks for your response. Since that message I've actually implemented a FTMS service myself (although still fiddling with some control point stuff) - But have been debating if I should run CSC or SPS alongside it.

Interestingly enough, I've done something very similar to you but for a Nordictrack S15i bike!

nategreco commented 1 year ago

@OevreFlataeker - One last question while I'm misusing this ticket - Did you implement a FTMS control point? I was surprised to learn that Zwift only uses the simulated bike data opcode and does not control grade or resistance directly.

OevreFlataeker commented 1 year ago

Yes, I implemented the control point. True, Zwift only sends simulation parameters controlling grade, wind resistance and rolling resistance though wind and rolling seem to be fixed. If you use FE-C over BLE, which is the custom TACX implementation of ANT+ FE-C is more finegrained here. You’ll see ;-)

nategreco commented 1 year ago

Yeah, I'm looking at that now. Seems like always Wind = 0, Crr = 0.004, Cw = 0.51. But that's certainly a bug in their FTMS client. When riding you can see that rougher terrain (dirt paths/etc) produces a slower speed for the same wattage/grade, so they must have a different Crr.

That's disappointing because I wanted to set both resistance and grade from the FTMS service but it seems it isn't supported.

OevreFlataeker commented 1 year ago

Well... it's not a bug if no one complains cause it's missing? If you remember in my write-up I had a HARD time calculating the speed for my stationary bike just from CSCS. Imagine my disappointment when I realized it wasn't actually used and it's just the power which Zwift uses to do all it's internal calculations? Everything Zwift does is based on the grade parameter in the bike simulation message and neither SetResistance nor SetGrade are actually sent. I agree with you, there seems to be hardly any difference when riding on mud, dirt or asphalt. I would need to check the FE-C stuff again, how it's done there. I own a used Neo 2T since the summer so I am no longer using the emu. What Zwift does is send roadfell change messages, potentially those let the smarttrainer itself create a different force profile. It doesn't feel much though.

OevreFlataeker commented 1 year ago

https://github.com/OevreFlataeker/ftms_emu

Beware.... it's a mess

nategreco commented 1 year ago

Haha - Thanks for sharing.

Looks like you're using the old Nordic SDK instead of the new nRF Connect SDK based on Zephyr?

I've been using the latter. New to Zephyr, bit of a learning curve with the device trees, project config, etc - But I think it simplifies tings a log in the end.

nategreco commented 1 year ago

@OevreFlataeker - I've masted CSCS, CPS, FTMS, and all of the standard BLE GATT profiles, but I am totally befuddled by "FE-C over Bluetooth".

Is there any standard for this? Specifically FE-C over BLE, or even a generalized approach to ANT+ profiles over BLE? I struggle to find any information on this at all.

The only link I could find that refers to this at all is this one: https://github.com/abellono/tacx-ios-bluetooth-example

Which suggests this is a TACX brand-specific implementation (those UUID's are not in Bluetooth documentation) - Is FE-C over Bluetooth only used by TACX?

I feel like FTMs, CPS, and CSCS cover the bulk of apps today but would like to pick-up any other profiles to maximize compatibility. That being said it doesn't seem worthwhile developing code without a standard to go by. Is the FE-C over BLE 'solution' really that populer?

OevreFlataeker commented 1 year ago

I don't know if it's that popular, but the Tacx devices implement it and Zwift will use it over FTMS if both are available. Also Rouvy uses it if present. There is another very brief documentation of the FE-C over BLE "protocol" linked somewhere in the pile. It's an official Tacx document but basically only describes the high level protocol. I had to RE everything and implement basically all the things an ANT+ subsystem would do (order and frequency of pages send out etc etc). It was no simple task I have to say...

nategreco commented 1 year ago

I'm surprised that Zwift/Rouvy choose it over FTMS, that seems to be the emerging standard. Feels a bit like ANT+ is losing out to BLE. Although Nordic and thisisant.com have released beta examples of nRF Connect SDK apps using ANT+, I might see if I can support CS, CSCS, FTMS, ANT+ FE-C, and TACX FE-C all at once!

Regarding the tax document - the only links I see to tacxtraining.com are the json and firmware file, everything else looks like a github repo?

OevreFlataeker commented 1 year ago

https://github.com/abellono/tacx-ios-bluetooth-example/blob/master/How-to%20FE-C%20over%20BLE%20v1_0_0.pdf

joaodullius commented 1 year ago

Hi, good to see this is a hot topic :) If you guys want to work and a collaborative open source project, I can help to port code to Zephyr RTOS and the latest Nordic NCS.

nategreco commented 1 year ago

@OevreFlataeker - That's the document I was looking for, thanks. It does explain that this is a TACX trainer (were they bought by Garmin?) pseudo-standard they made-up.

nategreco commented 1 year ago

One more question @OevreFlataeker -

I'm digging in deep to the FE-C documentation now. I noticed there are 3 different modes to control the bike:

What does Zwift favor? Also, does FE-C not change incline? I have auto-incline working with FTMS, it is disappointing if I can only incline from FTMS or resistance from FE-C, but not both from either.

And last one - Did you implement target power? That seems not practical from my trainer but is required.

OevreFlataeker commented 1 year ago

Hi, good to see this is a hot topic :) If you guys want to work and a collaborative open source project, I can help to port code to Zephyr RTOS and the latest Nordic NCS.

Thanks for that offer! From my side I am currently no longer actively maintaining the NRF SDK solution, as I don't have any need for it anymore now owning a real Tacx Neo 2T. But it would surely be nice to get that evolved into a state-of-the-art Zephyr solution.

OevreFlataeker commented 1 year ago

One more question @OevreFlataeker -

I'm digging in deep to the FE-C documentation now. I noticed there are 3 different modes to control the bike:

  • Basic Resistance
  • Target Power
  • Simulation (setting wind and rolling resistance come from two different commands)

What does Zwift favor? Also, does FE-C not change incline? I have auto-incline working with FTMS, it is disappointing if I can only incline from FTMS or resistance from FE-C, but not both from either.

And last one - Did you implement target power? That seems not practical from my trainer but is required.

What documentation do you mean? The official FE-C ANT+ documentation PDF from thisisant.com? This was my basis for implementing as well, I just didn't add it to the repo because I understood it's only to be obtained via the thisisant.com website.

Yes, there are 3 differnent modes. Have a look at ble_fec.c. I implemented in "fec_handle_command()" the following commands

IIRC Zwift favors FE-C over FTMS. I think I implemented it as an alternative: If FE-C is to be compiled into the firmware, FTMS is not included.

See main.c:

/* Normal mode */

bool ble_ftms_active = false;
bool ble_cps_active = true;
bool ble_cscs_active = true;
volatile bool ble_fec_active = true;
bool ble_hrs_active = true;

bool ble_dis_active = true;
bool ble_bas_active = true;
bool ble_atom_active = true;
bool ble_uds_active = false;

But now also configurable on startup via the steering button:

if (app_button_is_pushed(7)) // Right steer on startup, if pressed then active FTMS, else (default) FE-C
    {
        ble_ftms_active = false;
        ble_fec_active = true;
        oled_data.ftms_active = false;         
    }
    else
    {      
        ble_ftms_active = true;
        ble_fec_active = false;
        oled_data.ftms_active = true;           
    }

Implemented like that, cause Zwift was all fine with my FE-C implementation but with Rouvy I had some problems (sim changes were not properly send during training), so I had to revert to FTMS for Rouvy. Without a probler BLE traffic dump from a life Tacx Neo 2T I could not analyse what was wrong, because all my implementation was based on were the reference documentations from ANT and without live captures :-( Now that I have a Neo I could actually do some BLE traffic dumps while riding on FE-C to fix Rouvy not working properly with my FE-C... Can provide those if anyone cares? With the real Neo 2T, FE-C works also fine for Rouvy - well at least I presume they talk FE-C :-D. My FE-C also works fine with the original Tacx/Garmin training app, so not sure what's the matter with Rouvy.

"Did you implement target power?": --> BLE_FEC_CONTROL_SET_TARGET_POWER

The incline is set via BLE_FEC_CONTROL_SET_TRACK_RESISTANCE (see code)

nategreco commented 1 year ago

Thank you! I did not notice the 'Track Resistance' data page already also included inclination. That solves that issue!

Regarding all the protocols - I'm going to try and support them all in parallel. It seems like CPS, CSCS, and FTMS all play well with one another, but only FTMS has control. We'll see about FTMS and FE-C together. I may arrange it that when one connects the other stops advertisement, but that's very tricky.

One last question, the TACX_CUSTOM_DATAPAGE's - Where are they defined? I did not see them mentioned in the thisisant.com FE-C document or the TACX engineering document linked above. Also - I'm wondering if I even need them given its not part of the original ANT standard?

OevreFlataeker commented 1 year ago

I had them all in parallel, but it was hard: Timing requirements/firmware memory usage (at least on the nRF52DK). I don't think it's possible to stop advertising as soon as a connection is established cause the connection is wholesome and not per advertised service - the SUBSCRIPTION is per service. But - proof me wrong - I am not sure, It's been a while

CUSTOM_DATAPAGE: Well... they are NOT... It's Tacx proprietary IP.... I had no success in getting any official documentation. Contacted Garmin asking for the info to implement in my own app but it was declined.

You will find the results of my RE in the code... It's 80% complete, at least to Roadfeel implementation. But I have no idea about the "management" pages like calibration or those kind of things.

However I did not see any disadvantage during training. But maybe those pages are the reason why Rouvy is not 100% happy with my implementation. Zwift seems to be VERY FORGIVING for not following standards and basically just subscribes to EVERYTHING which is advertised, whether it uses it or not. Also there is 0 sanity check whether a device can be a Tacx and a Wattbike or Sterzo at the same time... ;-) (-> It works... see GEAR_SHIFT_SERVICE implementation I REed)

nategreco commented 1 year ago

Hi Oevre, Again, than's for all the feedback - It's very helpful. I've been reading all of the FE-C documentation thoroughly and believe I have a complete implementation with nRF Connect SDK, but oddly it is not working at all. I see:

nategreco commented 1 year ago

And one other issue - I'm struggling with which characteristic ID I should transmit on and receive. From the TACX document:

The 6E40-FEC2-B5A3-F393-E0A9-E50E24DCCA9E is the RX part of the service this characteristic will notify is the trainer sends out an packet(Notification must be turned on in the CharacteristicConfigurationBits). Characteristic 6E40-FEC2-B5A3-F393-E0A9-E50E24DCCA9E is the TX part of the service, message that need to be send to the trainer can be written to this characteristic. The Ant+ FE-C wound exceed the 20 byte maximum , so there is no need to split message up in multiple sections.

Obviously there is a typo, one of those ID's should be 0xFEC3. And it's a bit confusing because it's written from the perspective of the controller device, not the trainer. So I'm a bit at a loss if I should transmit on 0xFEC2 or 0xFEC3.

In your code it looks like you transmit on 0xFEC2 and receive on 0xFEC3 - Is that correct? For me Zwift indicates 'No Signal' if I do that (although it indicates 0 watts when I reverse it, but Bluetooth is otherwise healthy)

OevreFlataeker commented 1 year ago

Well, the code I've written works? https://github.com/OevreFlataeker/ftms_emu/blob/master/ble_fec/ble_fec.h

#define BLE_UUID_TACX_SERVICE 0xFEC1
#define BLE_UUID_TACX_RX_FEC2_CHARACTERISTIC 0xFEC2               /**< The UUID of the RX Characteristic. */
#define BLE_UUID_TACX_TX_FEC3_CHARACTERISTIC 0xFEC3               /**< The UUID of the TX Characteristic. */

#define BLE_FEC_MAX_RX_CHAR_LEN        BLE_FEC_MAX_DATA_LEN /**< Maximum length of the RX Characteristic (in bytes). */
#define BLE_FEC_MAX_TX_CHAR_LEN        BLE_FEC_MAX_DATA_LEN /**< Maximum length of the TX Characteristic (in bytes). */

#define TACX_BASE_UUID                  {{0x9E, 0xCA, 0xDC, 0x24, 0x0E, 0xE5, 0xA9, 0xE0, 0x93, 0xF3, 0xA3, 0xB5, 0xC1, 0xFE, 0x40, 0x6E}} /**< Used vendor specific UUID. */
OevreFlataeker commented 1 year ago

Hi Oevre, Again, than's for all the feedback - It's very helpful. I've been reading all of the FE-C documentation thoroughly and believe I have a complete implementation with nRF Connect SDK, but oddly it is not working at all. I see:

  • Watts/RPM are 0 w/ Zwift
  • No TX messages are received - seems like Zwift never sends anything?
  • When pairing with Zwift, I only get 'Power Source' and 'Controllable' - But never cadence? I assume Zwift was associating the advertised characteristics with the pairing option, so with 2 characteristic's (RX + TX) I guess it makes sense only two pairings happen, not three - But is that right? Did Zwift for you not indicate a cadence signal when only using FE-C?

Log the notification events when Zwift connects. Maybe it doesn't pick-up on the advertised notifications. IIRC you either have "Power source" or CSCS and Power source is the preferred option.

I understand you write your own implementation but don't use mine directly?

I am not sure what you mean with "Zwift was associating...". Zwift iterates/discovers all the services for a given blue tooth server and subscribes to every notification. It's Zwift internal what is then actually used in-game. IIRC it's priority is: "FE-C -> FTMS -> CPS" with respect to "Power source".

nategreco commented 1 year ago

You are correct, I have written a completely new implementation from scratch.

So a new development - Turns out the transmission portion of my implementation works on Rouvy but not Zwift? That confirms that I send on RX and receive on TX characteristics (Sorry not to trust your code, Zwift seemed to like it better the other way around).

I definitely see Zwift connect, but for some reason it immediately shows 'No Signal' (unless I reverse TX and RX, which is wrong). I occasionally see this on my FTMS implementation too. I think it means it doesn't like something about what I am sending, not necessarily a communication timeout.

For sure the FE-C implementation is much harder than CSCS/FTMS/CPS! I am only doing it so I can get resistance control from Zwift, otherwise the other three are enough.

OevreFlataeker commented 1 year ago

Hm, well NoSignal is usually a sign of a problem with the code itself, like NPE or something. Are you confident it is actually hitting the necessary subs and subscribes to the notifications?

nategreco commented 1 year ago

Rouvy subscribes to RX notifications although never writes to the TX characteristic. Zwift never, unless I swap RX and TX, which is clearly wrong. I have used several Bluetooth analyzer apps to confirm the message data looks correct and I am alternating between pages 0x10 + 0x19 (and occasionally 0x11, 0x50, and 0x51). I don't think an NPE is at play - Zephyr is pretty good about logging such things and the program never crashes.

Generally what has me most confused is neither Zwift or Rouvy ever write to my TX characteristic. That seems like something must be blatantly wrong. Do you know if it waits for a specific message before it starts sending data?