S’amuser avec le bus CAN d’un vélo Bosch – puce de débridage DIY

Après avoir démonté mon moteur suite à quelques soucis mécaniques, j’ai voulu en savoir plus sur son fonctionnement. Le système Bosch est propriétaire, peu documenté et du coup, assez mystérieux. Pourtant, il est extrêmement répandu. J’ai donc supposé qu’il était particulièrement verrouillé, c’est pourquoi j’ai attendu si longtemps avant de m’y attarder.

Une autre chose qui m’a toujours parue énigmatique sont les puces de débridages. Toutes interfèrent sur le capteur de vitesse de la roue, en général un aimant fixé sur un rayon et un ILS sur le cadre. En faussant les impulsions envoyées, on peut faire croire que le vélo roule plus lentement qu’en réalité. Jusque là, rien de sorcier. Beaucoup de puces de premières générations se contentaient de renvoyer une impulsion sur 2. L’inconvénient était que le compteur indiquait des vitesses et distances également divisées par 2.

Les puces plus récentes, en revanche, permettent de conserver un affichage des vitesses et distances correctes sur le compteur d’origine. Comment réussissent-elles l’exploit de désactiver le bridage alors que le système connaît (puisqu’il l’affiche) la vraie vitesse du vélo ? Comment l’ajout d’une « puce » peut modifier le comportement d’un firmware particulièrement sophistiqué ?

Un jour, j’ai compris, et c’est tout bête. Pour m’amuser et valider mes hypothèses, j’ai décidé de créer ma propre puce de débridage et de démystifier un peu leur fonctionnement.

Remarque n°1 : débrider un VAE n’est pas un acte anodin et cet article n’a pas pour but de l’encourager mais de transmettre des connaissances techniques.

Remarque n°2 : un VAE débridé devient assimilable à un véhicule à moteur non homologué et par conséquent impossible à assurer. En cas d’accident corporel, les conséquences peuvent être désastreuses.

Remarque n°3 : Bosch lutte activement contre le débridage de ses vélos. On trouve même des rumeurs qui affirment qu’après trop de détections, le moteur pourrait se bricker définitivement. En pratique, il semble que les 3 premières fois, il suffise de pédaler un certain temps avec l’assistance réduite pour désactiver la punition, mais qu’au bout de la 4è, il faille utiliser Bosch Diagnostic Tool (source). Évidemment, cela engendre l’annulation de la garantie.

Pour finir : il existe actuellement 2 grandes générations de systèmes Bosch :

  • La génération « historique » (de 2014 à 2022, peut-être même dès 2011 d’après certaines sources). Les équipement suivants en font partie :
    • Batteries Powerpack 300, 400, 500 ; Powertube 400, 500, 625
    • Ecrans Intuvia, Purion, Kiox et Nyon
  • La nouvelle génération appelée « Smart System » ou « Le système intelligent » qui a vu le jour en 2022 :
    • Batteries Powerpack 400, 500, 545, 800 ; Powertube 540, 625, 750, 800
    • Ordinateur de bord Kiox 300 et Intuvia 100

Les dénominations commerciales des moteurs sont restées identiques (Active Line, Active Line Plus, Performance Line, Performance Line CX) même si les moteurs eux-même ont évolué au fil des années. Par conséquent, on peut trouver des moteurs très différents derrière les mêmes appellations.

Tout ce que je dirai dans la suite ne s’applique qu’à la première génération (non Smart System).

On peut aussi supposer que les techniques de détection se sont améliorées avec les mises à jour logicielles. Dans le doute, je ne les ai jamais réalisées. Pour info, ce qui suit a été testé dans les conditions suivantes :

  • Intuvia : matériel V0.0.2.2, logiciel V5.9.2.0
  • Moteur Performance Line Speed Gen2 (BDU390) : matériel V0.0.6.0, logiciel V1.8.6.0

Le début : un bus CAN

Un bus CAN relie la batterie, le moteur et le compteur. Sa vitesse est de 500 kbits/s. Compte tenu de son câblage, on devrait trouver une résistance de terminaison de 120Ω dans la batterie et le compteur, qui correspondent physiquement aux extrémités du bus. En réalité, il y a seulement une résistance de 60Ω dans le moteur. Bon, pourquoi pas. Le câblage est court, cela ne pose pas de problème.

Le moteur s’alimente en 36V par la batterie et fournit 10,8V au compteur. La couleur des fils sur le schéma ci-dessus correspond à la réalité.

Pour se connecter au bus et faire les premiers essais, le moyen le plus simple est de se repiquer sur le port de charge de la batterie :

Grâce à lui, il est possible de récupérer plein d’informations intéressantes normalement inaccessibles, notamment :

  • Tension, courant et puissance délivrés par la batterie,
  • Couple appliqué par le cycliste sur les pédales,
  • Cadence de pédalage,
  • Puissance moteur,
  • Températures batterie et moteur,
  • SOC batterie précis (la console Intuvia donne seulement l’information avec 5 « barres »)
  • etc.

A la base, je voulais simplement connaître la puissance réelle du moteur (et insérer une pince ampèremétrique sur un fil d’alim en roulant n’était pas pratique). J’ai alors eu l’idée de remplacer, via le principe du Man-In-The-Middle, certaines données affichées sur l’écran par d’autres. Par exemple, afficher la puissance instantanée du moteur à la place de la vitesse ou de l’odomètre. Il faut pour cela intercaler un système entre le couple batterie/moteur et l’écran :

En temps normal, le système retransmet toutes les trames sans les modifier. Il est alors transparent et tout fonctionne normalement. Lorsqu’il voit passer une trame contenant la puissance moteur, il la mémorise. Plus tard, lorsqu’il verra passer une trame contenant la distance parcourue, il va l’intercepter et remplacer sa valeur par la puissance mémorisée. Ainsi, le compteur l’affichera à la place de la distance parcourue. Évidemment, les unités ne changeront pas par magie, et la « puissance » restera affichée en kilomètres/heure.

Réalisation matérielle du Man-In-The-Middle

Il faut donc un microcontrôleur avec deux interfaces CAN. Dans la famille STM32, les moins chers sont les STM32F105. Je pensais partir d’une carte Nucleo avec un MCU de cette famille et y ajouter le nécessaire sur du Veroboard, mais je suis tombé par hasard sur ces chinoiseries :

Un STM32F105, deux transceivers CAN, deux régulateurs de tension (probablement 3,3V et 5V)… Bingo, je ne sais pas vraiment à quoi ça sert, mais c’est ce qu’il me faut ! Il y a même un emplacement pour un header au pas de 2,54mm, avec un peu de chance pour l’interface de programmation. Je vais donc partir de cette base matérielle qui me fera gagner beaucoup de temps.

Après quelques recherches, je tombe sur ce site : https://dangerouspayload.com/2020/03/10/hacking-a-mileage-manipulator-can-bus-filter-device. Pas de surprise, il s’agit d’un circuit qui réalise un MITM sur un bus CAN, destiné en l’occurrence à trafiquer l’affichage du compteur kilométrique des voitures.

L’auteur s’est même donné la peine de dessiner le schéma et m’épargne cette étape. Il ne reste donc plus qu’à écrire le soft, mais avant, il faut juste gérer un cas particulier :

Pour allumer le vélo, l’ordinateur de bord (alors alimenté par sa batterie interne pour les Intuvia et Kiox, ou par sa pile CR2032 pour les Purion) écrit sur le bus, ce qui « réveille » la batterie et alimente le système. À noter que si elle est vide, il est toujours possible d’allumer le vélo grâce à un bouton présent sur la batterie elle-même, mais il n’est pas très accessible.

Bref, le MITM n’étant pas sous tension lorsque le vélo est éteint, les demandes d’allumage émanant du compteur ne parviendront plus à la batterie. Pour résoudre le problème, j’ajoute un relais 2RT, avec les contacts normalement fermés câblés de telle sorte à rétablir la continuité du bus lorsque l’alimentation est absente :

En pratique, le câblage ressemble donc à ceci (la bobine du relais n’est pas représentée) :

J’ai dû ajouter une résistance de 120 Ω, sinon aucune résistance de terminaison n’était présente sur le second bus CAN reliant la partie droite du MITM au compteur.

J’ai aussi rajouté des pull-ups entre le µC et le transceiver CAN pour fixer l’état des signaux. Sinon, lorsque le MCU était en reset et son TX par conséquent flottant, le vélo s’allumait occasionnellement sans intervention de ma part. Accessoirement, cela nous montre qu’il n’y a pas besoin d’envoyer une trame CAN valide pour allumer la batterie, une impulsion quelconque suffit.

La bobine du relais est pilotée par un transistor depuis un GPIO pour pouvoir facilement basculer entre le mode « connexion directe » et « MITM » via le soft.

Pour les essais, j’ai voulu garder le circuit accessible et ne pas l’intégrer sous le carter du moteur comme c’est fait habituellement. J’ai donc coupé le câble reliant le moteur au compteur au niveau du guidon. J’ai mis des connecteurs mâle et femelle pour pouvoir facilement rétablir la connexion sans la puce quand je ne m’en sers pas.

C’est fini pour le matériel, on peut commencer le logiciel.

Logiciel du MITM

Les trames transportant les informations essentielles ont déjà été identifiées par la communauté (cf. sources et remerciements à la fin de l’article). Pour commencer, je créé une structure contenant toutes les informations possibles que je visualiserai avec le debugguer.

typedef enum
{
APPUI_AUCUN = 99,
APPUI_TC_PLUS = 0,
APPUI_TC_INFO = 1,
APPUI_TC_MOINS = 2,
APPUI_TC_PIETON = 3,

APPUI_INTUVIA_RESET = 8,
APPUI_INTUVIA_INFO = 9,
APPUI_INTUVIA_PHARE = 10,
APPUI_INTUVIA_ONOFF = 11
} liste_boutons_t;

typedef struct
{
uint32_t uptime; // en s
uint8_t mode_pieton; // <=> booléen indiquant si le mode piéton est actif ou pas
uint8_t phares_actifs; // <=> booléen

uint8_t niveau_assistance; // <=> 0=off, 1=eco, 2=tour, 3=sport, 4=turbo
float couple_pedales; // => en Nm. Couple instantané, peu utilisable car doit être moyenné sur 1 tour de pédale
float couple_pedales_moyen; // version moyennée
uint16_t circonf_roue_min; // en mm
uint16_t circonf_roue_max;
uint16_t circonf_roue_defaut;
uint16_t circonf_roue_actuelle;

float accel_verticale; // Accélération verticale en Nm (proche de 9,81)
float accel_frontale; // Idem entre avant-arrière
float vitesse; // => en km/h
int cadence; // => nb de tours de pédalier par minute

float couple_moteur; // => Couple au niveau du pignon en Nm
float couple_nominal; // => Couple nominal actuel, dépend du mode eco/tour/sport/turbo
uint16_t rpm_moteur; // => ?

float puissance_moteur; // => en W
float puissance_bargraph_max; // => en W, puissance permettant d'avoir le bargraph rempli à fond

float puissance_batterie; // <=> en W
float tension_batterie; // <=> en V
float courant_batterie; // <=> en A
float courant_max_batterie; // => courant de décharge max en A, diminue quand le SOC est bas
uint8_t soc_batterie; // => en %
uint16_t energie_restante; // en Wh. Bizarre, on dirait que c'est calculé avec le SOC et la valeur "théorique"... Une batterie usée chargée à 100% donne les mêmes résultats qu'une batterie neuve également chargée à 100%
float temperature_batterie; // en °C

liste_boutons_t appuis_boutons; // Doit être RAZ par l'applicatif une fois l'appui traité pour pouvoir détecter le suivant
float temperature_moteur; // => En °C

float odometre; // <=> en km
float estimation_autonomie; // <=> en km
int8_t indic_changement_vitesse; // <=> 1, 0 ou -1

// <=> Date/heure
uint8_t rtc_annee;
uint8_t rtc_mois;
uint8_t rtc_jour;
uint8_t rtc_heure;
uint8_t rtc_minute;
uint8_t rtc_seconde;
} etat_velo_t;

Il suffit de la remplir lorsqu’un message intéressant arrive :

/**
* Capture des données au passage d'un message
* La variable globale 'etat_velo' est remplie
* \param id: ID du message CAN
* \param taille: taille du message CAN
* \param *data: pointeur vers les données
*/
void mitm_capturer_message(uint32_t id, uint8_t taille, uint8_t *data)
{
if(id == 0x01C && taille == 8) // Uptime
etat_velo.uptime = (data[3] (data[2]<<8) (data[1]<<16) (data[0]<<24)) / 1000.0;

if(id == 0x037 && taille == 3) // Phares et mode piéton
{
etat_velo.phares_actifs = data[1] & 0x80 ? 1 : 0;
etat_velo.mode_pieton = data[2] & 0x01 ? 1 : 0;
}

else if(id == 0x03B && taille == 4) // Niveau d'assistance
etat_velo.niveau_assistance = data[0] == 9 ? 0 : data[0];

else if(id == 0x048 && taille == 6) // Couple
{
etat_velo.couple_pedales = (int16_t)(data[1] (data[0]<<8)) / 10.0;

// On moyenne le couple car c'est le couple instantané sur les pédales
int const NB_ECHANTILLONS = 300; // 10ms entre échantillon donc 3s
static float tab_echantillons[300] = {0};
static uint16_t i_echantillon = 0;

tab_echantillons[i_echantillon ] = etat_velo.couple_pedales;
if(i_echantillon >= NB_ECHANTILLONS)
i_echantillon = 0;

float moyenne = 0;
for(uint16_t i = 0; i < NB_ECHANTILLONS; i )
moyenne = tab_echantillons[i];
etat_velo.couple_pedales_moyen = moyenne / NB_ECHANTILLONS;
}

else if(id == 0x0C6 && taille == 8) // Circonférence roue
{
etat_velo.circonf_roue_min = data[1] (data[0]<<8);
etat_velo.circonf_roue_max = data[3] (data[2]<<8);
etat_velo.circonf_roue_defaut = data[5] (data[4]<<8);
etat_velo.circonf_roue_actuelle = data[7] (data[6]<<8);
}

else if(id == 0x0C7 && taille == 8) // Energie batterie restante /!\ ne semble pas arriver sur ma batterie...
etat_velo.energie_restante = data[3] (data[2]<<8);

else if(id == 0x0D0 && taille == 4) // Accéléromètre
{
etat_velo.accel_verticale = (data[1] (data[0]<<8)) / 100.0;
etat_velo.accel_frontale = (data[3] (data[2]<<8)) / 100.0;
}

else if(id == 0x0D1 && taille == 2) // Vitesse
etat_velo.vitesse = (data[1] (data[0]<<8)) / 100.0;

else if(id == 0x0D2 && taille == 4) // Cadence de pédalage
etat_velo.cadence = data[1];

else if(id == 0x0D3 && taille == 6) // Couple moteur
{
etat_velo.couple_moteur = (int16_t)(data[1] (data[0]<<8)) / 100.0;
etat_velo.couple_nominal = (data[3] (data[2]<<8)) / 100.0;
etat_velo.rpm_moteur = (data[5] (data[4]<<8)) / 100.0;
}

else if(id == 0x0D4 && taille == 4) // Puissance moteur
{
etat_velo.puissance_moteur = (data[1] (data[0]<<8)) / 10.0;
etat_velo.puissance_bargraph_max = (data[3] (data[2]<<8)) / 10.0;
}

else if(id == 0x101 && taille == 8) // Batterie
{
etat_velo.courant_batterie = (data[3] (data[2]<<8)) / 1000.0;
etat_velo.puissance_batterie = (data[5] (data[4]<<8)) / 10.0;
etat_velo.tension_batterie = (data[7] (data[6]<<8)) / 1000.0;
}

else if(id == 0x111 && taille == 7) // Batterie suite
{
etat_velo.courant_max_batterie = (data[3] (data[2]<<8)) / 1000.0;
//etat_velo.xxxx = data[5] / 10.0; // capacité de la dernière charge complète ?
etat_velo.soc_batterie = data[6];
}

else if(id == 0x131 && taille == 8) // Appuis boutons
etat_velo.appuis_boutons = data[7];

else if(id == 0x170 && taille == 8) // Température moteur
etat_velo.temperature_moteur = (data[1] (data[0]<<8)) / 100.0 - 273.15;

else if(id == 0x202 && taille == 8) // Odomètre / estimation autonomie restante
{
etat_velo.odometre = (data[3] (data[2]<<8) (data[1]<<16) (data[0]<<24)) / 1000.0;
etat_velo.estimation_autonomie = (data[7] (data[6]<<8) (data[5]<<16) (data[4]<<24)) / 1000.0;
}

else if(id == 0x205 && taille == 8) // Indication changement vitesse
{
etat_velo.indic_changement_vitesse = data[1] == 0x9C ? -1 : 0;
etat_velo.indic_changement_vitesse = data[1] == 0x64 ? 1 : 0;
}

else if(id == 0x210 && taille == 6) // Date/heure
{
etat_velo.rtc_annee = data[0];
etat_velo.rtc_mois = data[1];
etat_velo.rtc_jour = data[2];
etat_velo.rtc_heure = data[3];
etat_velo.rtc_minute = data[4];
etat_velo.rtc_seconde = data[5];
}

else if(id == 0x2AA && taille == 8) // Température batterie
etat_velo.temperature_batterie = (data[1] (data[0]<<8)) / 100.0 - 273.15;

else // Autre : message non géré
{
}
}

Pour finir, il ne reste plus qu’à capturer l’information au passage puis la retransmettre de l’autre côté quand un message est reçu :

/**
* Réceptionne en IT un message CAN et le transmet à l'autre bus
* \param hcan : handle du contrôleur sur lequel le message arrive
*/
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
CAN_RxHeaderTypeDef header_canrx;
uint8_t data[8];
CAN_HandleTypeDef *hcan_dest = NULL; // Handle vers l'autre périphérique CAN, sur lequel envoyer le message

// On détermine quel est l'autre périphérique CAN sur lequel renvoyer le paquet
if(hcan == &hcan1)
hcan_dest = &hcan2;
else if(hcan == &hcan2)
hcan_dest = &hcan1;

// On récupère le paquet
HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &header_canrx, data);

// On extrait d'éventuelles données intéressantes à l'intérieur
mitm_capturer_message(header_canrx.StdId, header_canrx.DLC, data, hcan);

// On renvoie le paquet sur l'autre périphérique
// Todo on pourrait ici passer par un buffer circulaire pour être sûr qu'aucun message n'est perdu
CAN_TxHeaderTypeDef msg;
uint32_t TxMailbox;
msg.StdId = header_canrx.StdId;
msg.ExtId = header_canrx.ExtId;
msg.RTR = header_canrx.RTR;
msg.IDE = header_canrx.IDE;
msg.DLC = header_canrx.DLC;
msg.TransmitGlobalTime = DISABLE;

HAL_CAN_AddTxMessage(hcan_dest, &msg, data, &TxMailbox);
}

Le vélo s’allume, fonctionne bien (c’est déjà une petite victoire !) et on récupère comme prévu plein d’infos très intéressantes :

Ci-dessous, quelques constats en vrac :

  • Certains messages CAN transitent dans les deux sens, ce qui est assez étrange. Par exemple, on peut s’attendre à ce que la tension de la batterie provienne uniquement du bus CAN de gauche (sur mon schéma). Pourtant, on voit aussi venir cet ID depuis le bus de droite. Comme si l’ordinateur de bord, après l’avoir reçu de la batterie, le renvoyait.
    Mais ce n’est pas le cas de tous. Par exemple, ce phénomène n’est pas présent pour la vitesse du vélo ou le SOC.
    J’ai mis, dans les commentaires en face de chaque champ de la structure :
    • => si l’ID vient uniquement de la gauche
    • <= s’il vient de la droite
    • <=> s’il circule dans les deux directions.
  • Le moteur transmet seulement la distance totale parcourue par le vélo (odomètre). Le totaliseur journalier (celui que l’on peut remettre à zéro à tout moment) est donc calculé au niveau du compteur via celui-ci : lorsqu’il détecte son incrémentation, il incrémente aussi le journalier.
    Du coup, les affichages que l’on peut pirater sur l’Intuvia ne sont pas très nombreux :
    • L’estimation d’autonomie restante (0-XXX km)
    • L’odomètre (mais le totaliseur journalier part dans les choux, et sature rapidement à 99 999 km)
    • La vitesse
    • L’indicateur suggérant un changement de pignon (une flèche vers le haut, une flèche vers le bas)
  • Je n’ai pas réussi à modifier l’affichage du bargraph de puissance, ainsi que le nombre de barres dans le symbole batterie. Modifier la puissance moteur dans le message 0x0D4 ou le SOC dans le message 0x111 ne suffit pas. Ces infos doivent être transmise dans d’autres trames.
  • Puisque mon vélo est homologué 45 km/h, je ne peux pas éteindre les phares (décret 2007-271). Sur mon Intuvia, l’icône des phares dessinée sur le bouton permettant normalement de les allumer/éteindre a été masquée par un autocollant 😂. L’appui dessus remonte quand même les trames sur le bus.

En tant que preuve de concept, j’ai remplacé :

  • la vitesse par la puissance du moteur divisée par 10
  • l’estimation d’autonomie restante par le SOC de la batterie

Je recycle le bouton des phares qui m’est inutile pour activer ou non le MITM. Voici le résultat :

On voit donc un SOC à 100% (qui me permet de parcourir 26km – ma batterie est un peu usée) et que la puissance maximale du moteur se situe autour de 650W. Il s’agit ici de la puissance électrique absorbée par le moteur et non la puissance mécanique.

Cette puissance n’est obtenue que lorsque la batterie est chargée à fond. Par exemple, lorsque le SOC est aux alentours de 40%, alors la puissance max est plutôt de l’ordre de 550W.

Puce de débridage DIY

Théorie

Finalement, pour débrider un vélo, il suffit de mettre deux Man-In-The-Middle :

  • un pour modifier les informations du capteur de vitesse et faire croire au moteur que la roue tourne moins vite qu’en réalité,
  • un autre pour corriger la vitesse affichée à l’écran et remettre la bonne vitesse.

Et c’est tout… La puce ne retire pas le bridage du moteur à proprement parler !

J’ai quand même voulu vérifier mes hypothèses en fabriquant un prototype.

Mise en oeuvre matérielle

Le capteur de vitesse sur la roue est probablement un simple ILS. Quand on le débranche, l’erreur remontée par Bosch Diagnostic Tool va dans ce sens :

Un coup de multimètre permet de lever tout doute : le contact est polarisé en 3,3V et le courant qui le traverse, lorsqu’il est fermé, est 1mA.

Je vais donc ajouter au circuit précédent une entrée pour brancher l’ILS et un optocoupleur pour le simuler auprès du moteur. Je me connecte sur des ponts de soudure qui permettaient de choisir le comportement du firmware d’origine. J’ajoute aussi deux LEDs, c’est toujours pratique pour le debug :

Physiquement, j’ai tiré un câble 4 conducteurs pour amener les signaux jusqu’au guidon. Sur ces 4 conducteurs, deux sont connectés au capteur, les deux autres à l’entrée idoine du moteur. J’ai monté un connecteur différent de ceux du bus CAN afin qu’il n’y ait pas d’erreur de câblage possible. Lorsque je n’utilise pas la puce, j’installe un pont qui rétablit la continuité du capteur au moteur :

Avec la puce installée :

Pour mettre au point confortablement le soft, il faut pouvoir pédaler en gardant le vélo immobile. Voilà le mieux que j’ai trouvé pour soulever la roue arrière :

Côté logiciel (strict minimum)

Le minimum pour outrepasser la vitesse limite est l’algorithme suivant (les vitesses indiquées ne sont que des exemples, adaptées à bridage initial à 45 km/h) :

En gros, à partir d’une certaine vitesse (lue via l’ILS, dans mon cas 38 km/h), le débridage entre en action et la puce envoie au moteur des impulsions simulées correspondant à une vitesse inférieure (ici 30km/h). En parallèle, le contenu des trames envoyant la vitesse à l’écran est corrigé pour afficher la bonne vitesse.

Pour limiter les risques de détection du débridage, il peut être judicieux de :

  • Ne pas envoyer une vitesse constante pendant trop de temps (cela n’arriverait jamais en pratique). Il faudrait la faire fluctuer un peu.
  • Idéalement, il faudrait même qu’elle soit proportionnelle à la cadence de pédalage (le coefficient peut toutefois changer de temps en temps, cela correspondrait à un changement de pignon sur la cassette).
  • Dans Bosch Diagnostic Tool, le rapport de réduction mini et maxi de la transmission est renseigné et impossible à modifier. C’est le nombre de dents de la cassette divisé par le nombre de dents du pignon moteur. Il faudrait aussi rester dans cette plage.
  • La durée de l’impulsion (correspondant au moment où l’aimant est devant l’ILS) est censée être également proportionnelle à la vitesse à laquelle s’attend le moteur.
  • Un coup d’oscilloscope montre même que le passage de l’aimant devant le capteur génère 2 impulsions successives (et ce n’est pas un rebond). Peut-être faudrait-il également reproduire ce comportement.

En pratique, je n’ai pas pris ces précautions et mon moteur n’est jamais passé en mode dégradé.
Attention, j’ai un moteur de génération 2, qui date de 2019, sur lequel je n’ai jamais fait de mise à jour. Les nouveaux modèles sont peut-être mieux protégés !

Ajout du correctif de l’odomètre

Cet algorithme simpliste permet d’outrepasser la vitesse limite tout en affichant la bonne à l’écran, mais l’odomètre (total et journalier) stocké dans le moteur est sous-estimé puisque le vélo croit qu’il roule plus lentement.

On pourrait stocker dans la mémoire interne de la puce le vrai kilométrage et l’envoyer au compteur, comme c’est fait pour la vitesse. L’inconvénient est que si elle est un jour retirée ou tombe en panne, la correction disparaîtrait et le kilométrage affiché n’aurait plus aucune signification. C’est dommage, il faudrait trouver quelque chose de plus élégant.

Et si, lorsque le vélo est à l’arrêt, on lui faisait croire qu’il roule hyper vite pour récupérer rapidement les kilomètres manquants ? On dépasserait largement la vitesse de bridage, donc l’assistance serait désactivée, mais ce n’est pas gênant puisqu’on est immobile. Pour le vélo, ce serait semblable au cas où on serait dans une grande descente sans pédaler.

L’algorithme devient alors :

Il y a 2 subtilités ici :

  • Quand le vélo est immobile et qu’on envoie rapidement les impulsions manquantes, le moteur croit qu’il roule vite. En mettant du couple sur les pédales pour démarrer, l’assistance ne s’activera donc pas. Il est donc judicieux d’envoyer les impulsions manquantes seulement lorsque le vélo est immobile ET qu’on ne force pas sur les pédales (facile, le moteur renvoie cette info).
  • Comme précédemment, il serait approprié d’envoyer une vitesse qui fluctue légèrement pendant le rattrapage d’impulsions, pour éviter une détection du débridage.
  • Plutôt que d’afficher une vitesse nulle, on pourrait afficher une sorte de décompte a l’avantage d’indiquer la fin de l’opération. C’est ce qui est réalisé sur certaines puces commerciales.

Analyse d’une puce commerciale (Speedbox)

Nous avons donc trouvé une solution permettant de débrider notre vélo, tout en gardant toutes les informations affichées correctes. Il serait intéressant d’analyser le fonctionnement des puces existantes sur le marché, pour savoir si elles fonctionnent exactement de la même manière ou pas.

Évidemment, je n’en possède pas et ne souhaite pas en acheter. On va donc extrapoler quelques informations à partir de la fiche technique d’une puce répandue, la Speedbox :

Cette puce affiche bien la vitesse et l’odomètre réels. Dans le descriptif, il est bien indiqué qu’après un trajet, il faut attendre un certain temps avant d’éteindre le vélo :

Cela laisse supposer que le rattrapage de l’odomètre se fait comme sur la nôtre. Pour inciter leurs clients à bien attendre la fin du décompte (et éviter qu’ils se plaignent sinon que l’odomètre est faussé), rien de tel que de brandir la menace d’une détection par Bosch :

Dans sa gamme Smart System, Bosch a comblé les failles précédentes. A l’heure actuelle (novembre 2025), aucune puce, à ma connaissance, ne permet de débrider ces vélos en conservant l’affichage de la vitesse correcte sur le compteur d’origine :

Speedbox contourne le problème en fournissant une application smartphone qui l’affiche à la place. La distance parcourue est elle correcte, mais seulement une fois le vélo arrêté et les impulsions rattrapées.

Un mot sur le passage en mode dégradé (« limp mode »)

Si un débridage a été détecté dans la durée de vie du produit, cette information est enregistrée irrémédiablement par le moteur et visible depuis Bosch Diagnostic Tool.

Durant certains essais (je l’ai un peu cherché : j’ai simulé des impulsions à vitesse constante pendant l’équivalent de 15 km…), le code d’erreur 503 s’est déclenché :

Contrairement à d’habitude, celui-ci ne pouvait pas être effacé en cliquant sur la croix rouge. Je m’attendais à ce que la ligne « Manipulation » passe définitivement à « Oui », mais ça n’a pas été le cas (bizarre, mais tant mieux pour moi).

Je m’attendais donc à subir le fameux « limp mode » (assistance fortement réduite et nécessité de rouler 90 minutes dans ces conditions pour en sortir). Mais surprise : le comportement est très différent de celui lu sur les forums :

  • À faible cadence de pédalage, la puissance de l’assistance ne semble pas réduite, quoique très « brusque », bien loin de la souplesse et de la fluidité habituelle.
  • Par contre, l’assistance se coupe à partir d’une cadence de pédalage moyenne. La conséquence est que sur le grand pignon, l’assistance se désactive à très faible vitesse (je n’ai plus la valeur en tête) mais en contrepartie… je peux dépasser allégrement les 50 km/h sur le plus petit pignon, car ma cassette offre un développement très long 😂.

Vraiment très étrange. Et je n’ai pas eu le temps de bien investiguer, car tout est revenu à la normale après quelques minutes seulement.

Conclusion

Je suis très étonné par la discordance entre la sophistication apparente du système Bosch et la simplicité que j’ai eue pour le débrider.

Les ingénieurs ont développé le système vraiment élaboré et pointu sur certains aspects (Bosch Diagnostic Tool, mode Dual Battery, authentification de la batterie inviolé jusqu’à très récemment, compatibilité entre TOUS les équipements (batteries, moteurs et compteurs) de leurs VAE pendant plus de 10 ans, etc.) alors que certaines protections élémentaires ont été omises.

En particulier : il y a énormément de trafic sur le bus CAN, et finalement peu de trames ont été décodées. Il serait très simple de détecter la présence d’un MITM : lorsqu’une information circule dans un sens (par exemple, le moteur envoie la vitesse au compteur), le destinataire pourrait renvoyer, noyé dans la masse d’information, un checksum de cette valeur, calculé avec un algorithme secret. Ainsi, l’émetteur pourrait très simplement vérifier si la valeur reçue par le destinataire est bien celle qu’il a envoyé.

J’ai au début cru que cette méthode était employée, car certains messages CAN transitent dans les deux sens. Mais puisque les données sont en clair à l’intérieur, cela n’a aucun intérêt. De plus, on trouve ce phénomène sur certains messages relativement « inutiles », comme les flèches suggérant un changement de rapport de cassette, alors que certains messages « sensibles », comme la vitesse, n’y sont pas soumis.

Remerciements – sources

La majorité du décodage du bus CAN n’est pas de moi. Beaucoup de choses semblent venir initialement de ce thread : https://www.pedelecforum.de/forum/index.php?threads/bosch-active-can-daten-sammlung.40358/page-5 et on été reprises ailleurs.

En vrac, voici certains liens complémentaires :

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *