Développement de Protocoles de Communication Série Personnalisés sous Android

Dans le domaine du développement Android, la communication série est un mécanisme essentiel pour interagir avec des périphériques externes tels que des capteurs, des microcontrôleurs ou d'autres équipements inudstriels. Pour que cette interaction soit efficace et fiable, il est nécessaire d'établir un protocole qui définisse la structure des données et les règles d'échange.

Protocoles de Communication Série Courants

Plusieurs types de protocoles sont couramment employés pour l'échange de données via des liaisons série :

  • Protocole ASCII : Les données sont représentées par des caractères ASCII, ce qui les rend lisibles par un humain et faciles à déboguer. Chaque commande ou information est transmise sous forme de texte, souvent terminée par des caractères de contrôle comme le retour chariot ou le saut de ligne.
  • Protocole Binaire : Contrairement à l'ASCII, ce protocole transmet des octets bruts. Il est généralement plus compact et plus rapide, car il ne nécessite pas de conversion de caractères. Cependant, il est plus complexe à inspecter et à débugger.
  • Protocole Modbus : Largement utilisé dans l'automatisation industrielle, Modbus est un protocole maître/esclave pour la communication entre périphériques. Il se décline en variantes RTU (transmission binaire compacte), ASCII et TCP/IP (pour les réseaux).
  • Protocoles Spécifiques/Personnalisés : Lorsque les protocoles existants ne répondent pas aux besoins spécifiques d'une application (contraintes de performance, de sécurité, ou de format de données), il est courant de concevoir son propre protocole.

Étapes pour la Conception d'un Protocole Personnalisé

La création d'un protocole de communication série sur mesure implique une approche structurée pour garantir la robustesse et la flexibilité. Voici les étapes clés :

  1. Définition du Format de Trame : Il s'agit de structurer les paquets de données échangés. Cela inclut généralement :
    • Un octet de début de trame : un caractère ou une séquence d'octets fixe indiquant le début d'un message.
    • Un identifiant de commande ou de type de message : un octet pour distinguer les différents types de requêtes ou de réponses.
    • Un champ de longueur : un octet ou plus indiquant la taille des données utiles (charge utile).
    • La charge utile (data) : les données brutes que le message transporte.
    • Un champ de vérification d'intégrité (checksum ou CRC) : pour détecter les erreurs de transmission.
    • Un octet de fin de trame : marquant la fin du message.
  2. Établisseemnt des Règles de Transmission : Définir comment les messages sont envoyés et reçus. Cela peut inclure des notions comme le contrôle de flux, les délais d'attente, ou la gestion des accusés de réception (ACK/NACK).
  3. Implémentation des Mécanismes d'Encodage et de Décodage : Développer les fonctions qui convertissent les données applicatives en trames protocolaires (encodage) côté émetteur, et inversement (décodage) côté récepteur.
  4. Intégration de la Vérification d'Erreurs : Au-delà du simple checksum, il est important de concevoir des mécanismes pour la gestion des trames corrompues ou incomplètes, et de définir la réponse du système à ces erreurs.
  5. Tests et Validation Rigoureux : Effectuer des tests approfondis dans diverses conditions (forte charge, erreurs simulées) pour s'assurer de la stabilité, de la fiabilité et de la conformité du protocole.
  6. Documentation Complète : Rédiger une spécification claire et détaillée du protocole. Cette documentation est essentielle pour la maintenance, l'intégration avec d'autres systèmes et le travail d'équipe.

Exemple d'Implémentation d'un Protocole Série Simplifié

Voici un exemple Java illustrant la construction et l'analyse de trames pour un protocole série simple. Ce protocole utilise un octet de début, un type de commande, la longueur des données, la charge utile, un checksum XOR et un octet de fin.

Classe pour l'Assemblage et l'Analyse des Trames (FrameAssembler)


import java.nio.ByteBuffer;
import java.util.Arrays;

public class FrameAssembler {

   // Constantes du protocole
   private static final byte START_DELIMITER = (byte) 0xAA; // Début de trame
   private static final byte END_DELIMITER = (byte) 0x55;   // Fin de trame
   private static final int MAX_PAYLOAD_LENGTH = 250; // Longueur maximale des données utiles

   /**
    * Calcule un checksum XOR simple pour un tableau d'octets.
    * @param data Les octets sur lesquels calculer le checksum.
    * @return Le checksum calculé.
    */
   private static byte calculateXORChecksum(byte[] data) {
       byte checksum = 0;
       for (byte b : data) {
           checksum ^= b;
       }
       return checksum;
   }

   /**
    * Construit une trame protocolaire à partir d'un type de commande et d'une charge utile.
    * Format: [START_DELIMITER] [COMMAND_TYPE] [LENGTH] [PAYLOAD] [CHECKSUM] [END_DELIMITER]
    * @param commandType Le type de commande (1 octet).
    * @param payload Les données utiles (charge utile).
    * @return Le tableau d'octets représentant la trame complète, ou null si la charge utile est trop longue.
    */
   public static byte[] buildFrame(byte commandType, byte[] payload) {
       if (payload.length > MAX_PAYLOAD_LENGTH) {
           System.err.println("Erreur: Charge utile trop longue (" + payload.length + " > " + MAX_PAYLOAD_LENGTH + " octets)");
           return null;
       }

       // Le checksum est calculé sur le COMMAND_TYPE, LENGTH et PAYLOAD
       byte[] dataForChecksum = ByteBuffer.allocate(1 + 1 + payload.length)
                                           .put(commandType)
                                           .put((byte) payload.length)
                                           .put(payload)
                                           .array();
       byte checksum = calculateXORChecksum(dataForChecksum);

       // Allouer le buffer pour la trame complète
       // 1 (START) + 1 (COMMAND_TYPE) + 1 (LENGTH) + payload.length + 1 (CHECKSUM) + 1 (END)
       ByteBuffer frameBuffer = ByteBuffer.allocate(5 + payload.length);

       frameBuffer.put(START_DELIMITER)
                  .put(commandType)
                  .put((byte) payload.length)
                  .put(payload)
                  .put(checksum)
                  .put(END_DELIMITER);

       return frameBuffer.array();
   }

   /**
    * Représente une trame décodée.
    */
   public static class DecodedFrame {
       public final byte commandType;
       public final byte[] payload;
       public final boolean isValid;

       public DecodedFrame(byte commandType, byte[] payload, boolean isValid) {
           this.commandType = commandType;
           this.payload = payload;
           this.isValid = isValid;
       }

       @Override
       public String toString() {
           return "DecodedFrame{" +
                  "commandType=0x" + String.format("%02X", commandType) +
                  ", payload=" + Arrays.toString(payload) +
                  ", isValid=" + isValid +
                  '}';
       }
   }

   /**
    * Analyse une trame d'octets complète et en extrait les informations.
    * @param frameBytes La trame d'octets complète à analyser.
    * @return Un objet DecodedFrame contenant le type de commande, la charge utile et l'état de validation.
    */
   public static DecodedFrame parseFrame(byte[] frameBytes) {
       if (frameBytes == null || frameBytes.length < 5) { // Taille minimale: START + COMMAND + LENGTH + CHECKSUM + END
           return new DecodedFrame((byte) 0, new byte[0], false); // Trame trop courte
       }

       // Vérifier les délimiteurs de début et de fin
       if (frameBytes[0] != START_DELIMITER || frameBytes[frameBytes.length - 1] != END_DELIMITER) {
           System.err.println("Erreur de délimiteurs de trame.");
           return new DecodedFrame((byte) 0, new byte[0], false);
       }

       byte commandType = frameBytes[1];
       int payloadLength = Byte.toUnsignedInt(frameBytes[2]); // La longueur est un octet non signé

       if (frameBytes.length != (5 + payloadLength)) {
           System.err.println("Erreur: Longueur de trame incorrecte. Attendue: " + (5 + payloadLength) + ", Reçue: " + frameBytes.length);
           return new DecodedFrame(commandType, new byte[0], false);
       }
       
       // Extraire la charge utile
       byte[] payload = new byte[payloadLength];
       System.arraycopy(frameBytes, 3, payload, 0, payloadLength);

       byte receivedChecksum = frameBytes[3 + payloadLength];

       // Recalculer le checksum
       byte[] dataForChecksum = ByteBuffer.allocate(1 + 1 + payloadLength)
                                           .put(commandType)
                                           .put((byte) payloadLength)
                                           .put(payload)
                                           .array();
       byte calculatedChecksum = calculateXORChecksum(dataForChecksum);

       if (receivedChecksum != calculatedChecksum) {
           System.err.println("Erreur de checksum. Reçu: 0x" + String.format("%02X", receivedChecksum) +
                              ", Calculé: 0x" + String.format("%02X", calculatedChecksum));
           return new DecodedFrame(commandType, payload, false);
       }

       return new DecodedFrame(commandType, payload, true);
   }
}

Classe pour la Gestion de la Communication Série (SerialPortManager)

Cette classe gère l'ouverture, la fermeture et l'échange de trames via le port série. Elle utilise FrameAssembler pour la logique protocolaier.


import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.io.ByteArrayOutputStream; // Nécessaire pour l'implémentation de MockSerialPort

// Interface simplifiée pour le port série, à remplacer par votre bibliothèque spécifique (ex: jssc, purejavacomm, ou une implémentation Android)
interface ISerialPort {
   InputStream getInputStream();
   OutputStream getOutputStream();
   void openPort() throws IOException;
   void closePort() throws IOException;
   void setParams(int baudRate, int dataBits, int stopBits, int parity) throws IOException;
}

// Classe de simulation pour l'exemple, NE PAS UTILISER EN PRODUCTION
class MockSerialPort implements ISerialPort {
   protected byte[] mockInputBuffer;
   protected int readPointer = 0;
   protected ByteArrayOutputStream outputBuffer = new ByteArrayOutputStream();

   public MockSerialPort(byte[] simulatedInput) {
       this.mockInputBuffer = simulatedInput;
   }

   @Override
   public InputStream getInputStream() {
       return new InputStream() {
           @Override
           public int read() throws IOException {
               if (readPointer < mockInputBuffer.length) {
                   return Byte.toUnsignedInt(mockInputBuffer[readPointer++]);
               }
               // Simuler un blocage ou -1 si plus de données
               try {
                   Thread.sleep(100); // Simuler un délai de lecture
               } catch (InterruptedException e) {
                   Thread.currentThread().interrupt();
               }
               return -1; // End of stream or no data
           }

           @Override
           public int read(byte[] b, int off, int len) throws IOException {
               if (readPointer >= mockInputBuffer.length) {
                   return -1;
               }
               int bytesAvailable = mockInputBuffer.length - readPointer;
               int bytesToRead = Math.min(len, bytesAvailable);
               System.arraycopy(mockInputBuffer, readPointer, b, off, bytesToRead);
               readPointer += bytesToRead;
               return bytesToRead;
           }

           @Override
           public int available() throws IOException {
               return mockInputBuffer.length - readPointer;
           }
       };
   }

   @Override
   public OutputStream getOutputStream() {
       return outputBuffer;
   }

   @Override
   public void openPort() throws IOException {
       System.out.println("MockSerialPort: Port ouvert.");
   }

   @Override
   public void closePort() throws IOException {
       System.out.println("MockSerialPort: Port fermé. Données envoyées: " + Arrays.toString(outputBuffer.toByteArray()));
   }

   @Override
   public void setParams(int baudRate, int dataBits, int stopBits, int parity) throws IOException {
       System.out.println("MockSerialPort: Paramètres définis: " + baudRate + " bauds, " + dataBits + " bits de données, " + stopBits + " bit d'arrêt, parité " + (parity == 0 ? "aucune" : "spécifiée"));
   }
}


public class SerialPortManager {
   private ISerialPort serialPort; // Utilisez votre véritable implémentation ici
   private InputStream inputStream;
   private OutputStream outputStream;
   private final String portPath;
   private final int baudRate;

   // Définir les types de commandes pour l'application
   public static final byte COMMAND_SEND_DATA = 0x01;
   public static final byte COMMAND_REQUEST_STATUS = 0x02;
   public static final byte COMMAND_ACK = 0x06; // Accusé de réception

   public SerialPortManager(String portPath, int baudRate) {
       this.portPath = portPath;
       this.baudRate = baudRate;
   }

   public void openConnection() throws IOException {
       // En production: instanciez votre SerialPort réel ici. Ex: this.serialPort = new VotreSerialPortClass(portPath);
       // Pour la démo, nous utilisons MockSerialPort pour simuler
       this.serialPort = new MockSerialPort(new byte[0]); // Initialiser avec un buffer vide pour l'entrée
       serialPort.openPort();
       serialPort.setParams(baudRate, 8, 1, 0); // 8 bits, 1 stop bit, pas de parité
       inputStream = serialPort.getInputStream();
       outputStream = serialPort.getOutputStream();
       System.out.println("Connexion au port série " + portPath + " établie.");
   }

   public void closeConnection() {
       if (serialPort != null) {
           try {
               if (inputStream != null) inputStream.close();
               if (outputStream != null) outputStream.close();
               serialPort.closePort();
               System.out.println("Connexion au port série fermée.");
           } catch (IOException e) {
               System.err.println("Erreur lors de la fermeture du port série: " + e.getMessage());
           }
       }
   }

   /**
    * Envoie un message via le port série en utilisant le protocole personnalisé.
    * @param commandType Le type de commande à envoyer.
    * @param data La charge utile (payload) à inclure.
    * @throws IOException Si une erreur d'E/S se produit.
    */
   public void sendMessage(byte commandType, byte[] data) throws IOException {
       byte[] frame = FrameAssembler.buildFrame(commandType, data);
       if (frame != null) {
           outputStream.write(frame);
           outputStream.flush(); // S'assurer que les données sont écrites immédiatement
           System.out.println("Message envoyé: " + Arrays.toString(frame));
       } else {
           System.err.println("Impossible de construire la trame pour l'envoi.");
       }
   }

   /**
    * Lit et décode la prochaine trame complète du port série.
    * Cette implémentation est simplifiée : elle recherche un délimiteur de début,
    * puis collecte tous les octets suivants jusqu'à ce qu'un délimiteur de fin soit trouvé ou un timeout.
    * Une implémentation réelle devrait utiliser un thread dédié pour la lecture asynchrone,
    * un buffer circulaire pour assembler les octets et une machine à états pour une robustesse accrue.
    * @return Une DecodedFrame si une trame valide est reçue, ou null en cas d'erreur/timeout ou si aucun octet n'est lu.
    * @throws IOException Si une erreur d'E/S se produit.
    */
   public FrameAssembler.DecodedFrame readNextFrame() throws IOException {
       ByteArrayOutputStream frameBuffer = new ByteArrayOutputStream();
       int b;
       long startTime = System.currentTimeMillis();
       final long READ_BYTE_TIMEOUT_MS = 500; // Timeout pour la lecture d'un seul octet
       final long FRAME_COMPLETE_TIMEOUT_MS = 2000; // Timeout pour assembler une trame complète après le début

       // Attendre le délimiteur de début
       while (System.currentTimeMillis() - startTime < FRAME_COMPLETE_TIMEOUT_MS) {
           if (inputStream.available() > 0) {
               b = inputStream.read();
               if (b == -1) continue; // Pas de données réelles pour l'instant
               if ((byte) b == FrameAssembler.START_DELIMITER) {
                   frameBuffer.write(b);
                   break; // Début de trame trouvé
               }
           } else {
               try {
                   Thread.sleep(10); // Attendre que des données soient disponibles
               } catch (InterruptedException e) {
                   Thread.currentThread().interrupt();
                   return null;
               }
           }
       }

       if (frameBuffer.size() == 0) { // Pas de délimiteur de début trouvé dans le délai imparti
           System.out.println("Timeout: Aucun délimiteur de début trouvé.");
           return null;
       }
       
       startTime = System.currentTimeMillis(); // Réinitialiser le timeout pour le reste de la trame

       // Lire le reste de la trame jusqu'au délimiteur de fin ou timeout
       while (System.currentTimeMillis() - startTime < FRAME_COMPLETE_TIMEOUT_MS) {
           if (inputStream.available() > 0) {
               b = inputStream.read();
               if (b == -1) continue;
               frameBuffer.write(b);
               if ((byte) b == FrameAssembler.END_DELIMITER) {
                   // Fin potentielle de trame, essayer de la parser
                   FrameAssembler.DecodedFrame decoded = FrameAssembler.parseFrame(frameBuffer.toByteArray());
                   if (decoded.isValid) {
                       System.out.println("Trame reçue: " + Arrays.toString(frameBuffer.toByteArray()));
                       return decoded;
                   } else {
                       // Trame invalide (ex: checksum incorrect, longueur ne correspond pas).
                       // Dans un scénario réel, il faudrait une gestion plus sophistiquée des erreurs
                       // (ex: rejeter la trame et continuer à chercher un nouveau START_DELIMITER).
                       // Pour cet exemple, nous réinitialisons le buffer et continuons à chercher la prochaine trame.
                       System.err.println("Trame reçue invalide, réinitialisation du buffer et recherche d'une nouvelle trame.");
                       frameBuffer.reset();
                       // Recommence la recherche depuis le début.
                       return readNextFrame(); // Récursive, mais attention au StackOverflow en cas de flux bruité continu.
                   }
               }
           } else {
               try {
                   Thread.sleep(10);
               } catch (InterruptedException e) {
                   Thread.currentThread().interrupt();
                   return null;
               }
           }
       }
       System.err.println("Timeout: Trame incomplète ou corrompue.");
       return null;
   }

   // --- Méthode main pour l'exemple d'utilisation ---
   public static void main(String[] args) {
       String portToUse = "/dev/ttyUSB0"; // Chemin du port série (peut être un chemin virtuel pour Android)
       int baud = 9600;

       SerialPortManager manager = new SerialPortManager(portToUse, baud);

       try {
           manager.openConnection();

           // 1. Exemple d'envoi de données
           byte[] dataToSend = {0x10, 0x20, 0x30, 0x40};
           manager.sendMessage(COMMAND_SEND_DATA, dataToSend);

           // 2. Simuler une réponse du périphérique pour tester la réception
           // En production, cette réponse proviendrait du périphérique réel.
           byte[] responsePayload = {0x01, 0x02, 0x03, 0x04, 0x05};
           byte[] simulatedResponseFrame = FrameAssembler.buildFrame(COMMAND_ACK, responsePayload);

           // "Injecter" la trame simulée dans le MockSerialPort pour la lecture
           if (manager.serialPort instanceof MockSerialPort) {
               ((MockSerialPort) manager.serialPort).mockInputBuffer = simulatedResponseFrame;
               ((MockSerialPort) manager.serialPort).readPointer = 0; // Réinitialiser le pointeur de lecture
           }

           System.out.println("\nTentative de lecture de la réponse...");
           FrameAssembler.DecodedFrame receivedFrame = manager.readNextFrame();

           if (receivedFrame != null) {
               System.out.println("Trame reçue et décodée: " + receivedFrame);
               if (receivedFrame.commandType == COMMAND_ACK) {
                   System.out.println("Accusé de réception (ACK) reçu avec payload: " + Arrays.toString(receivedFrame.payload));
               }
           } else {
               System.out.println("Aucune trame valide reçue.");
           }

       } catch (IOException e) {
           System.err.println("Erreur de communication série: " + e.getMessage());
           e.printStackTrace();
       } finally {
           manager.closeConnection();
       }
   }
}

Étiquettes: Android Communication Série Protocole Personnalisé Java iot

Publié le 22 juin à 02h02