Aller au contenu
Jboll

OBD II DIY retours d'expérience sur un sniffeur de bus CAN

Messages recommandés

Bonjour,

 

Il y a quelques mois, par curiosité et par envie, j'ai souhaité avoir plus d'informations sur la batterie de ma voiture, et les données provenant de TeslaMate via Tesla ne me convenait pas : des informations manquantes ou agrégées et parfois même extrapolées. A la fin je ne savais même plus si ce que je voyais en face de moi était une donnée mesurée ou calculé. Bref, je me suis mis en quête de trouver un lecteur OBD sur le net pour avoir plus d'informations et surtout des résultats de mesures. Sur le net, ce n'est ce qui manque, il y a plein de solutions, dont le plus célèbre Scan My Tesla. Mais ça ne me convenait toujours pas pour plusieurs raisons:

- la première c'est que je ne sais si les données affichées sont calculées ou lu directement sur le bus (par exemple le SOC ? calculé par SMT ou lu sur le bus ?)

- la seconde c'est la précision de la valeur qui n'est pas donné, par exemple le nominal full pack est donné en kWh, très bien, mais à combien de kWh prés ? sur le bus on ne passe que de petites valeurs de 8 octets max, alors certaines sont multipliées ou divisées par un coefficient qui définie la précision de la valeur.

- la 3eme c'est que je ne voulais pas que l'OBD écrive sur le BUS sans que je le sache. En effet pour avoir des informations sur le bus il y a deux méthodes : soit on attend qu'une donnée soit produite par son producteur, soit on envoie une trame de demande du style "au fait, c'est quoi la valeur de tel machin maintenant ?" et le producteur répond sur le bus (a tout le monde). Evidement je ne voulais pas ça, car je ne voulais donner aucun moyen pour Tesla de détecter cet OBS. Donc juste de l'écoute, un sniffeur.

- enfin c'est que je voulais la capacité de persister ces trames provenant du bus et de pouvoir dépouiller les données a tête reposée, sur mon pc, à la maison :) confort oblige :) 

- la cerise sur le gâteau : être en mesure de superposer des données provenant du bus CAN avec des données de TeslaMate, sur le même graphique !

 

Bref, au final, par curiosité et par envie, j'ai souhaité réaliser mon propre sniffeur de bus CAN pour ma model 3 en me disant que j'allais apprendre pleins de choses sympa, eh bien oui c'est le cas, et j'aimerai vous les partager, pour les curieux comme moi :) Peut-être que parmi vous il y en a qui aimerait se lancer dans cette aventure sans oser, ou tout simplement des curieux de voir comment ça se passe

 

Alors évidement j'ai une idée de ce que j'aimerai avoir comme information et du projet que j'aimerai faire, mais je ne dit pas tout pour l'instant, j'en garde pour plus tard ;)

 

Pour le lecteur de bus CAN, j'ai choisi un lecteur pas cher, mais avec un lecteur de carte SD (histoire de pouvoir mettre les trames lu par le bus dessus) et j'ai bien vérifié les critères : ainsi qu'une carte SD avec une grande capacité et une classe 10 afin d'améliorer sa rapidité d'écriture (mais qui n'a servi a rien non plus car une carte de trop grande capacité n'était pas pris en charge par mon arduino ...)

 

J'ai donc choisi :

OBD-II CAN Bus Basic Dev Kit (ref : 1030007) a 17$ et 5$ de frais de port auprès de Longan. Cette version n'existe plus mais il y en a d'autres https://www.longan-labs.cc/can-bus/obd-ii.html

- Un cable en Y pour connecter cette OBD à la voiture. https://www.amazon.fr/dp/B09MQSPFWP/ref=pe_27091421_487052621_TE_item 

- Une carte SD HC 16Go V10. J'ai plus la référence mais c'est celle là en version 16Go : https://www.amazon.fr/Philips-FM32MP45D-jusquà-microSDHC-Adaptateur/dp/B0B3GHFV7H/ref=sr_1_2?__mk_fr_FR=ÅMÅŽÕÑ&crid=3OZWYP7NQJV08&keywords=sd+philips+16gb+v10&qid=1683300483&s=automotive&sprefix=sd+philips+16+gb+v10%2Cautomotive%2C124&sr=1-2-catcorr

 

Quelques remarques sur ce matériel :

- j'ai choisi un OBD 2 avec une vitesse de bus allant jusqu'à 1Mbit/s. Mais ça n'a servi a rien, vu que la vitesse du bus de nos modèles 3 est de 500 Kbits/sec. Je l'ai découvert en faisant mes essais et sur https://www.racelogic.co.uk/_downloads/vbox/Vehicles/Other/Docs/Tesla-Model 3.pdf

 

- j'ai lu quelque part, j'ai oublié la référence, que la vitesse du bus (1Mb/s, 500kb/s, 250Kb/s etc dépendait de la longueur du bus en mètre, et de mémoire c'était 40 mètres max pour faire du 1Mb/s, ce qui je pense explique pourquoi Tesla utilise une bande passante de 500kb/s ? je ne sais pas, 40 mètres c'est long et cours en même temps. Bref, j'avais lu également que dans les voitures c'est souvent du 500kb/s qui est utilisé 

 

- J'ai dû changer de carte SD, j'avais pris une 64Go initialement, mais j'ai appris plus tard que la librairie pour écrire/lire sur les cartes SD avec un arduino était limité a une certaine capacité... Bref, j'ai pris une 16Go et formatter correctement c'est bon.

 

- La prise en Y est difficile a installer je trouve, l'espacement entre les deux fiches a connecter à la voiture sont trop rapprochées ce qui fait que c'est difficile de tout bourrer dedans comme dans des videos comme celle-ci : 

 
 
Pour l'installation de Arduino IDE, il faut suivre le tuto sur le site du vendeur de l'OBD : https://docs.longan-labs.cc/1030003/
 
 
J'ai eu plusieurs déboires avant d'arriver à uploader un programme dessus et qu'il tourne, mais le fabriquant répond rapidement par mail, c'est toujours appréciable
Le problème était surtout que les numéro de PIN m'était inconnu, et qu'il faille pour ma part appuyer sur le bouton reset de la carte, brancher le cable usb au pc, attendre 2 sec, relaché, et enfin uploader dessus. Sinon il y a un erreur de port COM visiblement sur ARduino IDE 😕 
 
Après quelques essais, j'ai commencé à écrire un programme pour se connecter au BUS CAN et écrire les données sur carte SD. Grosso modo, ça a l'air simple mais en fait il m'a fallu pas mal de temps de mise au point
 
Pour tuer le suspense, voici mon programme dans son intégralité, c'est fou comme il est cours, mais j'en ait fait des aller-retours avec ma voiture : 

#include <SPI.h>
#include <SD.h>
#include <mcp_can.h>
 

#define SPI_CS_PIN  9
#define CAN0_INT 2 
MCP_CAN CAN0(SPI_CS_PIN); 


long unsigned int id;
unsigned char len = 0;
unsigned char rxBuf[8];

 

/* SD card Settings */
char fileName[20];
File dataFile;

 

// Time (4), id(3), len(1), data(max 8  ) = 4+3+1+8 = 16 bytes
byte capsule[16];
uint32_t flushTime = 0;

 

void setup(){
  pinMode(12, OUTPUT);      // OPEN THE POWER SUPPLY
  digitalWrite(12, HIGH);
    
  // init sd card
  if (!SD.begin(5)) {
    return;
  }

  // find next log file name
  for (uint8_t i = 1; i < 10000; i++){
    sprintf(fileName, "DATA%d.log", i);
    if (SD.exists(fileName) == false){
      break;
    }
  }

  // create new log file & append header
  dataFile = SD.open(fileName, FILE_WRITE);
  

  boolean canInit = false;

  while (!canInit) {
    // 500KBPS : https://www.racelogic.co.uk/_downloads/vbox/Vehicles/Other/Docs/Tesla-Model 3.pdf
    // does not work : 20MHz nor 1MBPS
    if (CAN0.begin(MCP_ANY, CAN_500KBPS, MCP_16MHZ) == CAN_OK) {
      canInit = true;
    }
    else {
      dataFile.println("MCP2515 can not be Initialized");
      dataFile.flush();
      delay(1000);
    }
  }

  // listen bus only
  CAN0.setMode(MCP_LISTENONLY);              

  dataFile.println("data format : Time (4 bytes), id (3 bytes), data len (1 bytes), data (0-8 bytes)");
  dataFile.flush();
}

void loop()
{
  while(CAN_OK == CAN0.readMsgBuf(&id, &len, rxBuf)) {
      uint32_t now = millis();
      
      // time
      capsule[0] = (byte)((now >> 24) & 0xFF);
      capsule[1] = (byte)((now >> 16) & 0xFF);
      capsule[2] = (byte)((now >> 8 )& 0xFF);
      capsule[3] = (byte)(now & 0xFF);

      // tesla use only 11 bits, max id see : 0x07FF (2047)
      capsule[4] = (byte)((id >> 16) & 0xFF);
      capsule[5] = (byte)((id >> 8 ) & 0xFF);
      capsule[6] = (byte)(id & 0xFF);

      // data
      capsule[7] = len;
      for (int i=0; i<len; i++) {
        capsule[8 + i] = rxBuf;
      }
    
      // send to internal buffer
      dataFile.write(capsule, 8 + len);

      // write on sd card every 10 sec max
      if (now - flushTime > 10000) {
          dataFile.flush();
          flushTime = now;
      }
  }
}

 

Plusieurs remarques sur ce programme :

- N'ayant pas d'horloge, il n'y a pas de notion de date. on connais juste le temps en milli secondes depuis le démarrage du programme, donc les fichiers sont simplement nommé DATA1.LOG, DATA2.LOG, etc. A noter que la librairie SD limite la casse les noms de fichiers : donc tout est écrit en majuscule

- J'ai essayé avec une fréquence de BUs de 500KBPS : je n'ai reçu aucune trame -> marche pas

- J'ai essayé avec une fréquence de la puce a 20MHz : je n'ai reçu aucune trame -> marche pas

- J'ai essayé avec une fréquence de la puce a 8MHz (dans le doute...) : je n'ai reçu aucune trame -> marche pas

- Si j'appelle pas la méthode flush() sur le fichier, alors l'écriture ne se fait pas et mon fichier n'a que l'entête de début de fichier, c'est pourquoi toutes les 10secondes je force un flush(). Mais en contre partie, ça veut dire que je vais être aveugle toutes les 10 secondes aux trames passant sur le BUS, vu que pendant ce temps, le programme écrit sur la carte SD. (il n'y a pas de multi thread). Sauf qu'en réalité ce sera pas exactement le cas, j'expliquerai pourquoi plus tard

- J'avais idée d'utiliser plutôt les interruptions (comme ici : https://www.locoduino.org/spip.php?article148), mais j'ai pas concrétisé cette idée, j'expliquerai plus tard pourquoi

- J'ai voulu avoir le temps précis à la milliseconde pour chaque message

- Pas de conversion en chaine de caractères, tout est stocker en binaire pour éviter de perte du temps et risquer de perdre des messages

- J'ai réduit a 3 bytes la place nécessaire pour stocker l'identifiant du message après avoir fait des tests et vu que Tesla n'utilisait finalement que 11bits

 

Une fois les données collectés sur ma carte SD, transféré sur mon pc, j'ai créé le programme qui lit ces trames 
 
Grosso modo, c'est assez simple, en java ça fait ça :
final FileInputStream fileInputStream = new FileInputStream("../MyObd2/data/DATA4.LOG");
final DataInputStream d = new DataInputStream(new BufferedInputStream(fileInputStream, 1000000));
final String dataFormat = d.readLine(); // data format : Time (4 bytes), id (3 bytes), data len (1 bytes), data (0-8 bytes)
while (d.available() > 0) {
   int now = d.readInt();
   final int messageId = (d.readByte() << 16 & 0x00FF0000) | (d.readByte() << 8 & 0x0000FF00) | (d.readByte() & 0x000000FF);
   final byte len = d.readByte();
   final byte[] data = d.readNBytes(len);
   
   ...
}
 
Facile hein ;) 
 
Au passage, je sniffe environ 1300 messages à la secondes, l'identifiant max que j'ai vu c'est 2047
 
Avant de passer au décodage des messages, j'en ai profité pour déterminer si j'arrivais a sniffer tout les messages ou si des messages était perdu entre temps. Alors comment faire ? comment savoir qu'on a pas reçu un message ? pas facile hein ;) vu qu'on l'a pas reçu ...
 
Pour savoir, dans un premier temps j'ai juste compter le nombre de fois que je passe dans ma loop de mon programme arduino et je compte le nombre de messages que j'ai stocké sur ma carte SD. Résultat : bonne nouvelle, il passait quasiment 2 fois plus souvent dans ma loop que du nombre de message. Rassurant. ça veut dire que 50% du temps il regarde s'il y a un nouveau message (dans un des deux buffers en interne à la librairie) et il n'y a pas de nouveau message. Alors il attend. 
 
Oui sauf que il est possible que 3 messages arrivent très vite les uns derrière les autres et dans ce cas il y a des pertes non ?
Pour savoir si c'est le cas, j'ai utilisé une autre méthode : je me suis dit qu'il devait y avoir des messages périodiques, c'est à dire des messages qui sont émis tout les temps de temps, et donc ce serait facile de savoir si j'en perd un : il y aurait un trou dans le graphique !
 
Bon ben du coup j'ai un graphique a créer... alors c'est parti :
J'ai installé un InfluxDB (un apt-get install sur un serveur qui trainait dans le coin) et j'ai fais quelques lignes pour créer une database avec ces messages :
 
       InfluxDB influxDB = InfluxDBFactory.connect("http://houston:8086");
//    influxDB.deleteDatabase("myobd");
//    influxDB.createDatabase("myobd");
//    influxDB.createRetentionPolicy("defaultPolicy", "myobd", "300000000d", 1, true);
      influxDB.setDatabase("myobd");
      influxDB.setRetentionPolicy("defaultPolicy");
 
Puis envoie :
Point point = Point.measurement("message")
      .time(startTime + now, TimeUnit.MILLISECONDS)
      .addField("messageId", messageId)
      .tag("index", String.valueOf(messagesCount))
      .build();

influxDB.write(point);
A noter que j'ai rajouté artificiellement une date de démarrage, vu que mon arduino n'a pas d'horloge. J'ai mis une startTime qui semblait correspondre a peu prés au moment ou le programme a dû commencer à s'exécuter (on est pas à la seconde prés ici)
 
Une fois toutes les valeurs mises dedans, au passage j'en profite pour dire que j'ai dû rajouter un "tag" lors de la création du point ci-dessus, parce que influxDB ne permet pas d'écrire plusieurs points ayant la même date. Pas de chance.... alors j'ai trouvé cette astuce (perfectible bien sûr) pour écrire plusieurs points avec la même date... astuce vu sur un forum
 
Aprés j'ai rajouté un graph dans le grafana de mon TeslaMate :
image.thumb.png.47e95a1193f2eaed3e9d34da52714525.png
 
On peux voir qu'il y a des endroits de creux, j'imagine que c'est à ces moments que l'écriture sur la carte est faite (il y a un buffer de 512 octects) j'ai pas vérifié la corrélation par contre. 
 
 
 
 
Si je zoom, on voit bien le trou :
image.thumb.png.c660d3fd855b2dda80d5023a8edf5a93.png
 
 
... de 2ms ! pas si énorme que ça en fin de compte, mais il y a une perte tout de même.
 
Maintenant, trouvons un message qui se répète, et pas n'importe lequel ! il faut un message qui ait une fréquence rapide, et qui a un id (en ordonné sur le graphique) très bas, car les messages avec des id bas préempte les messages avec des ids plus grand. (donc les messages important ont un id bas)
Par contre, comme le bus i2c, le bus CAN est un bus sans perte ! c'est à dire que si deux écrivains écrivent leurs trame en même temps, alors celui avec le plus faible ID va écraser l'autre (une mise à la masse, donc un 0, remportera un bit 1 qui serait émit au même moment) et donc c'est le message avec l'id le plus faible qui va passer. Le second écrivain verra qu'il n'arrive pas écrire son message et donc tentera de l'émettre à nouveau juste après). 
Donc si je trouve un message émit régulièrement, il peux être éventuellement décalé, mais il doit y avoir un point !
 
Et voici mon choix, le 306 :
image.thumb.png.64e46efd1234a4fa985ef73c78919266.png
 
il est émis toutes les 10 ms, tout de même !
 
Bon clairement il y a des trous
image.thumb.png.0e69c835e4f3a8d39ccbb45457d0c87b.png
 
 
 
Si j'ajoute ceux qui sont prioritaire par rapport à lui :
image.thumb.png.4ed648f14b9e19f9c5a4b5db8dc59590.png
 
C'est bizarre cette histoire, parce que si un message est préempté, il devrait être décalé, mais c'est pas le cas ici, il y a un vide, et pas un décalage
Et si c'est l'écriture sur carte SD qui aveugle les nouveaux messages, il ne devrait pas avoir de points verts dans certains trou jaune, ce qui est pourtant le cas
Et puis l'écriture sur carte SD, je pense que c'est plus les pause de 2 ms régulièrements
 
Mais alors ou sont passé les 306 manquants ?
Sont-ils au moins émis ?
 
Pour tester, j'ai fais un autre petit programme, mais au lieu d'écrire sur carte chaque message, j'ai décidé de uniquement me focaliser sur les 306, de les compter un à un, jusqu'à recevoir 1000 messages et mesure le temps qu'il faut que je les reçoivent.A raison de 10 ms entre deux messages, ça fait donc 10 secondes. et j'écris sur carte SD uniquement ce delta de temps 
 
while(CAN_OK == CAN0.readMsgBuf(&id, &len, rxBuf)) {
     // track 10 ms signal, meaning 1000 messages received in 10000ms
     if (id == 306) {
       if (count306 == 0) {
          count306StartTime = millis();
       }
       count306++;
       if (count306 == 1000) {
         // 10 sec ?
         dataFile.print("count 1000 messages 306 in ");
         uint32_t now = millis();
         dataFile.print(now - count306StartTime);
         dataFile.println(" ms");
         dataFile.flush();

         count306 = 0;
       }
     }
   }
}
 
Résultat :
data format : Time (4 bytes), id (3 bytes), data len (1 bytes), data (0-8 bytes). No Filter, 500 KBPS, 16 MHz
count 1000 messages 306 in 12653 ms
count 1000 messages 306 in 12105 ms
count 1000 messages 306 in 12834 ms
count 1000 messages 306 in 16305 ms
count 1000 messages 306 in 11813 ms
count 1000 messages 306 in 11374 ms
 
ça c'est surprenant ! il me faut une durée variable de temps pour compter jusqu'à 10000. C'est pas normal
et puis clairement il y a des pertes de paquets, pour le 1er cas, 12653ms, je m'attendais à avoir 1265 messages, mais j'en ai reçu que 1000, soit une perte de 26.5% tout de même !
 
Mais comment expliquer cette perte ?
Clairement ce n'est pas ma carte SD vu qu'elle n'est pas sollicitée entre deux mesures
Et comme le programme fait des loop à vide, c'est sans doute pas le cpu du microcontrôleur non plus
Finalement, c'est peut-être qu'il n'a pas été émis, mais ce serait bizzare
Ou qu'il y a des erreurs sur le bus
Ou que le mécanisme de ré-emission ne marche pas mais ce serait bizzare
Bref, je n'ai pas la réponse, tout du moins pour l'instant (désolé)
 
Pour ma part, si je loupe quelques message ce n'est pas grave dans le cas d'usage
 
Passons au gros morceau sympa, le décodage des messages :)
 
Bon alors clairement, ici c'est le bordel ! (désolé pour les âmes sensibles)
 
Tesla ne fournit pas de documentations sur ces messages, donc c'est uniquement du reverse engineering que l'on trouve. ça veut dire : tous les messages sont loin d'être décodé (sur les 300 types de messages j'ai pu en décoder que 108) et selon les mise à jour logiciels ça peut remettre en cause les décodages précédents. Bref un gros boulot très technique et très compliqué, dont la référence a connaitre est ce forum : https://www.teslaownersonline.com/threads/diagnostic-port-and-data-access.7502/page-106 ou plein de personnes plus motivé que jamais essayent de décortiquer le machin avec en tête Josh Wardell. Bravo a cette équipe.
 
Exemple de reverse engineering qu'ils ont fait :
AKZxkHJ.png
 
ça vaut le coup d'aller voir ;)
 
Bref, sur son Git Hub, Josh a compilé un .dbc qui explique tous les messages qu'ils ont découvert :
 
Malheureusement il n'est pas à jour, mais il y a tout de même un bon paquet de message qui reste encore bon dedans
 
Oui mais c'est quoi un dbc et comment ça se lit ?
Alors là, ça va faire mal ! 
Je me suis arraché pas mal de cheveux là-dessus, oui parce qu'il y a 50% des sites qui disent n'importe quoi (j'exagère un peu)
Par exemple, dans ce fichier on trouve ça :
 
BO_ 12 ID00CUI_status: 8 VehicleBus
  SG_ UI_audioActive : 1|1@1+ (1,0) [0|1] "" Receiver   SG_ UI_autopilotTrial : 12|2@1+ (1,0) [0|3] "" Receiver   SG_ UI_bluetoothActive : 2|1@1+ (1,0) [0|1] "" Receiver
 
 
J1939 EEC1 EngSpeed RPM CAN DBC File Database Format Message Signal
 
Le BO indique comment décoder un message, le 12 c'est à dire avec l'id 12, le nom du message ID00CUI_status, qui comprend 8 bytes
Dans ce message il y a plusieurs informations dedans, que l'on appel des signaux, chaque ligne qui commence par SG définit un signal
Par exemple le premier veut dire que le nom du signal est UI_audioActive, qui commence au bit1 et à 1bit en little endian de longueur 1 de scale et 0 d'offset d'une valeur min de 0 et de max 1 et l'unité n'existe pas
 
ça a l'air simple comme ça, mais le problème c'est le StartBit qui pose problème et le sens de lecture. Franchement beaucoup de site se plante quand il s'agit de l'expliquer, et d'ailleurs même le document de référence pour le décodage des fichiers dbc se trompe entre @0 et @1 :
 
0=little endian, 1=big endian
 
Endianness: 0 = big endian (most significant byte first), 1 = little endian (least significant byte first) (Note: the DBC format documentation is wrong on this)
 
Bref, en deux mots : @1 ça veut dire little endian, et contrairement a ce qu'on peut lire partout, le startBit se lit en dents de scie et pas de gauche à doite comme le prétendent certains site
 
Autrement dit, le signal :
SG_ Exemple : 5|12@1+ (42,23) [52|96] "Km"
 
se décode comme ça :
image.png.910eb2c140849918f336482cb02a39ab.png
 
on lit a partir du bit 5, c'est à dire la valeur "l" (évidement c'est des 0 et des 1 qu'il y a dedans et pas des lettres, j'ai mis des lettres parceque je trouve que c'est plus facile a comprendre) et on lit ensuite le bit 6, qui est à GAUCHE du bit 5, puis le bit 7, puis le bit 8 à l'autre bout, puis le 9 etc.
Bref, à la fin on lit la valeur : abcdefghijkl
Que l'on convertit ensuite en décimal et que l'on appelle rawValue 
pour avoir la valeur physique il faut alors faire rawValue * scale + offset pour obtenir la valeur en Km
Evidement il faut que cette valeur soit inclus entre [52 et 96] inclus, sinon la valeur n'est pas bonne -> a rejeter
 
Pour ma part, j'ai fais ce code là en Java
class BitReader {
   final byte[] data;
   final int dataCount;

   BitReader(byte[] data, int dataCount) {
      this.data = data;
      this.dataCount = dataCount;
   }

   boolean readBit(int index) {
      final int dataIndex = index / 8;
      final int bitIndex = index % 8;

      if (dataIndex >= dataCount) {
         throw new IllegalStateException("buffer overflow");
      }

      final byte dataByte = this.data[dataIndex];

      // signed
      switch (bitIndex) {
         case 0:
            return (dataByte & 0b0000_0001) != 0;
         case 1:
            return (dataByte & 0b0000_0010) != 0;
         case 2:
            return (dataByte & 0b0000_0100) != 0;
         case 3:
            return (dataByte & 0b0000_1000) != 0;
         case 4:
            return (dataByte & 0b0001_0000) != 0;
         case 5:
            return (dataByte & 0b0010_0000) != 0;
         case 6:
            return (dataByte & 0b0100_0000) != 0;
         case 7:
            return (dataByte & 0b1000_0000) != 0;
      }
      throw new IllegalStateException("bitIndex shall not be > 7");
   }

   long readBits(int startBit, int bitLen) {
      long res = 0;

      for (int i = 0; i < bitLen; i++) {
         int currentBitIndex = startBit + i;
         final boolean bitValue = readBit(currentBitIndex);
         if (bitValue) {
            res |= 1L << i;
         }
      }

      return res;
   }


   public OptionalDouble readDouble(int startBit, int bitLen, boolean signed, double scale, double offset, double min, double max) {
      if (startBit + bitLen > dataCount * 8) {
         return OptionalDouble.empty();
      }

      long rawValue = readBits(startBit, bitLen);
      if (signed) {
         long msbMask = 1L << (bitLen - 1);
         rawValue = (rawValue ^ msbMask) - msbMask;
      }

      double physicalValue = (rawValue * scale) + offset;
      if ((min <= physicalValue && physicalValue <= max) || (max == min && min == 0)) {
         return OptionalDouble.of(physicalValue);
      } else {
         return OptionalDouble.empty();
      }
   }
}
record Message(long messageId, String messageName, int bytesCounts, List<Signal> signals) {
}
record Signal(String signalId, int startBit, int bitLen, boolean signed, double scale, double offset, double min,
           double max, String unit, boolean multiplexedKey, OptionalInt multiplexedValue,
           Map<Integer, String> format) {

}
 
Bon, maintenant qu'on sait comment décoder, il faut lire le fichier dbc, le parser, puis décoder les messages et les envoyé dans la base de donnée inluxDB...
 
Alors j'ai fais un parseur de fichier dbc
public class BcdReader {

   public Map<Integer, Message> read(File file) throws IOException {
      Map<Integer, Message> messages = new HashMap<>();
      Message currentMessage = null;

      for (String line : Files.readAllLines(file.toPath())) {
         // BO_ 12 ID00CUI_status: 8 VehicleBus
         if (line.startsWith("BO_")) {
            final String[] words = line.split(" ");
            int messageId = Integer.parseInt(words[1]);
            final String messageName = words[2].replace(":", "");
            int bytesCounts = Integer.parseInt(words[3]);

            currentMessage = new Message(messageId, messageName, bytesCounts, new ArrayList<>());
            messages.put(messageId, currentMessage);
         }

         // SG_ UI_cellReceiverPower : 24|8@1+ (1,-128) [-128|127] "dB"  Receiver
         // SG_ UI_audioActive : 1|1@1+ (1,0) [0|1] "" Receiver
         // SG_ UI_autopilotControlIndex M : 0|3@1+ (1,0) [0|7] ""  Receiver
         // SG_ UI_apmv3Branch m1 : 40|3@1+ (1,0) [0|5] ""  Receiver
         if (line.startsWith(" SG_")) {
            final String[] words = line.trim().split(" ");

            String signalId = words[1];

            boolean multiplexedKey = false;
            OptionalInt multiplexedValue = OptionalInt.empty();

            int readIndex = 3;

            if (words[2].equals("M")) {
               multiplexedKey = true;
               readIndex++;
            }
            if (words[2].startsWith("m")) {
               multiplexedValue = OptionalInt.of(Integer.parseInt(words[2].replace("m", "")));
               readIndex++;
            }


            final int startBit = Integer.parseInt(words[readIndex].split("\\|")[0]);
            final int bitLen = Integer.parseInt(words[readIndex].split("\\|")[1].split("@")[0]);
            final boolean signed = words[readIndex].endsWith("+");
            readIndex++;

            final double scale = Double.parseDouble(words[readIndex].split(",")[0].replace("(", ""));
            final double offset = Double.parseDouble(words[readIndex].split(",")[1].replace(")", ""));
            readIndex++;

            double min = Double.parseDouble(words[readIndex].split("\\|")[0].replace("[", ""));
            double max = Double.parseDouble(words[readIndex].split("\\|")[1].replace("]", ""));
            readIndex++;

            final String unit = words[readIndex].replace("\"", "");

            Signal signal = new Signal(signalId, startBit, bitLen, signed, scale, offset, min, max, unit, multiplexedKey, multiplexedValue, new HashMap<>());
            currentMessage.signals().add(signal);
         }

         // VAL_ 777 DAS_leftVeh2Type 4 "BICYCLE" 2 "CAR" 3 "MOTORCYCLE" 5 "PEDESTRIAN" 1 "TRUCK" 0 "UNKNOWN" ;
         if (line.startsWith("VAL_")) {
            final String[] words = line.trim().split(" ");
            final int messageId = Integer.parseInt(words[1]);
            final String signalId = words[2];

            Signal foundSignal = null;
            final Message message = messages.get(messageId);
            for (Signal signal : message.signals()) {
               if (signal.signalId().equals(signalId)) {
                  foundSignal = signal;
                  break;
               }
            }

            final String[] words2 = line.substring(line.indexOf(signalId) + signalId.length()).split("\"");
            for (int i = 0; i < words2.length - 1; i = i + 2) {
               final int key = Integer.parseInt(words2[i].trim());
               final String value = words2[i + 1].replaceAll("\"", "").trim();

               foundSignal.format().put(key, value);
            }
         }

      }

      return messages;
   }
}
 
Bref, on met tout ça dans influDB, et on obtient quelque chose comme ça pour un trajet :
image.thumb.png.4da4ae08a5bcf5394fd55ee74042492e.png
 
 
A noter que la bonne manière de décoder le message 850 concernant les informations du BMS, c'est maintenant de la manière suivante :
BO_ 850 ID352BMS_energyStatus: 8 VehicleBus
 SG_ BMS_energyStatusMultiplexer M : 0|2@1+ (1,0) [0|3] "" VehicleBus
 SG_ BMS_nominalFullPackEnergy m0 : 16|16@1+ (0.02,0) [0|1310.7] "kWh" VehicleBus
 SG_ BMS_nominalEnergyRemaining m0 : 32|16@1+ (0.02,0) [0|1310.7] "kWh" VehicleBus
 SG_ BMS_idealEnergyRemaining m0 : 48|16@1+ (0.02,0) [0|1310.7] "kWh" VehicleBus
 SG_ BMS_fullChargeComplete m1 : 15|1@1+ (1,0) [0|1] "" VehicleBus
 SG_ BMS_energyBuffer m1 : 16|16@1+ (0.01,0) [0|655.35] "kWh" VehicleBus
 SG_ BMS_expectedEnergyRemaining m1 : 32|16@1+ (0.02,0) [0|1310.7] "kWh" VehicleBus
 SG_ BMS_energyToChargeComplete m1 : 48|16@1+ (0.02,0) [0|1310.7] "kWh" VehicleBus
(Encore merci au topic de Josh)
 
Et pour le décodage des tensions des cellules, c'est pas encore clair, mais le gars de SMT a posté sur le forum cet algo (que j'ai traduit en java) :
if (messageId == 1025) {
   int cell = 0;
   double v = 0;

   for (int i = 0; i < 3; i++) {
      v = ((data[i*2+3]<<8) + data[i*2+2])/10000.0;
      if (v < 0) {
         double c = (( data[0])*3+i+1);
         System.err.println("cell id "+c + " value : " + v + " volt ");
      }
   }
}
Et j'ai l'impression que c'est mieux que celui de Josh, à savoir :
BO_ 1025 ID401BrickVoltages: 8 VehicleBus
 SG_ MultiplexSelector M : 0|8@1+ (1,0) [0|0] ""  Receiver
 SG_ StatusFlags : 8|8@1+ (1,0) [0|0] ""  Receiver
 SG_ Brick0 m0 : 16|16@1+ (0.0001,0) [0|0] "V"  Receiver
 SG_ Brick1 m0 : 32|16@1+ (0.0001,0) [0|0] "V"  Receiver
 SG_ Brick2 m0 : 48|16@1+ (0.0001,0) [0|0] "V"  Receiver
 SG_ Brick3 m1 : 16|16@1+ (0.0001,0) [0|0] "V"  Receiver
 SG_ Brick4 m1 : 32|16@1+ (0.0001,0) [0|0] "V"  Receiver
 SG_ Brick5 m1 : 48|16@1+ (0.0001,0) [0|0] "V"  Receiver
 SG_ Brick6 m2 : 16|16@1+ (0.0001,0) [0|0] "V"  Receiver
 SG_ Brick7 m2 : 32|16@1+ (0.0001,0) [0|0] "V"  Receiver
 SG_ Brick8 m2 : 48|16@1+ (0.0001,0) [0|0] "V"  Receiver
 SG_ Brick9 m3 : 16|16@1+ (0.0001,0) [0|0] "V"  Receiver
 SG_ Brick10 m3 : 32|16@1+ (0.0001,0) [0|0] "V"  Receiver
 SG_ Brick11 m3 : 48|16@1+ (0.0001,0) [0|0] "V"  Receiver
 SG_ Brick12 m4 : 16|16@1+ (0.0001,0) [0|0] "V"  Receiver
 SG_ Brick13 m4 : 32|16@1+ (0.0001,0) [0|0] "V"  Receiver
 SG_ Brick14 m4 : 48|16@1+ (0.0001,0) [0|0] "V"  Receiver
 SG_ Brick15 m5 : 16|16@1+ (0.0001,0) [0|0] "V"  Receiver
 
Bref, je poursuis mes investigations, je voulais faire un post, histoire que j'oublie pas tout, et que si certaines personnes ont autant galéré que moi, qu'ils puissent savoir comment j'ai fais. Si vous avez des questions n'hésitez pas je vous répondrez volontiers, Je mettrai peut-être mon code sur github à l'occasion, et je ferai sans doute un post plus tard pour l'exploitation des données, j'ai déjà des infos croustillantes, mais je mettrait ça sans doute dans le topic associé. A voir...
 
 
Annexe :
quelques liens utile en vrac' :

https://github.com/DieselDuz42/Arduino-CAN-bus-SD-logger/blob/master/canbus-logger.ino
https://raw.githubusercontent.com/DieselDuz42/Arduino-CAN-bus-SD-logger/master/Example Logs/35516-042417.TXT
https://github.com/DieselDuz42/Arduino-CAN-bus-SD-logger

forum 
https://forum.arduino.cc/t/can-bus-traffic-logger/222157

https://github.com/akpc806a/CAN_Logger/tree/master/Firmware/IAR

// can logger with buffer:
https://github.com/akpc806a/CAN_Logger/blob/master/Firmware/ChibiStudio/CAN-Logger/main.c


// can avec interruption
https://www.locoduino.org/spip.php?article148

dbc data base
https://github.com/joshwardell/model3dbc/blob/master/Model3CAN.dbc
https://docs.google.com/spreadsheets/d/1ijvNE4lU9Xoruvcg5AhUNLKr7xYyHcxa8YSkTxAERUw/edit#gid=0
https://github.com/nmullaney/candash/blob/main/android/app/src/main/java/app/candash/cluster/CANSignalHelper.kt#L128

dbc sheet
https://docs.openvehicles.com/en/latest/components/vehicle_dbc/docs/dbc-primer.html
https://www.csselectronics.com/pages/can-dbc-file-database-intro

https://www.scanmytesla.com/

https://www.longan-labs.cc/1030003.html
http://docs.longan-labs.cc/1030003/#arduino-ide-setup-for-v2-rp2040-version
https://www.longan-labs.cc/1030002.html
https://www.longan-labs.cc/blog/post/can-bus-products-selection-guide/
https://github.com/Longan-Labs

 
 

 

Modifié par Jboll
correction dans le code

Partager ce message


Lien à poster
Partager sur d’autres sites

Bravo et courage.

Tu as mis le doigt dans un engrenage...

J'y ai mis le mien en 2010 avec l'achat de ma Prius, et mon désir de consommer le moins possible, puis pour les Rallyes.

 

J'ai fais cela avec Priusfan et d'autres, avec un mbed et pas un arduino, et pour la Prius, puis la eGolf, puis la DS3 Etense qui ont été mes voitures pour les Rallyes de Monte-Carlo Energies Nouvelles, qui maintenant sont de e-Rallyes. J'ai utilisé mon mbed pour la Tesla, puis j'ai trouvé le site de Josh, puis son canserver, que j'ai acheté. J'aurais du en prendre plusieurs.

 

Quelques remarques :

il est préférable de prendre un "connecteur" obd qui permette de filtrer les trames que tu veux lire (c'est ce que nous avions fait avec le mbed) sinon il te sera très difficile de ne pas perdre des trames. Maintenant ton arduino et ton "connecteur" y arrive peut-être.

 

Le fichier dbc n'est effectivement plus à jour, mais tu peux poser des questions dans une discussion Discord de Josh et des autres devs dont SMT. Si tu ne l'as pas trouvée, je te donnerai le lien en MP.

 

Effectivement, il vaut mieux eviter d'écrire si tu ne veux pas transformer ta voiture en un sapin de Noel. Cela m'est arrivé plus d'une fois sur ma Prius, mais j'avais mon maitre, Priusfan, à mon secours. C'est pourquoi je déconseille fortement le connecteur de Sexy boutons, qui lui permet d'écrire, et le fait. C'est d'autant plus dangereux sur la Tesla que Tesla change les messages quand il veut. Et il le fait. sur la Prius, nous avions utilisé l'écriture pour activer au max le ventilateur du refroidissement de la batterie car dès quelle atteignait 50°, plus de regen.

 

C'est beaucoup moins dangereux les requetes de lecture. C'est en général utilisé par les outils de diagnostic de constructeur. Il est probable que le menu Service utilise ces trames pour avár des informations complémentaires. Si tu sniffes les traces, tu verras passer des requètes d'interrogations puis les trames de réponse. Il y en avait sur la Prius, et nous les utilisions. Effectivement, il y a un risque si tu les utilises sur la Tesla, que Tesla detecte ta requete, mais c'est peu probable. Je ne connais pas de requetes pour la Tesla. Josh s'interdit d'écrire sur le bus, et le canterver ne le permet pas.

 

Cela peut être embétant de perdre des trames, car certaines valeurs doivent être cumulées. Par exemple sur la Prius, nous avions plusieurs façon d'avoir la distance dont une où dans la trame il nous indiquait la quantité avec la précedente trame. Dans ce cas tu perdais de la distance. Parfois d'autres valeurs sont circulaires: 1 à 256 puis 1 à 256 ; si tu rates un cycle, tu as perdu 256 de quelque chose. C'était le cas sur la eGolf et sur la DS3. Je n'ai pas encore eu ces cas avec les trames de la Tesla qui m'intéressaient.

 

Pour la date et heure, il y a une trame qui contient ces valeurs et transmises par la Tesla. Ainsi je pense une base de temps en entier. Il faut que j'aille regarder sur le canserver. J'essaierai de te donner ça.

 

En général l'écart temporel entre deux trames du meme pid est constant. Pour l'instant je n'ai pas encore vu de voiture qui ne respectait pas cela. Mais comme tu es sérieux, et que tu dis le contraire, tu me mets un doute.

 

Quel casse-tête l'encodage des trames en little indian. Mais bravo, tu y es arrivé. J'ai bien cru que je n'y arriverai pas à temps pour aller faire les reconnaisances avec ma Tesla avant le e-Rallye de Monte-Carlo l'année dernière. Je ne suis finalement même pas sur que j'ai compris, mais cela marche. Je relirai ce que tu as écrit pour terminer de comprendre.

 

Ecrire sur une carte est en général très long comparé à la vitesse des trames. Nous avions préféré avec Priusfan les transmettre immediatement en bluetooth ou en filaire (port com) à un PC qui lui se chargeait de stocker. Comme toi, nous les comptions, des deux coté, pour vérifier qu'il n'en manquait pas dans nos phases de mise au point. Et s'il y avait des risques d'en perdre, alors nous faisions pour celles qu'il ne fallait pas perdre les calculs de somme ou de modulo-reverse sur le mbed, et ajoutions des fausses trames qui étaient envoyées au PC et contenaient ces données.

 

Bravo et bon courage.

Partager ce message


Lien à poster
Partager sur d’autres sites

Merci @tben pour ton message d’encouragement et bienveillant

 

 coïncidences, hier soir après mon post, j’ai trouvé le message qui contient la date, pratique ça !

https://www.teslaownersonline.com/threads/diagnostic-port-and-data-access.7502/page-13
 

pour le filtrage : oui c’est possible de filtrer en amont, c’est à dire qu’il y a une api arduino pour le faire qui, j’imagine, est plus performant que le code que j’ai essayé. Je vais tenter ça pour voir si je retrouve les paquets manquants

 

 je vais aussi compter le nombre d’erreur que la carte vois sur le bus, au cas où (il y a aussi une api pour ça)

 

 merci pour ton retour d’expérience

Partager ce message


Lien à poster
Partager sur d’autres sites

Le 06/05/2023 à 08:43, Jboll a dit :

pour le filtrage : oui c’est possible de filtrer en amont, c’est à dire qu’il y a une api arduino pour le faire qui, j’imagine, est plus performant que le code que j’ai essayé. Je vais tenter ça pour voir si je retrouve les paquets manquants

Ce n'est pas trop que ton code n'est pas performant, je ne l'ai pas lu, et avant de mettre au point le mien sur le mbed, je ne savais pas ce qu'il faire. C'est un équilibre entre lecture, calcul, écriture. Tu as resolu le pb du calcul, tu as raison entre autre de ne rien transformer sur l'arduino. Une idée, plutôt que de vouloir tout écrire sur la carte, n'écrit qu'un seul pid. Mais le mieux est de transmettre, et stocker à l'autre bout. Et filtre sur le connecteur, ce sera une aide non négligeable aussi.

 

En étant moins gourmand tu sauras être sur de ce qu'il se passe, puis tu pourras augmenter le nombre de données que ton connecteur lit, le nombre de données calculées, le nombre de données transmises, le nombre de données stockées localement, tout en vérifiant par comparaison avec le traitement le plus léger (un pid que tu lis et transmets, que tu n'as rien perdu. Vas y lentement. Même le canserver je n'arrive pas à avoir plus que quelques (entre 30 et 70) données lu et transmises.

Le 06/05/2023 à 08:43, Jboll a dit :

coïncidences, hier soir après mon post, j’ai trouvé le message qui contient la date, pratique ça !

https://www.teslaownersonline.com/threads/diagnostic-port-and-data-access.7502/page-13

Cool, bravo, n'oublie pas que cela a pu changer depuis le moment où les posts ont été écrits.

 

Le 06/05/2023 à 08:43, Jboll a dit :

ton message d’encouragement

c'est interessé ;-) comme cela je ne serai plus tout seul. Encore que je ne suis pas seul, j'ai toujours eu de l'assistance de l'équipe autour de Josh.

Partager ce message


Lien à poster
Partager sur d’autres sites

Le 06/05/2023 à 09:13, tben a dit :

Ce n'est pas trop que ton code n'est pas performant, je ne l'ai pas lu, et avant de mettre au point le mien sur le mbed, je ne savais pas ce qu'il faire. C'est un équilibre entre lecture, calcul, écriture. Tu as resolu le pb du calcul, tu as raison entre autre de ne rien transformer sur l'arduino. Une idée, plutôt que de vouloir tout écrire sur la carte, n'écrit qu'un seul pid. Mais le mieux est de transmettre, et stocker à l'autre bout. Et filtre sur le connecteur, ce sera une aide non négligeable aussi.

 

En étant moins gourmand tu sauras être sur de ce qu'il se passe, puis tu pourras augmenter le nombre de données que ton connecteur lit, le nombre de données calculées, le nombre de données transmises, le nombre de données stockées localement, tout en vérifiant par comparaison avec le traitement le plus léger (un pid que tu lis et transmets, que tu n'as rien perdu. Vas y lentement. Même le canserver je n'arrive pas à avoir plus que quelques (entre 30 et 70) données lu et transmises.

Cool, bravo, n'oublie pas que cela a pu changer depuis le moment où les posts ont été écrits.

 

c'est interessé ;-) comme cela je ne serai plus tout seul. Encore que je ne suis pas seul, j'ai toujours eu de l'assistance de l'équipe autour de Josh.

 

Après investigation j'ai trouvé les paquets manquants :) 

 

J'ai tout d'abord modifié ma loop, pour enregistrer :

- le nombre d'erreurs vu sur le bus

- le temps qu'il faut pour recevoir 1000 messages 306, c'est à dire normalement, 10 secondes

- le nombre de fois que je passe dans ma loop (pour voir si le CPU est surchargé)

void loop()
{
   while(CAN_OK == CAN0.readMsgBuf(&id, &len, rxBuf)) {
      // track 10 ms signal, meaning 1000 messages received in 10000ms
      if (id == 306) {
         if (count306 == 0) {
            count306StartTime = millis();
         }
         count306++;
         if (count306 == 1000) {
            // 10 sec ?
            dataFile.print("count 1000 messages with id 306 in ");
            uint32_t now = millis();
            dataFile.print(now - count306StartTime);
            dataFile.print(" ms, error count : ");
            dataFile.print(CAN0.errorCountRX());
            dataFile.print(", loopCount : ");
            dataFile.print(loopCount);
            dataFile.print(", messageCount : ");
            dataFile.print(messageCount);
            dataFile.println("");
            dataFile.flush();

            count306 = 0;
         }
      }

      messageCount++;
   }

   loopCount ++;
}

 

Et le résultat :

count 1000 messages with id 306 in 14674 ms, error count : 0, loopCount : 48386, messageCount : 20656
count 1000 messages with id 306 in 14254 ms, error count : 0, loopCount : 95699, messageCount : 40699

 

Conclusion :

- quasiment 46% de perte 0_o 

- aucune erreur sur le bus détecté par le module du lecture de bus CAN, ça c'est rassurant :)

- le programme a fait ~50 000 boucles et n'a vu "que" 20 656 messages passer... donc une fois sur deux, le programme trouve rien à lire

- hypothèse je suppose que c'est juste que des messages sont suffisamment collées entre eux pour ne pas en détecter certains

 

 

J'ai essayé d'initialisé le CAN a 20 MHz au lieu de 16 MHz, mais je ne reçois plus rien, donc c'est pas une solution

 

 

Puis j'ai essayé le mécanisme de filtrage, (entre nous les explications qu'on trouve sur les différents sites internet/forum c'est une vrai gageure a comprendre, moi ce qui l'a fait fonctionné c'est d'initialiser a MCP_STDEXT, sinon le filtrage est désactivé, d'initialiser les deux buffer init_Mask avec 0 ET 1, sinon ça marche pas non plus, en enfin les codes HEXA sont en étendu et pas en 11 bits : c'est à dire CAN0.init_Filt(0, 0, 0x01320000)et pas CAN0.init_Filt(0, 0, 306); Ce qui était pas forcement évidant aux premiers abord de faire un décalage de bit...)

 

Bref, ça fonctionne :

void setup(){
   pinMode(12, OUTPUT);      // OPEN THE POWER SUPPLY
   digitalWrite(12, HIGH);

   // init sd card
   if (!SD.begin(5)) {
      return;
   }

   // find next log file name
   for (uint8_t i = 1; i < 10000; i++){
      sprintf(fileName, "DATA%d.log", i);
      if (SD.exists(fileName) == false){
         break;
      }
   }

   // create new log file & append header
   dataFile = SD.open(fileName, FILE_WRITE);


   boolean canInit = false;

   while (!canInit) {
      // 500KBPS : https://www.racelogic.co.uk/_downloads/vbox/Vehicles/Other/Docs/Tesla-Model%203.pdf
      // does not work : 8MHz nor 20MHz nor 1MBPS
      // use MCP_STDEXT for enabling filters (MCP_STD does allows to MCP2515 init)
      if (CAN0.begin(MCP_STDEXT, CAN_500KBPS, MCP_16MHZ) == CAN_OK) {
         canInit = true;
      }
      else {
         dataFile.println("MCP2515 can not be Initialized");
         dataFile.flush();
         delay(1000);
      }
   }

   // filter
   CAN0.init_Mask(0, 0, 0x07FF0000); // all filter bits enabled
   CAN0.init_Mask(1, 0, 0x07FF0000); // all filter bits enabled
   CAN0.init_Filt(0, 0, 0x01320000); // keep only messages with id 306

   // listen bus only
   CAN0.setMode(MCP_LISTENONLY);

   dataFile.println("filter 306");
   dataFile.flush();
}

Résultat :

count 1000 messages with id 306 in 9975 ms, error count : 0, loopCount : 114365, messageCount : 999
count 1000 messages with id 306 in 9983 ms, error count : 0, loopCount : 228834, messageCount : 1999
count 1000 messages with id 306 in 9983 ms, error count : 0, loopCount : 343302, messageCount : 2999
count 1000 messages with id 306 in 9983 ms, error count : 0, loopCount : 457769, messageCount : 3999
count 1000 messages with id 306 in 9987 ms, error count : 0, loopCount : 572233, messageCount : 4999
count 1000 messages with id 306 in 9981 ms, error count : 0, loopCount : 686662, messageCount : 5999
count 1000 messages with id 306 in 9984 ms, error count : 0, loopCount : 801127, messageCount : 6999
count 1000 messages with id 306 in 9984 ms, error count : 0, loopCount : 915597, messageCount : 7999
count 1000 messages with id 306 in 9984 ms, error count : 0, loopCount : 1030066, messageCount : 8999 

 

Conclusion :

Aucune perte de paquet ! C'est rassurant, ça veut dire que mon leur de bus CAN est en mesure de lire ce volume de données, qu'elles sont bien transmises, et qu'il n'y a pas d'erreurs; ni sur le bus ni dans mon programme :) c'est toujours sympa de le dire

 

Finalement, la seule manière de ne pas perdre de paquets comme tu dis, c'est de faire du filtrage. J'ai lu qu'il était fait d'un point de vue hardware, donc plus performant que n'importe quel code, et puis si c'était "juste" un "if" a faire dans du code ils ne se seraient pas embêter à faire ce mécanisme de filtrage hardware...

 

Je n'ai pas testé le mécanisme d'interruption par contre, mais je doute qu'il soit suffisament performant pour ne pas perdre ces paquets

 

Et encore une fois, le problème c'est pas que c'est un paquet qui soit émis toutes les 10ms, c'est à dire très rapidement, le problème c'est qu'il y a pleins de messages émis en même temps, car ayant tous la même base temporelle, ils vont arriver au même moment (par exemple toutes les secondes on va avoir tous les messages émis toutes les secondes mais aussi tout les message émis toutes les 100ms et tout les messages émis toutes les 10 ms ! ça fait beaucoup de message d'un coup !), et c'est là que le filtrage est important, car il permet de faire un choix de qui va remplir le buffer d'entrée. Car même si la plupart du temps le CPU de mon Arduino ne fait rien, au moment ou tous les messages arrivent d'un coup, ben il est surchargé et des messages sont perdu

 

Pour bien comprendre, voici un trajet aller-retour que j'ai effectué :

image.png.95d67753d16d1395203a75d892e79826.png

Et on voit bien que de 0 secondes à 1000 secondes (l'aller) et de 3000 à 4000 secondes (le retour), je reçois une quantité de messages plus importante que quand je suis à l'arrêt (1000 <-> 3000 & de 4000 <-> 5500)

Intuitivement, on pourrait se dire : si en conduisant on arrive à recevoir plus de message qu'en stationnement, c'est qu'au moins quand on est stationné on reçoit tout les messages, vu que le programme est en mesure de recevoir plus :)

 

Alors évidement maintenant je sais que c'est faux, justement avec ce que j'ai expliqué au paragraphe précédent : à cause des périodes de stress, car c'est dans ces moments là qu'il y a des pertes.

 

Une autre manière de l'illustrer, c'est de s'apercevoir que toutes les secondes, c'est une grosse période de stress (en rouge), toutes les 100ms de tension (en orange), et la plupart du temps c'est peinard (vert) :

image.png.d93313d0da726267b1308b4c80c010d3.png 

 

Le mécanisme de filtrage est efficace pendant ces moments de stress, où le logiciel ne suit pas

Au début je voyais le mécanisme de filtrage comme un capacité à pouvoir enlever certains messages qui sont inutile pour un certain cas d'usage ( du coup je pensais qu'un "if" dans le code suffisait), alors qu'en fait  c'est pas son but premier, c'est plutôt la gestion de ces moments de stress, autrement dit, c'est un mécanisme qui sécurise ce qu'il faut garder

 

 

Ps: pour le message contenant la date et le temps courant, ça marche !! très bonne nouvelle ça, car ça va me permettre d'économiser 4 bytes par messages :) (je n'ai pas besoin d'une précision à la milli secondes pour mon cas d'usage)

Ce message est envoyé 10 fois par secondes tout de même !  

Le code pour les curieux :

 

// date time
if (messageId == 0x318) {
   BitReader bitReader1 = new BitReader(data, len);
   ZonedDateTime z = ZonedDateTime.of(
         // year
         2000 + (int) bitReader1.readDouble(0, 8, false, 1, 0.0, 0, 100).getAsDouble(),

         // month
         (int) bitReader1.readDouble(8, 8, false, 1, 0.0, 0, 100).getAsDouble(),

         // day
         (int) bitReader1.readDouble(32, 8, false, 1, 0.0, 0, 100).getAsDouble(),

         // hour
         (int) bitReader1.readDouble(24, 8, false, 1, 0.0, 0, 100).getAsDouble(),

         // min
         (int) bitReader1.readDouble(40, 8, false, 1, 0.0, 0, 100).getAsDouble(),

         // sec
         (int) bitReader1.readDouble(16, 8, false, 1, 0.0, 0, 100).getAsDouble(),

         // nano sec
         0,

         ZoneId.of("GMT")
   );
   System.err.println(z.withZoneSameInstant(ZoneId.systemDefault()));
}

 

 

 

Modifié par Jboll

Partager ce message


Lien à poster
Partager sur d’autres sites

Le 06/05/2023 à 15:00, Jboll a dit :

Aucune perte de paquet !

Bravo

Le 06/05/2023 à 15:00, Jboll a dit :

Finalement, la seule manière de ne pas perdre de paquets comme tu dis, c'est de faire du filtrage. J'ai lu qu'il était fait d'un point de vue hardware, donc plus performant que n'importe quel code

c'est ça.

Le 06/05/2023 à 15:00, Jboll a dit :

Je n'ai pas testé le mécanisme d'interruption par contre, mais je doute qu'il soit suffisament performant pour ne pas perdre ces paquets

Je suis du même avis

Le 06/05/2023 à 15:00, Jboll a dit :

je n'ai pas besoin d'une précision à la milli secondes pour mon cas d'usage

Dans mon cas j'en ai besoin car je recalcule la distance par intégration de la vitesse sur le temps, je recalcule les kWh consommés par intégration de produit UI sur le temps.

 

Amuse-toi bien.

Modifié par tben

Partager ce message


Lien à poster
Partager sur d’autres sites

Le 06/05/2023 à 15:00, Jboll a dit :

 

Je n'ai pas testé le mécanisme d'interruption par contre, mais je doute qu'il soit suffisament performant pour ne pas perdre ces paquets

S'il s'agit de la perte des paquets pendant le flush, en principe les interruptions continuent à être déclenchées pendant un flush, donc ça peut résoudre le problème

Partager ce message


Lien à poster
Partager sur d’autres sites

Le 13/05/2023 à 22:17, MrFurieux a dit :

S'il s'agit de la perte des paquets pendant le flush, en principe les interruptions continuent à être déclenchées pendant un flush, donc ça peut résoudre le problème

c’est les deux, l’interruption va sans doute permettre de garder les gros blanc de quelque ms que j’ai mais j’ai aussi le problème pendant le traitement d’un autre message..

 

C’est a dire, admettons que le programme sache traiter 1 message au max pendant 1 ms (nombres arbitraires), si à chaque ms un message arrive sur le bus, il est lu et traité. Très bien. Aucune perte. Sauf que comme les messages sont périodiques, de fréquences 10Hz ou 100Hz le plus souvent, ET qu’ils ont tous le même base de temps, et bien ils sont tous écrit au même moment.  Mais évidemment le bus CAN ne va permettre cette cacophonie et va faire passer tous ces messages à la queue leu leu, mais du coup dans 1ms il y aura peut-être 10 messages à traiter, et comme mon programme ne peut en lire que un, les autres seront automatique écrasés par les suivants. Sauf si un mécanisme de filtrage est mise en place pour éviter la saturation du buffer d’entrée 

 

en tout cas comme ça que je le comprend

 

 la solution de filtrage marche par contre parfaitement.

 

 Être gourmand mais accepter des pertes non maîtrisées ou être raisonnable et avoir des pertes maîtrisées, il faut choisir en fonction des usages ^^

Modifié par Jboll
Typo

Partager ce message


Lien à poster
Partager sur d’autres sites

Le 14/05/2023 à 00:05, Jboll a dit :

c’est les deux, l’interruption va sans doute permettre de garder les gros blanc de quelque ms que j’ai mais j’ai aussi le problème pendant le traitement d’un autre message..

 

C’est a dire, admettons que le programme sache traiter 1 message au max pendant 1 ms (nombres arbitraires), si à chaque ms un message arrive sur le bus, il est lu et traité. Très bien. Aucune perte. Sauf que comme les messages sont périodiques, de fréquences 10Hz ou 100Hz le plus souvent, ET qu’ils ont tous le même base de temps, et bien ils sont tous écrit au même moment.  Mais évidemment le bus CAN ne va permettre cette cacophonie et va faire passer tous ces messages à la queue leu leu, mais du coup dans 1ms il y aura peut-être 10 messages à traiter, et comme mon programme ne peut en lire que un, les autres seront automatique écrasés par les suivants. Sauf si un mécanisme de filtrage est mise en place pour éviter la saturation du buffer d’entrée 

 

en tout cas comme ça que je le comprend

 

 la solution de filtrage marche par contre parfaitement.

 

 Être gourmand mais accepter des pertes non maîtrisées ou être raisonnable et avoir des pertes maîtrisées, il faut choisir en fonction des usages ^^

C'est très possible qu'il n'y ait pas d'intérêt pratique à utiliser des interruptions dans ton cas de figure, mais c'est une technique intéressante à avoir dans sa boîte à outil pour ce genre de traitement.

Le handler d'interruption est appelé quand un message est reçu, et il doit faire qqchose avec dans un délai très court (avant qu'un autre message n'arrive). Juste une copie dans un tampon interne, par ex. Tant qu'il n'y a pas d'opérations d'E/S, on est largement sous la µs.

A côté, la boucle de traitement, au lieu de lire le bus, surveille le tampon, et quand il atteint une taille minimale, elle en extrait un fragment dans un autre tampon. Ce fragment est ensuite écrit dans le fichier. De cette façon, la seule partie à protéger d'un conflit entre le handler l'interruption et la boucle est l'extraction du fragment du tampon de réception - si c'est juste des déplacements de pointeur dans un tampon circulaire, ça doit aller.

void loop()
{
  while(1) {
    // si qté de messages < N, dort 1 ms et reboucle
    // extraction de N messages du tampon (attention aux conflits)
    // write et flush
  }
}

 

(je précise que je ne connais pas la plateforme Arduino, ce que j'ai écrit n'est pas forcément 100% complet et correct - c'est pour illustrer l'idée générale. Merci de votre indulgence)

Partager ce message


Lien à poster
Partager sur d’autres sites

Le 14/05/2023 à 17:36, MrFurieux a dit :

Juste une copie dans un tampon interne, par ex. Tant qu'il n'y a pas d'opérations d'E/S, on est largement sous la µs.

A côté, la boucle de traitement, au lieu de lire le bus, surveille le tampon

c'est comme cela que je procédais sur le mbed. mais quand je ne filtrais pas le tampon interne finissait par déborder.

 

Le 14/05/2023 à 00:05, Jboll a dit :

Être gourmand mais accepter des pertes non maîtrisées ou être raisonnable et avoir des pertes maîtrisées, il faut choisir en fonction des usages

En ce qui me concerne, j'avais besoin de n'avoir aucune perte. Donc je limitais les pid lus (ou interrogés).

Partager ce message


Lien à poster
Partager sur d’autres sites

Le 14/05/2023 à 18:39, tben a dit :

c'est comme cela que je procédais sur le mbed. mais quand je ne filtrais pas le tampon interne finissait par déborder.

Le vrai facteur limitant c'est le débit d'écriture sur la carte SD. Dans son cas ça a l'air de passer.

Il faut aussi un minimum de RAM bien sûr, possible que ça soit un peu juste sur un Arduino.

Partager ce message


Lien à poster
Partager sur d’autres sites

Le 14/05/2023 à 19:36, MrFurieux a dit :

Le vrai facteur limitant c'est le débit d'écriture sur la carte SD.

ça c'est vrai, et ce que nous avions fait, c'est de transmettre en bluetooth.

Le 14/05/2023 à 19:36, MrFurieux a dit :

Dans son cas ça a l'air de passer.

Sans filtrage? j'ai compris l'inverse de toi, c'est pourquoi il est passé sur mon conseil au filtrage.

 

Edit: la ram n'est pas une solution pour le buffer car cela ne fait que repousser le moment où cela sera plein, sans compter le retard à traiter.

Modifié par tben

Partager ce message


Lien à poster
Partager sur d’autres sites

Le 14/05/2023 à 21:59, tben a dit :

ça c'est vrai, et ce que nous avions fait, c'est de transmettre en bluetooth.

Sans filtrage? j'ai compris l'inverse de toi, c'est pourquoi il est passé sur mon conseil au filtrage.

Ce qui fait penser qu'il y a de la marge pour l'écriture sur la SD c'est ce graphe:

image.thumb.png.8693f1032c45c5d63a68f8bd62f4a9f6.png

 

Le trou correspond au flush(), çàd à l'écriture sur la SD. En doublant le nb de flush(), on peut doubler le débit d'écriture, non ?

 

Le 14/05/2023 à 21:59, tben a dit :

Edit: la ram n'est pas une solution pour le buffer car cela ne fait que repousser le moment où cela sera plein, sans compter le retard à traiter.

Le buffer en RAM sert à absorber les pics de messages, pas à régler un déficit de débit chronique. Si le déficit est chronique, oui on est obligé de filtrer.

Partager ce message


Lien à poster
Partager sur d’autres sites

Le 14/05/2023 à 22:24, MrFurieux a dit :

Le buffer en RAM sert à absorber les pics de messages, pas à régler un déficit de débit chronique. Si le déficit est chronique, oui on est obligé de filtrer.

c'est pourquoi tous ceux qui ne veulent perdre aucun message d'un pid filtrent. Après suivant les usages, ce n'est pas grave de perdre des messages. Dans mon cas ça l'était donc j'ai filtré.

 

Le 14/05/2023 à 22:24, MrFurieux a dit :

Le trou correspond au flush(), çàd à l'écriture sur la SD. En doublant le nb de flush(), on peut doubler le débit d'écriture, non ?

Dans mon cas l'écrire sur la carte était trop lent, mais de toutes les façons j'avais besoin de recevoir les données en temps réel donc le bluetooth s'imposait. La carte ne m'a servi que pour stocker le paramétrage (dont ce que je voulais recevoir = filtre, et quelques données de reprise = gestion des arrêts et redémarrages, et le mode debug ou non)

Partager ce message


Lien à poster
Partager sur d’autres sites

Le 14/05/2023 à 22:24, MrFurieux a dit :

Ce qui fait penser qu'il y a de la marge pour l'écriture sur la SD c'est ce graphe:

image.thumb.png.8693f1032c45c5d63a68f8bd62f4a9f6.png

 

Le trou correspond au flush(), çàd à l'écriture sur la SD. En doublant le nb de flush(), on peut doubler le débit d'écriture, non ?

 

Le buffer en RAM sert à absorber les pics de messages, pas à régler un déficit de débit chronique. Si le déficit est chronique, oui on est obligé de filtrer.

En doublant le nb de flush() tu aura juste deux trous de même taille, car un flush prend un durée constante pour écrire sur le disque, et ça peu importe si tu dois écrire 1 octet ou 100. (mais au delà d'une certaine limite, il faudra compter X2 sur le temps d'écriture, puis X3 etc. : Tant que ça reste dans le buffer c'est bon (de 512 octets) ce sera écrit en une fois)

 

Le 14/05/2023 à 19:36, MrFurieux a dit :

Le vrai facteur limitant c'est le débit d'écriture sur la carte SD. Dans son cas ça a l'air de passer

Oui mais pas que

Oui et ça se voit directement sur le graphique ci-dessous, avec ces 2ms de trou

Mais pas que, parce que si tu regardes attentivement les points tu verra qu'il ne sont pas très rapproché, ou en tout cas il devrait en avoir plus, le graphique devrait être plus dense. ça c'est pas à cause de la carte SD, c'est à cause du CPU qui n'arrive pas à suivre la cadence effrénée du BUS. Pour donner quelques chiffres, ce programme arrive à traiter 1300 messages par secondes. Sauf que j'avais lu que sur le bus Tesla c'est plutôt 2600 messages par secondes qui sont envoyé. Ce qui laisserai penser à première vue que je perd environ 50% des paquets ! et c'est pas les 2ms de trou de temps en temps qui vont expliquer entièrement cette perte.

ça se voit bien sur ce graphique :

image.thumb.png.4ed648f14b9e19f9c5a4b5db8dc59590.png

Si tu regardes attentivement les points jaunes, qui correspondent tous au même type de message (celui avec l'identifiant 306), est un message périodique de période de 10ms. Sauf que comme expliquer les trous ? a première vue on pourrait se dire que c'est à cause du flush sur la carte SD, mais non puisse que par moment c'est un point vert qui a été reçu à la place d'un jaune. C'est donc un autre problème : c'est le CPU qui n'arrive pas a lire aussi vite que les données poussées dans le buffer de réception

 

Pour ce problème : oui il est possible que les interruptions puisse lisser la charge en utilisant un buffer interne entre le bus et la carte SD, si effectivement ce n'est pas un pb chronique.

 

Comme ça à piquer ma curiosité, j'ai tenté de modifier mon programme pour gérer les interruptions mais pour une raison que j'ignore encore, je n'arrive jamais a être interrompu. Sans doute que je n'ai pas le bon numéro de PIN lors de l'initialisation de l'interruption. J'ai demandé l'avis du constructeur, la dernière fois il m'avait répondu pour un problème similaire. Je vais voir s'il me répond.

 

A part la curiosité, je n'ai ni besoin de ce mécanisme d'interruption ni de filtrage pour mon usage. Mais je garde précieusement ces cartes en mains au cas où j'en aurais besoin.

 

Au passage, j'ai profité de cette journée pour faire un tour au SUC et voir si la cellule 60 a chargé un peu plus que sur une charge lente

image.thumb.png.aa693193f8a91a969852ddb8f2cb8f6d.png

 

Eh bien non, elle est toujours en rose/violette tout en bas du graphique

 

Je rappelle les phases :

- 15h35 je branche le pistolet du SUC

- 15h36 -> 15h52 (pic) : charge jusqu'à 99%

- 15h52 -> 15h57 phase de décroissance de l'intensité jusqu'à 0

- 15h57 : 100% affiché, fin de la charge, recalcul de l'autonomie au km (404 Km soit dit en passant)

- 15h57 (débranchement du pistolet) -> 16h (stationnement) : stabilisation des tensions

 

Vu la faible réception des messages contenant le voltages des cellules, je soupçonne que l'arrêt de la charge se situe dès que la tension du pack complet atteint une certaines valeur (396 V), à partir de là -> diminution de l'intensité progressivement ce qui permet de charger encore quelques cellules  

 

Car si c'est la somme des cellules qui est prise en compte au lieu de la tentions global, à cause du délai d'envoie des messages on chargerai trop. Sur le graphique suivant (qui correspond pas à la recharge d'aujourd'hui au SUC) j'affiche la somme des tensions reçu en bleu, et en orange la tension du pack :

image.thumb.png.a0049b83c7fa06a3adb8394d94ec618e.png

La courbe bleu (somme des cellules que j'ai calculé à la main) est décalée sur la droite par rapport à l'état courant. Et ce décalage correspond au temps qu'il faut pour envoyer toutes les tensions de toutes les cellules à travers le bus, donc c'est trop lent pour être utilisé en tant qu'arrêt de charge.

Evidement je suppose que les 4 "modules" envoient leur données au BMS central via le bus CAN (et c'est pour ça qu'elles transitent sur le réseau) Mais je n'en suis pas sûr. J'ai un doute s'il y a un seul et unique BMS ou pas. Mais s'il n'y en a qu'un seul, je ne vois pas l'intérêt de faire circuler ces info sur le bus CAN.

 

Bref, tout ça pour dire que ma cellule rose ne se charge toujours pas complètement

 

Comme je ne sais pas comment ils comptent, j'ai essayé de mettre en vert les cellules qui se charge bien, en orange les derniers de la classe et en rouge le cancre XD

image.png.94cd62d868025caafeb36d94f427572a.png

 

J'ai fais ce schéma en fonction de la vidéo qui montre une de nos batteries :

image.thumb.png.9a4a731f680965b38ce19539651f30e1.png

image.thumb.png.0be6aa9773271808301a14e3a2c89494.png

 

 

J'ai l'impression que ce sont les cellules les plus proche du "+" qui se chargent le moins (sauf ma 60 qui est en plein milieu du pack) c'est à dire celles qui sont le plus usée ?!

 

A suivre

 

Modifié par Jboll
Ajout d'une photo + typo

Partager ce message


Lien à poster
Partager sur d’autres sites

Le 15/05/2023 à 18:32, Jboll a dit :

En doublant le nb de flush() tu aura juste deux trous de même taille, car un flush prend un durée constante pour écrire sur le disque, et ça peu importe si tu dois écrire 1 octet ou 100. (mais au delà d'une certaine limite, il faudra compter X2 sur le temps d'écriture, puis X3 etc. : Tant que ça reste dans le buffer c'est bon (de 512 octets) ce sera écrit en une fois)

C'était la question de départ, est-ce que les interruptions sont déclenchées aussi pendant le flush, sinon ça sert à rien. C'est le principe, avoir une sorte de multi-thread rudimentaire dans un environnement mono-CPU et mono-thread comme le tien. En dehors de pouvoir traiter plus de flux, c'est aussi un moyen d'améliorer la résolution temporelle pour vraiment faire du temps réel.

D'un côté, le filtrage permet à la fois de régler des problèmes de saturation et de ne pas s'encombrer avec des trames inutiles.

D'un autre côté, les interruptions permettent à la fois de traiter plus rapidement (suivant le code), de ne pas avoir de moments aveugles pendant lesquels les trames sont perdues, et d'horodater précisément chaque trame.

 

Le 15/05/2023 à 18:32, Jboll a dit :

Mais pas que, parce que si tu regardes attentivement les points tu verra qu'il ne sont pas très rapproché, ou en tout cas il devrait en avoir plus, le graphique devrait être plus dense. ça c'est pas à cause de la carte SD, c'est à cause du CPU qui n'arrive pas à suivre la cadence effrénée du BUS. Pour donner quelques chiffres, ce programme arrive à traiter 1300 messages par secondes. Sauf que j'avais lu que sur le bus Tesla c'est plutôt 2600 messages par secondes qui sont envoyé. Ce qui laisserai penser à première vue que je perd environ 50% des paquets ! et c'est pas les 2ms de trou de temps en temps qui vont expliquer entièrement cette perte.

ça se voit bien sur ce graphique :

image.thumb.png.4ed648f14b9e19f9c5a4b5db8dc59590.png

Si tu regardes attentivement les points jaunes, qui correspondent tous au même type de message (celui avec l'identifiant 306), est un message périodique de période de 10ms. Sauf que comme expliquer les trous ? a première vue on pourrait se dire que c'est à cause du flush sur la carte SD, mais non puisse que par moment c'est un point vert qui a été reçu à la place d'un jaune. C'est donc un autre problème : c'est le CPU qui n'arrive pas a lire aussi vite que les données poussées dans le buffer de réception

 

Pour ce problème : oui il est possible que les interruptions puisse lisser la charge en utilisant un buffer interne entre le bus et la carte SD, si effectivement ce n'est pas un pb chronique.

 

Comme ça à piquer ma curiosité, j'ai tenté de modifier mon programme pour gérer les interruptions mais pour une raison que j'ignore encore, je n'arrive jamais a être interrompu. Sans doute que je n'ai pas le bon numéro de PIN lors de l'initialisation de l'interruption. J'ai demandé l'avis du constructeur, la dernière fois il m'avait répondu pour un problème similaire. Je vais voir s'il me répond.

2600 messages par secondes, même pour un microcontrôleur anémique, c'est assez peu. La limitation serait éventuellement dans la boucle de traitement qui pourrait prendre du temps même sans flush:

void loop()
{
  while(CAN_OK == CAN0.readMsgBuf(&id, &len, rxBuf)) { // appel de fct, pourrait être assez long
      uint32_t now = millis(); // appel de fct, pourrait être assez long 

      // <...> arithmétique et copies, pas long
    
      dataFile.write(capsule, 8 + len); // appel de fct et écriture dans un cache (?), pourrait être long

      if (now - flushTime > 10000) {
          dataFile.flush(); // accès obligatoire à la carte SD, très long
          flushTime = now;
      }
  }
}

 

Une récupération dans un tampon local sur interruption pourrait être bien plus efficace, mais j'ai vu dans la fiche produit une RAM de 1Ko, c'est ce que tu as ? Je ne savais pas qu'on fabriquait des cartes avec des RAM aussi minuscules.

Si les interruptions ne se déclenchent pas, c'est peut être qu'elles ne sont pas activées par défaut pour ne pas gaspiller ?

Partager ce message


Lien à poster
Partager sur d’autres sites

Le 15/05/2023 à 18:32, Jboll a dit :

J'ai fais ce schéma en fonction de la vidéo qui montre une de nos batteries :

 

 

 

J'ai l'impression que ce sont les cellules les plus proche du "+" qui se chargent le moins (sauf ma 60 qui est en plein milieu du pack) c'est à dire celles qui sont le plus usée ?!

NB c'est pas un pack LFP, c'est une  NCA mid-range

Partager ce message


Lien à poster
Partager sur d’autres sites

Le 15/05/2023 à 18:32, Jboll a dit :

Bref, tout ça pour dire que ma cellule rose ne se charge toujours pas complètement

Mais est-ce qu'au bout d'un moment l'équilibrage se fait ou il reste un écart de tension persistant même 1h après la charge ?

Le test ultime de décharge-charge c'est la procédure "battery health" du mode service, mais est-ce qu'il y aurait la place sur la carte pour l'enregistrer en entier, pas sûr.

Partager ce message


Lien à poster
Partager sur d’autres sites

Le 16/05/2023 à 23:07, MrFurieux a dit :

C'était la question de départ, est-ce que les interruptions sont déclenchées aussi pendant le flush, sinon ça sert à rien. C'est le principe, avoir une sorte de multi-thread rudimentaire dans un environnement mono-CPU et mono-thread comme le tien. En dehors de pouvoir traiter plus de flux, c'est aussi un moyen d'améliorer la résolution temporelle pour vraiment faire du temps réel.

D'un côté, le filtrage permet à la fois de régler des problèmes de saturation et de ne pas s'encombrer avec des trames inutiles.

D'un autre côté, les interruptions permettent à la fois de traiter plus rapidement (suivant le code), de ne pas avoir de moments aveugles pendant lesquels les trames sont perdues, et d'horodater précisément chaque trame.

Oui c'est bien l'idée, je n'ai pas eu de nouvelles du constructeur encore, mais je suis curieux de voir ce qu'il est possible d'avoir comme bénéfice.

 

Le 16/05/2023 à 23:07, MrFurieux a dit :

2600 messages par secondes, même pour un microcontrôleur anémique, c'est assez peu

Ben ça dépend s'ils arrivent tous au même moment ou pas... si c'est homogènement réparti dans le temps il est possible de bosser en juste à temps, mais si c'est des burst de 500 messages par ms toutes les 100 ms c'est pas pareil

 

Le 16/05/2023 à 23:07, MrFurieux a dit :

Une récupération dans un tampon local sur interruption pourrait être bien plus efficace,

Oui c'est le code que j'ai fais, mais qui n'est pas encore opérationnel

 

Le 16/05/2023 à 23:07, MrFurieux a dit :

j'ai vu dans la fiche produit une RAM de 1Ko, c'est ce que tu as ? Je ne savais pas qu'on fabriquait des cartes avec des RAM aussi minuscules

Non c'est davantage, mais de manière général on parle de petite cartes qui ne consomme rien et qui démarre vite

Pour info j'ai 2Mo max pour le programme et 262 Ko pour la mémoire dynamique

Pour te donner un ordre de grandeur, je n'utilise que 3% de l'espace réservé pour le programme et 4% pour les variables globales, et il me reste les 96% (250Ko) restant pour les variables locales

Autant te dire : c'est large question mémoire

 

Le 16/05/2023 à 23:07, MrFurieux a dit :

Si les interruptions ne se déclenchent pas, c'est peut être qu'elles ne sont pas activées par défaut pour ne pas gaspiller ?

Oui par défaut c'est désactivé, mais même en l'activant ça ne marche pas (encore)

 

Le 16/05/2023 à 23:10, MrFurieux a dit :

NB c'est pas un pack LFP, c'est une  NCA mid-range

Ok, il faudra que j'en trouve une autre alors, celle de Monroe montre pas grand chose je trouve

 

Le 16/05/2023 à 23:16, MrFurieux a dit :

Mais est-ce qu'au bout d'un moment l'équilibrage se fait ou il reste un écart de tension persistant même 1h après la charge ?

 Je ne comprend pas ta question, l'équilibrage est fait au moment de la charge non ?

Pour bien se comprendre sur la terminologie j'ai fais un dessin :)

image.thumb.png.2a46b3a5801d9f4c4d394f7f660728ce.png

Voici les différentes étapes d'une charge que je vois

- A : Phase de charge, le SOC monte progressivement, ici l'intensité est représentée par la courbe rose et de valeur 12A

- B : A ce moment précis on rentre dans une nouvelle phase, qui consiste à réduire drastiquement le courant et à laisser un léger courant pour continuer à charger très doucement certaines cellules. Pour moi cette phase, ça dure 2 minutes dans cet exemple, et environ 10 minutes en SUC. C'est ce que tu appelles l'équilibrage ?

- C : A ce moment précis d'intensité tombe à 0, un message est envoyé sur mon tel me disant que la charge est terminée, et la tension de charge n'existe plus. La voiture est maintenant isolée de la source externe d'énergie

-D : Pendant cette période, la tension que l'on voit en orange correspond à après la charge, les tensions des cellules diminuent lentement et se rejoignent (sauf ma 60) :

image.thumb.png.293ba87069571a9de50644ce00fff566.png

 

Sur un SUC:

image.thumb.png.40509db76aca6a561e354705fb4c4c3d.png

 

 

image.thumb.png.7ddcee5852350e0f4e75522a944b7600.png

 

Je ne sais pas si ça répond à ta question. 

 

Le 16/05/2023 à 23:16, MrFurieux a dit :

Le test ultime de décharge-charge c'est la procédure "battery health" du mode service, mais est-ce qu'il y aurait la place sur la carte pour l'enregistrer en entier, pas sûr.

Sans filtrage, non je ne pense pas. Mais c'est pas la taille de la carte qui va limiter, c'est la taille que peux avoir un fichier (4Go) dans cet ancien format

Donc filtrage, pas le choix.

 

 

 

Partager ce message


Lien à poster
Partager sur d’autres sites

Le 17/05/2023 à 10:23, Jboll a dit :

 Je ne comprend pas ta question, l'équilibrage est fait au moment de la charge non ?

Sur les 3/Y l'équilibrage est passif, assez lent, et post-charge. Pour une batterie assez neuve et en bonne santé, le déséquilibre est < 10 mV, comme tu peux voir en fin de charge tu es au dessus de 100 mV même hors cellule 60. Normalement si tu attends 1h ou 2 après la charge ça doit revenir dans les clous, c'est pas le cas ?

Partager ce message


Lien à poster
Partager sur d’autres sites

Mon programme est terminé pour enregistrer une charge de 0 à 100%, j'ai noté les points suivants :

- Enregistrement de 6 types de messages différents, dont celui avec l'id 306 contenant la tension et l'intensité de la batterie haute voltage

- Ce filtrage + une compression des id vont me permettre d'enregistrer au maximum 654 heures dans un fichier de 4 Go (autant dire que c'est large)

- J'ai mesuré une perte de message de 0.1% pour tout les types de messages sauf un qui est à 0.9% (mais dont la précision n'est pas si importante)

- Je n'ai pas eu besoin d'avoir recours au mécanisme d'interruption (le % de perte est ok)

 

Il y a néanmoins un message, le 1025 (tensions des cellules) qui est périodique mais dont la période est différentes des autres, j'ai d'abord cru a des pertes de messages :

image.thumb.png.d7f753e024e94af765ed961d041edefb.png

 

Mais je me suis aperçu que ces pertes se répètent de manière régulière et pas n'importe comment, j'ai fais un schéma :

 

image.png.5514ca55e0d01e15ee69e8cb16a2abb1.png

 

ce message ne contient que 3 tensions à chaque fois, pour mes 106 cellules il va envoyer 36 messages précisément, et toujours espacées de 1.6 seconde précisément (à la milli seconde prés !). Une fois les 106 messages envoyés, il y a 4 secondes de trou (toujours précis à la ms) avant de recommencer à nouveau, avec une période globale d'une minute (c'est précis à la milli seconde prés !)

 

Donc je ne pense pas que ce soit une perte, c'est juste le message qui est comme ça : 

- ils envoient des paquets de 3 tensions toutes les 1.6 secondes

- ils recommence toutes les minutes

 

 

Pour les curieux et la postérité, voici mon programme :

#include <SPI.h>
#include <SD.h>
#include <mcp_can.h>

#define SPI_CS_PIN  9
MCP_CAN CAN(SPI_CS_PIN);


long unsigned int id;
unsigned char len = 0;
unsigned char rxBuf[8];

/* SD card Settings */
char fileName[20];
File dataFile;

// time(4), id(1), len(1), data(max 8) = 4+1+1+8 = 14 bytes
byte capsule[14];

uint32_t flushTime;

void setup() {
   pinMode(12, OUTPUT);      // OPEN THE POWER SUPPLY
   digitalWrite(12, HIGH);

   // init sd card
   if (!SD.begin(5)) {
      return;
   }

   // find next log file name
   for (uint8_t i = 1; i < 10000; i++){
      sprintf(fileName, "DATA%d.log", i);
      if (SD.exists(fileName) == false){
         break;
      }
   }

   // create new log file
   dataFile = SD.open(fileName, FILE_WRITE);


   // init CAN BUS module
   boolean canInit = false;
   while (!canInit) {
      // 500KBPS : https://www.racelogic.co.uk/_downloads/vbox/Vehicles/Other/Docs/Tesla-Model%203.pdf
      // does not work : 8MHz nor 20MHz nor 1MBPS
      // use MCP_STDEXT for enabling filters (MCP_STD does allows to MCP2515 init)
      if (CAN.begin(MCP_STDEXT, CAN_500KBPS, MCP_16MHZ) == CAN_OK) {
         canInit = true;
      }
      else {
         dataFile.println("MCP2515 can not be Initialized");
         dataFile.flush();
         delay(1000);
      }
   }

   // filter (with the following filters, we can store 654 hours of recording inside a 4 Go file size ! and with a loss between 0.1% and 0.9%)
   CAN.init_Mask(0, 0, 0x07FF0000); // all filter bits enabled
   CAN.init_Filt(0, 0, 0x01320000); // keep messages with id 306 Battery pack voltage
   CAN.init_Filt(1, 0, 0x02640000); // keep messages with id 612 ChargeLineStatus

   CAN.init_Mask(1, 0, 0x07FF0000); // all filter bits enabled
   CAN.init_Filt(2, 0, 0x033A0000); // keep messages with id 826 SOC
   CAN.init_Filt(3, 0, 0x03520000); // keep messages with id 850 BMS
   CAN.init_Filt(4, 0, 0x04010000); // keep messages with id 1025 BMS
   CAN.init_Filt(5, 0, 0x03180000); // keep messages with id 792 date

   // listen bus only
   CAN.setMode(MCP_LISTENONLY);

   // write how the data are written
   dataFile.println("data format : time (4bytes), id (1 byte {0=Unknown, 1=306, 2=612, 3=792, 4=826, 5=850, 6=1025}), data len (1 byte), data (0-8 bytes)");
   dataFile.flush();
}

void loop() {

   // readMsgBuf does a check inside this function so we can call it immediatly
   while(CAN_OK == CAN.readMsgBuf(&id, &len, rxBuf)) {

      // does not allows degenerated messages
      if (len > 0 && len < 9) {

         // time
         uint32_t uptime = millis();
         capsule[0] = (byte)((uptime >> 24) & 0xFF);
         capsule[1] = (byte)((uptime >> 16) & 0xFF);
         capsule[2] = (byte)((uptime >> 8) & 0xFF);
         capsule[3] = (byte)(uptime & 0xFF);

         // message id (compressed : 3 bytes gain by using this mapping function instead of write the full id that took 4 bytes)
         if (id == 306) capsule[4] = 1;       // period of 10 ms, check it first
         else if (id == 612) capsule[4] = 2;  // period of 100 ms
         else if (id == 792) capsule[4] = 3;  // period of 100 ms
         else if (id == 826) capsule[4] = 4;  // period of 1 second
         else if (id == 850) capsule[4] = 5;  // period of 1 second
         else if (id == 1025) capsule[4] = 6; // period of 1.6 seconds
         else capsule[4] = 0;                 // error

         // data len
         capsule[5] = len;

         // data
         for (int i=0; i<len; i++) {
            capsule[6 + i] = rxBuf[i];
         }

         // send to internal buffer
         dataFile.write(capsule, 6 + len);

         // write on sd card every 10 sec max
         if (uptime - flushTime > 10000) {
            dataFile.flush();
            flushTime = uptime;
         }
      }
   }
}

 

 

 

Modifié par Jboll

Partager ce message


Lien à poster
Partager sur d’autres sites

Le 17/05/2023 à 12:25, MrFurieux a dit :

Sur les 3/Y l'équilibrage est passif, assez lent, et post-charge. Pour une batterie assez neuve et en bonne santé, le déséquilibre est < 10 mV, comme tu peux voir en fin de charge tu es au dessus de 100 mV même hors cellule 60. Normalement si tu attends 1h ou 2 après la charge ça doit revenir dans les clous, c'est pas le cas ?

Après 1 ou 2 heures les tensions se stabilisent, c'est surtout en charge qu'on voit la différence

Une fois stabilisée, l'écart est d'environ 16 mV entre la tension de la plus haute et la plus basse (toujours ma 60) 

image.thumb.png.fa0db6083154c30d305a4b21ef16d499.png

 

Ma 60eme cellule (nommé ici Brick59 vu que c'est compté à partir de 0) a une tension de 3.292 V tandis que la plus haute (Brick 82) est de 3.308 V, une différence donc de 16 mV

 

Pour revenir a la phase déquilibrage, ça correspond pas vraiment à la phase D du coup

image.thumb.png.2a46b3a5801d9f4c4d394f7f660728ce.png

 

bien que les tensions se rejoignent :

image.thumb.png.7ddcee5852350e0f4e75522a944b7600.png

 

Car d'après la Spec de CATL, dès qu'on arrête la charge, les cellules passent de 3.7 V à 3.4V

image.thumb.png.7885dcaff032e287996e58966ff18f9c.png

 

image.png.f729d349161c76296fdec6ae4b61b5e3.png

 

Et c'est bien ce qui est constaté : elles chutent une fois la charge terminée à 3.4V

Et ça peut importe si on fait un équilibrage ou pas, c'est juste le comportement normal après une charge

 

Par contre l'équilibrage c'est donc un autre phénomène, mais je ne vois pas trop comment il marche sur nos LFP : vu la faible différences de tension des cellules, c'est pas facile de savoir celles qui sont plus chargées que d'autre rien qu'en se basant sur leurs tensions. Est-ce qu'il y a de la doc sur le sujet ? sur nos Model 3 LFP ?

 

 

 

Modifié par Jboll

Partager ce message


Lien à poster
Partager sur d’autres sites





×
×
  • Créer...
Automobile Propre

Automobile Propre est un site d'information communautaire qui est dédié à tout ce qui concerne l'automobile et l'environnement. Les thématiques les plus populaires de notre blog auto sont la voiture électrique et les hybrides, mais nous abordons également la voiture GNV / GPL, les auto à l'hydrogène, les apects politiques et environnementaux liés à l'automobile. Les internautes sont invités à réagir aux articles du blog dans les commentaires, mais également dans les différents forums qui sont mis à leur dispositon. Le plus populaire d'entre eux est certainement le forum voiture électrique qui centralise les discussions relatives à l'arrivée de ces nouveaux véhicules. Un lexique centralise les définitions des principaux mots techniques utilisés sur le blog, tandis qu'une base de données des voitures (commercialisées ou non) recense les voitures électriques et hybrides.