Développement d'un logiciel de test pour la communication Modbus TCP en C#

I. Configuration de l'environnement et des dépendances

Pour ce projet, nous utilisons :

  • Visual Studio 2022 ou supérieur
  • .NET 6.0 ou version ultérieure
  • Packages NuGet : NModbus pour l'implémentation du protocole, MaterialDesignThemes pour l'interface utilisateur

La structure du projet est organisée comme suit :

ModbusTesterApp/
├── Modèles/            # Modèles de données
├── Vues/               # Interface WPF
├── ModèlesVue/         # Logique MVVM
├── Services/           # Services de communication
└── Utilitaires/        # Classes utilitaires (vérification CRC, journalisation)

II. Implémentation du code principal

1. Service de communication Modbus TCP

// Services/ServiceModbusTcp.cs
using Modbus.Device;
using System.Net.Sockets;

public class ServiceModbusTcp : IDisposable
{
    private ModbusIpMaster _maitreModbus;
    private TcpClient _clientTcp;

    public async Task ConnexionAsync(string adresseIp, int port = 502)
    {
        _clientTcp = new TcpClient();
        await _clientTcp.ConnectAsync(adresseIp, port);
        _maitreModbus = ModbusIpMaster.CreateIp(_clientTcp);
    }

    // Lecture des registres de maintien (code fonction 03)
    public ushort[] LireRegistresMaintien(ushort adresseDebut, ushort quantite)
    {
        return _maitreModbus.ReadHoldingRegisters(1, adresseDebut, quantite);
    }

    // Écriture dans un registre unique (code fonction 06)
    public void EcrireRegistreUnique(ushort adresse, ushort valeur)
    {
        _maitreModbus.WriteSingleRegister(1, adresse, valeur);
    }

    // Écriture multiple dans des registres (code fonction 16)
    public void EcrireRegistresMultiples(ushort adresseDebut, ushort[] valeurs)
    {
        _maitreModbus.WriteMultipleRegisters(1, adresseDebut, valeurs);
    }

    public void Dispose()
    {
        _maitreModbus?.Dispose();
        _clientTcp?.Close();
    }
}

2. Conception du modèle de données

// Modèles/DonneeRegistre.cs
public class DonneeRegistre : INotifyPropertyChanged
{
    private ushort _valeurRegistre;
    public ushort ValeurRegistre
    {
        get => _valeurRegistre;
        set
        {
            _valeurRegistre = value;
            OnPropertyChanged(nameof(ValeurRegistre));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged(string nomPropriete) =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nomPropriete));
}

III. Création de l'interface WPF

1. XAML de la fenêtre principale


<window height="450" title="Outil de test Modbus TCP" width="800" x:class="ModbusTesterApp.Vues.FenetrePrincipale" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:materialdesign="http://materialdesigninxaml.net/winfx/xaml/themes">
    <grid>
        
        <groupbox header="Paramètres de connexion" margin="10">
            <stackpanel orientation="Vertical">
                <textbox materialdesign:hintassist.hint="Adresse IP" width="200" x:name="champAdresseIP"></textbox>
                <textbox materialdesign:hintassist.hint="Port" width="100" x:name="champPort"></textbox>
                <button command="{Binding CommandeConnexion}" content="Se connecter"></button>
            </stackpanel>
        </groupbox>

        
        <groupbox header="Opérations sur les registres" margin="10,120,10,10">
            <stackpanel orientation="Vertical">
                <textbox materialdesign:hintassist.hint="Adresse de départ" width="100" x:name="champAdresseDebut"></textbox>
                <textbox materialdesign:hintassist.hint="Nombre de registres" width="100" x:name="champQuantite"></textbox>
                <button command="{Binding CommandeLectureRegistres}" content="Lire les registres"></button>
                <datagrid autogeneratecolumns="False" itemssource="{Binding ListeRegistres}">
                    <datagrid.columns>
                        <datagridtextcolumn binding="{Binding Adresse}" header="Adresse"></datagridtextcolumn>
                        <datagridtextcolumn binding="{Binding Valeur}" header="Valeur"></datagridtextcolumn>
                    </datagrid.columns>
                </datagrid>
            </stackpanel>
        </groupbox>
    </grid>
</window>

2. Logique du ViewModel

// ModelesVue/ModeleVuePrincipal.cs
using MaterialDesignThemes.Wpf;
using System.Windows.Input;

public class ModeleVuePrincipal : ObservableObject
{
    private readonly ServiceModbusTcp _serviceModbus = new ServiceModbusTcp();
    public ObservableCollection<donneeregistre> ListeRegistres { get; } = new();

    public ICommand CommandeConnexion => new RelayCommand(async () => 
    {
        await _serviceModbus.ConnexionAsync(champAdresseIP.Text, int.Parse(champPort.Text));
        EffectuerLectureRegistres();
    });

    public ICommand CommandeLectureRegistres => new RelayCommand(() =>
    {
        var valeurs = _serviceModbus.LireRegistresMaintien(0, 10);
        ListeRegistres.Clear();
        for (int index = 0; index < valeurs.Length; index++)
        {
            ListeRegistres.Add(new DonneeRegistre { 
                Adresse = 0 + index, 
                Valeur = valeurs[index] 
            });
        }
    });

    private void EffectuerLectureRegistres()
    {
        // Logique de lecture asynchrone
    }
}</donneeregistre>

IV. Débogage et optimisations

Pour la vérification de l'intégrité des données, voici une méthode de calcul de CRC :

// Utilitaires/VerificationIntegrite.cs
public static byte[] CalculerCrc(byte[] donnees)
{
    ushort crc = 0xFFFF;
    foreach (byte octet in donnees)
    {
        crc ^= (ushort)octet;
        for (int iteration = 0; iteration < 8; iteration++)
        {
            if ((crc & 0x0001) != 0)
                crc >>= 1;
            else
                crc = (ushort)((crc >> 1) ^ 0xA001);
        }
    }
    return new[] { (byte)crc, (byte)(crc >> 8) };
}

Pour la gestion des erreurs, une approche robuste est essentielle :

try
{
    await _serviceModbus.ConnexionAsync(adresseIp, port);
}
catch (SocketException exception)
{
    AfficherMessageErreur($"Échec de la connexion : {exception.Message}");
    Journal.Erreur(exception, "Problème réseau");
}

Optimisations recommandées :

  • Gestion d'un pool de connexions pour réutiliser les instances TcpClient
  • Privilégier les opérations en lot comme EcrireRegistresMultiples pour réduire les appels
  • Utilisation de async/await pour préserver la réactivité de l'interface

V. Fonctionnalités supplémentaires

Pour la surveillance graphique, l'intégration d'une bibliothèque comme LiveCharts est utile :

// Suivi de courbes en temps réel
public SeriesCollection CollectionSeries { get; } = new();

private void MettreAJourGraphique(double valeur)
{
    CollectionSeries.Add(new LineSeries
    {
        Values = new ChartValues<double> { valeur }
    });
}</double>

Pour inspecter les trames brutes, un affichage hexadécimal pratique :

public string AffichageHexadecimal
{
    get
    {
        return BitConverter.ToString(donneesBrutes).Replace("-", "");
    }
}

VI. Exemples de cas de test

Scénario Paramètres Résultat attendu
Lecture de registres de maintien IP=192.168.1.100, Port=502, Adresse=0, Quantité=10 10 valeurs hexadécimales retournées
Écriture dans un registre unique Adresse=40001, Valeur=25 Registre mis à jour à 0x19
Écriture en lot Adresse=40002, Tableau de valeurs=[10,20,30] Trois registres modifiés avec succès

VII. Conseils pour le déploiement

Pour préparer l'application :

  • Utiliser ILMerge pour combiner les DLL et inclure la licence NModbus
  • Créer un installeur avec Inno Setup ou un outil similaire

Exemple de configuraton pour Inno Setup :

[Fichiers]
Source: "MonApplicationModbus.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "NModbus.dll"; DestDir: "{app}"; Flags: ignoreversion

Étiquettes: Modbus TCP C# WPF MVVM NModbus

Publié le 2 juin à 00h34