Communications UDP

Vous pouvez télécharger l'intégralité du code source de ce projet : udp_ds3231.tar.gz


L'exemple Horloge temps réel (DS 3231) est limité du fait qu'il ne donne aucun moyen de mettre l'horloge temps réel à jour. Seules possibbilités, donner une mise àjour approximative par codage en dur dans le code. Ou, brancher le circuit DS3231 sur une autre machine qui le mettrai à jour, puis le rebrancher sur le Pico. Comme le circuit dispose d'une pile bouton, la mise à jour serait gardée. Ce n'est pas très pratique !


L'exemple donné ci-dessous complète donc l'exemple Horloge temps réel (DS 3231) en lui adjoignant un service de communication UDP qui permet de mettre le circuit DS3231 et/ou de lire la date et l'heure.


Bien sur cet exemple n'est valide que pour un Pico W. Pour appréhender les librairies d'interfaces réseaux du Pico W, il est recommandé de lire d'abord l'exemple Connexion au réseau local.


Cet exemple sera complété d'un extrait de code C++ sous Qt qui pourra mettre à jour le circuit DS3231 et lire la date et l'heure via le réseau.


Bref, on est alors en présence d'un mini serveur UDP et de son client. Un mini service NTP pour la maison.

Code source

Le code source complète celui de l'exemple Horloge temps réel (DS 3231) en lui adjoignant une classe nommée "CUDP_RTC" implémentée par les fichiers "cudp_rtc.h" et "cudp_rtc.cpp". Le fichier "pico_ds3231.cpp" est modifié et renommé "pico_udp_ds3231.cpp". Ce sera également le nom du projet. Le fichier "CMakeLists.txt" est mis à jour pour le support du Pico W, des librairies réseaux et conformément à ce nouveau nom de projet. Les fichiers "ds3231.h" et "ds3231.cpp" de l'exemple Horloge temps réel (DS 3231) sont conservés sans aucune modification.

CMakeLists.txt

 - Note : Notez la présence de la ligne "set(PICO_BOARD pico_w)" car ce projet ne peut fonctionner que sur un Pico W.

 - Note : Notez l'ajout de "pico_cyw43_arch_lwip_threadsafe_background" dans la section "target_link_libraries".


cmake_minimum_required(VERSION 3.13)

set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)

# Initialise pico_sdk from installed location
# (note this can come from environment, CMake cache etc)
set(PICO_SDK_PATH "/home/alain/pico/pico-sdk")

set(PICO_BOARD pico_w)

# Pull in Raspberry Pi Pico SDK (must be before project)
include($ENV{PICO_SDK_PATH}/external/pico_sdk_import.cmake)

if (PICO_SDK_VERSION_STRING VERSION_LESS "1.4.0")
message(FATAL_ERROR "Raspberry Pi Pico SDK version 1.4.0 (or later) required. Your version is ${PICO_SDK_VERSION_STRING}")
endif()

project(pico_udp_ds3231 C CXX ASM)

# Initialise the Raspberry Pi Pico SDK
pico_sdk_init()

# Add executable. Default name is the project name, version 0.1
add_executable(pico_udp_ds3231
  pico_udp_ds3231.cpp
  ds3231.cpp
  cudp_rtc.cpp)

pico_set_program_name(pico_udp_ds3231 "pico_udp_ds3231")
pico_set_program_version(pico_udp_ds3231 "0.1")

pico_enable_stdio_uart(pico_udp_ds3231 1)
pico_enable_stdio_usb(pico_udp_ds3231 0)

pico_add_extra_outputs(pico_udp_ds3231)

# Add the standard include files to the build
target_include_directories(pico_udp_ds3231 PRIVATE
${CMAKE_CURRENT_LIST_DIR}
${CMAKE_CURRENT_LIST_DIR}/.. # for our common lwipopts or any other standard includes, if required
)

# Add any user requested libraries
target_link_libraries(pico_udp_ds3231
  pico_stdlib
  hardware_i2c
  pico_cyw43_arch_lwip_threadsafe_background
)

Classe CUDP_RTC

Dans ce code, vous devrez remplacer les chaines "ssid" et "password" par le nom de votre réseau local et son mot de passe.


L'utilisation de la constante "CYW43_COUNTRY_WORLDWIDE" à la ligne "cyw43_arch_init_with_country" n'est pas standard. Il aurait fallu utiliser la constante "CYW43_COUNTRY_FRANCE". J'ai cependant constaté que l'utilisation de cette dernière constante conduisait à des echecs sur certains réseaux wifi alors que la constante "CYW43_COUNTRY_WORLDWIDE" semble toujours donné un résultat de connection correcte.


L'adresse IP du pico est connue grâce à la ligne 69 du fichier "cudp_rtc.cpp" : "m_MyAddr = cyw43_state.netif[0].ip_addr;". Elle a été fournie par un DHCP. Le port d'écoute UDP du Pico est fixé à 3333 par la variable statique constante "m_nMyPort". Vous devrez modifier sa valeur selon vos convenances à la ligne 17 du fichier "cudp_rtc.cpp" : "const int CUDP_RTC::m_nMyPort = 3333".


L'adresse et le port du "client" sont connus via les paramètres du constructeur de la classe. Ce constructeur est appelé via la "factory" de la classe par la fonction "main" du fichier "pico_udp_ds3231.cpp" à la ligne 36. Dans cet exemple, l'adresse est fixée à 192.168.0.60 et le port à 2222. Vous pouvez modifier ces valeurs selon votre convenance.


La méthode "login" connecte le Pico au réseau. Elle déclare deux instances de communication UDP aux lignes 72 et 75. L'une pour la réception des données, l'autre pour l'émission de données. L'instance de réception est ensuite reliée à l'adresse du Pico et à son port d'écoute UDP (3333). Enfin elle indique à la librarie "lwIP" la fonction de "callback" à appeler lorsque des données sont reçues en UDP.


A propos de cette callback : Comme nous utilisons la librairie "pico_cyw43_arch_lwip_threadsafe_background" (Voir CMakeLists.txt), celle-ci peut nous prévenir quand des données sont reçues. Il est donc inutile d'interroger périodiquement la librairie pour savoir si des données sont disponibles en réception. Lorsque des données sont disponibles, la librairie appelle une fonction de callback que nous lui précisons via la ligne 84 : "udp_recv(m_pUDPReceiver, udp_readyread_relayer, NULL);". La fonction de callback se nomme "udp_readyread_relayer". Elle ne peut être qu'externe à la classe car la librarie ne connait pas cette classe, alors qu'elle définie un typedef pour ce type de fonctions. Cette fonction est déclarée comme statique afin de ne pas pouvoir être appelée par un fichier externe. Afin que le travail se passe dans la classe, elle appelle une fonction privée de la classe nommée "udp_readyread", qui fait le travail d'exploitation des données. Comme cette fonction est également privée, on déclare dans la classe la fonction externe "udp_readyread_relayer" comme "friend". Voir les lignes 20 et 41 à 45 du fichier ""pico_udp_ds3231.h".


Bien evidemment, les données échangées entre le Pico et son client doivent respecter un formatage précis. Lorsque le client souhaite mettre à jour l'horloge RTC du client il envoie une chaine commençant par "RTC suivi de 7 valeurs entières sous forme de chaines de caractères, toutes séparées par "!". Enfin la chaine est terminée par "END\0". La méthode "udp_readyread" exploite ce format pour mettre à jour le DS3231. Lorsque le client souhaite lire la date et l'heure courante du Pico, il evnvoie la chaine "?RTC!END\0". A la réception de cette commande, la fonction "udp_readyread" prépare une chaine commençant par "RTC" suivi de 6 valeurs entières sous forme de chaines de caractères, toutes séparées par "!". Il n'y a que 6 champs car le numéro du jour de la semaine est inutile. A titre d'exemple :


  - Pour mettre à jour le Pico à la date du Mardi 23 mai 2023 à 9:45:30, il envoie la chaine : "RTC!23!5!2023!2!9!45!30!END\0",

 - Lorsque le pico renvoie une date et heure équivalente, il emmet vers le client la chaine : RTC!23!5!2023!9!45!30\0".


Il est à noter, que comme la classe "CDS3231", la classe "CUDP_RTC" est une classe singleton. Elle ne peut être instantiée qu'une seule fois. Son constructeur est privé. On ne peut créer cette classe que par un premier appel à la fonction "UDP_RTC". Il n'est pas nécessaire de garder un pointeur vers l'objet que retourne cette fonction car cette dernière, si la classe est instanciée renverra toujours le même pointeur. Pour simplifier la syntaxe on déclare une macro "D_UDP" pour l'appel de cette fonction.


Finalement, remarquez que tous les appels de fonctions issues de la librairie "lwIP" sont précédées d'un appel à la fonction "cyw43_arch_lwip_begin()" et suivies d'un appel à la fonction "cyw43_arch_lwip_end()".


Fichier cudp_rtc.h


/*! \file cudp_rtc.h.h
* \brief Fichier d'entête de la classe CUDP_RTC pour utlisation sur un Raspbery Pi Pico W
* \author WickedCpp
* \date 23 mai 2023
*/

#ifndef CUDP_RTC_H
#define CUDP_RTC_H

// Include lwIP headers
#include "pico/cyw43_arch.h"
#include "lwip/pbuf.h"
#include "lwip/udp.h"
#include "lwip/ip_addr.h"

// Raccourci d'accès au singleton
#define D_UDP CUDP_RTC::UDP_RTC()

// The private callback relayer called when some UDP data are received
static void udp_readyread_relayer(void *arg, struct udp_pcb *pcb, struct pbuf *p, const struct ip4_addr *addr, short unsigned int port);

// La classe singleton CUDP_RTC
class CUDP_RTC
{
// Factory d'un singleton
public:
static CUDP_RTC* UDP_RTC(char* pIPClient = nullptr, int nPortClient = 2222);

private:
  CUDP_RTC(char* pIPClient, int nPortClient);
  static CUDP_RTC* m_pInstance;

// Public methods
public:
  bool login();
  void logout();
  char* IPAddress();
  int port();

// The callback called when some UDP data are received
private:
// Make the callback relayer a friend
  friend void udp_readyread_relayer(void *arg, struct udp_pcb *pcb, struct pbuf *p, const struct ip4_addr *addr, short unsigned int port);
// The method itself
  void udp_readyread(void *arg, struct udp_pcb *pcb, struct pbuf *p, const struct ip4_addr *addr, short unsigned int port);

// Private methods
private:
  bool SendUDP(char* payload);

// Private members to identify the guy we talk to....
private:
  char m_IPClient[16];
  ip_addr_t m_ClientAddr;
  int m_nClientPort;
  bool m_bValidClient;

  ip_addr_t m_MyAddr;
  static const int m_nMyPort;

  bool m_tfLogged;

  struct udp_pcb* m_pUdpSender;
  struct udp_pcb* m_pUdpReceiver;

};

#endif // CUDP_RTC_H

Fichier cudp_rtc.cpp


/*! \file cudp_rtc.cpp
* \brief Fichier d'implémentation de la classe CUDP_RTC pour utlisation sur un Raspbery Pi Pico W
* \author WickedCpp
* \date 23 mai 2023
*/

// Standard C/C++ includes
#include ‹string.h›

// Inclusion de la classe
#include "cudp_rtc.h"

// Inclusion de la classe CDS3231
#include "ds3231.h"

// Notre port d'écoute est fixé à 3333
const int CUDP_RTC::m_nMyPort = 3333;

// Au démarrage l'instance du singleton est nulle
CUDP_RTC* CUDP_RTC::m_pInstance = nullptr;

// La factory
CUDP_RTC* CUDP_RTC::UDP_RTC(char* pIPClient, int nPortClient)
{
  // Retourne une instance déjà existante
  if(m_pInstance != nullptr) return m_pInstance;

  // Si il n'y a pas encore d'instance, on la drée et on la retourne
  m_pInstance = new CUDP_RTC(pIPClient, nPortClient);
  return m_pInstance;
}

// Constructeur privé
CUDP_RTC::CUDP_RTC(char* pIPClient, int nPortClient)
{
  // Remember the clientIP address
  strcpy(m_IPClient, pIPClient);

  // Convert the human readable IP adress of the guy we want to talk to to a binary equivalent for lwIP
  m_bValidClient = ip4addr_aton(pIPClient, &m_ClientAddr) == 1 ? true : false;

  // Remember the client port
  m_nClientPort = nPortClient;

  // Initialize our pointer to null
  m_pUdpSender = nullptr;
  m_pUdpReceiver = nullptr;

  // Say we are not yet connected
  m_tfLogged = false;
}

// Fonction de log sur le réseau
// Crée également l'émetteur DP et le receveurr UDP
// Relie le receveur UDP à notre adresse IP et à notre port découte UDP
// Définie la fonction de callback lorsque des données sont reçues en UDP
bool CUDP_RTC::login()
{
  // Initialize the WIFI country
  if(cyw43_arch_init_with_country(CYW43_COUNTRY_WORLDWIDE)) return false;

  // Enable the WIFI
  cyw43_arch_enable_sta_mode();

  // Connect to the WIFI SSID with a 10 seconds timeout
  if(cyw43_arch_wifi_connect_timeout_ms("ssid", "password", CYW43_AUTH_WPA2_AES_PSK, 10000)) return false;

  // Remeber my own IP address
  m_MyAddr = cyw43_state.netif[0].ip_addr;

  // Declare a UDP to talk to the guy defined in the constructor
  m_pUdpSender = udp_new();

  // Declare a UDP to receive data from the guy defined in the constructor
  m_pUdpReceiver = udp_new();

  // bind the receiver UDP to my IP address on port 3333
  cyw43_arch_lwip_begin();
  if(udp_bind(m_pUdpReceiver, &m_MyAddr, m_nMyPort) != ERR_OK) return false;
  cyw43_arch_lwip_end();

  // set the callback function for received packets on the receiver UDP
  cyw43_arch_lwip_begin();
  udp_recv(m_pUdpReceiver, udp_readyread_relayer, NULL);
  cyw43_arch_lwip_end();

  return m_tfLogged = true;
}

// Fonction de déconnection réseau
void CUDP_RTC::logout()
{
  udp_remove(m_pUdpReceiver);
  udp_remove(m_pUdpSender);
  cyw43_arch_deinit();
  m_tfLogged = false;
}

// Retourne l'adress IP du pico sous forme compréhensible
char* CUDP_RTC::IPAddress()
{
  return ipaddr_ntoa(((const ip_addr_t *)&cyw43_state.netif[0].ip_addr));
}

// Retourne le port d'écoute UDP du pico
int CUDP_RTC::port()
{
  return m_nMyPort;
}

// Fonction appelée par la callback de réception des données UDP pour traiter les données reçues
void CUDP_RTC::udp_readyread(void *arg, struct udp_pcb *pcb, struct pbuf *p, const struct ip4_addr *addr, short unsigned int port)
{
  if (p != NULL)
  {
    // Extract Payload received
    char* Payload = (char *)malloc((p->len + 2) * sizeof(char));
    memcpy(Payload, p->payload, p->len);

    // split the received paylooad
    char* Token;
    char delim[] = "!";
    Token = strtok(Payload, delim);

    // Check if we received a RTC update
    if(strcmp(Token, "RTC") == 0)
  {
      printf("- Réception mise à jour RTC\n");
      int nValues[7];
      int nElem = 0;
      while(Token != NULL && nElem ‹ 7)
      {
        Token = strtok(NULL, delim);
        nValues[nElem] = atoi(Token);
        nElem++;
      }
      struct DS3231DateTime dt;
      dt.DS3231_Year = nValues[0];
      dt.DS3231_Month = nValues[1];
      dt.DS3231_Day = nValues[2];
      dt.DS3231_Weekday = nValues[3] + 1; // Because day of week count is base 0 on Qt versus base 1 on DS3231
      dt.DS3231_Hour = nValues[4];
      dt.DS3231_Minute = nValues[5];
      dt.DS3231_Second = nValues[6];
      // Apply new RTC value
      D_RTC->setDateTime(dt);
      // Display the new time
      D_RTC->getDateTime(dt);
      printf("- Mise à jour : %s - %s\n",CDS3231::toLongDateString(dt), CDS3231::toTimeString(dt));
    }

    // Check if we received a ask for RTC Value
    if(strcmp(Token, "?RTC") == 0)
    {
      printf("- Réception lecture RTC\n");
      printf("- Envoi de la réponse\n");

      char strDT[80];
      struct DS3231DateTime dt;
      D_RTC->getDateTime(dt);
      sprintf(strDT,"RTC!%d!%d!%d!%d!%d!%d",
          dt.DS3231_Year,
          dt.DS3231_Month,
          dt.DS3231_Day,
          dt.DS3231_Hour,
          dt.DS3231_Minute,
          dt.DS3231_Second);

      SendUDP(strDT);
    }
    // Free memory
    free(Payload);
    pbuf_free(p);
  }
}

// Fonction utiltaire permetant d'emettre une chaine en UDP
bool CUDP_RTC::SendUDP(char* payload)
{
  if(!m_tfLogged) return false;

  int payloadSize = sizeof(char) * (strlen(payload) + 1);

  struct pbuf * p = pbuf_alloc(PBUF_TRANSPORT, payloadSize, PBUF_RAM);
  memcpy(p->payload, payload, payloadSize);

  cyw43_arch_lwip_begin();
  err_t e = udp_sendto(m_pUdpSender, p, &m_ClientAddr, m_nClientPort);
  cyw43_arch_lwip_end();

  pbuf_free(p);

  return e != ERR_OK ? false : true;
}

// Callback de réception des données UDP : Délègue le travail à la méthode privée udp_readyread de la classe CUDP_RTC
static void udp_readyread_relayer(void *arg, struct udp_pcb *pcb, struct pbuf *p, const struct ip4_addr *addr, short unsigned int port)
{
  cyw43_arch_lwip_begin();
  D_UDP->udp_readyread(arg, pcb, p, addr, port);
  cyw43_arch_lwip_end();
}

Fichier principal : pico_udp_ds3231.cpp

#include ‹stdio.h›
#include "pico/stdlib.h"
#include "ds3231.h" // Inclue également hardware/i2c.h
#include "cudp_rtc.h" // Inbclue également les entêtes de cyw43_arch_* et lcIP

// On utilise le port I2C N°0
#define I2C_PORT i2c0

// Les broches SDA et SCL à attribuer au port I2C N°0
static const uint I2C0_SDA_PIN = 20; // GP20 : SDA du bus I2C N°0
static const uint I2C0_SCL_PIN = 21; // GP21 : SCL du bus I2C N°0

int main()
{
  struct DS3231DateTime dt;
  char strClientIP[] = "192.168.0.60";

  stdio_init_all();

  // Clear the console
  printf("\033[2J");

  // Initialisation du port I2C N°0 sur les broches 20 et 21
  i2c_init(I2C_PORT, 100000); // Bus I2C n°0 à 100 KHz
  gpio_set_function(I2C0_SDA_PIN, GPIO_FUNC_I2C); // GP20 : SDA
  gpio_set_function(I2C0_SCL_PIN, GPIO_FUNC_I2C); // GP21 : SCL
  gpio_pull_up(I2C0_SDA_PIN);
  gpio_pull_up(I2C0_SCL_PIN);

  // On instancie le singleton de la classe CDS3231
  CDS3231::DS3231(I2C_PORT);

  // On instancie le singleton de la classe CUDP_RTC
  // Dans cet exemple le client UDP est à l'adresse 192.168.0.30 et écoute sur le port 2222
  CUDP_RTC::UDP_RTC(strClientIP, 2222);

  // On initialise le composant DS3231
  if(!D_RTC->initialize())
  {
    // Si cela se passe mal on envoie un message sur la console et on sort
    printf("Erreur d'initialisation du composant DS3231\n");
    return 0;
  }

  // On se logue sur le réseau
  printf("Connexion au réseau local en cours...\n");
  if(!D_UDP->login())
  {
    // Si cela se passe mal on envoie un message sur la console et on sort
    printf("Erreur de connexion réseau\n");
    return 0;
  }
  printf("Connecté au réseau. Connection UDP : %s:%d\n", D_UDP->IPAddress(), D_UDP->port());

  // On affiche l'heure toutes les secondes
  while(true)
  {
    sleep_ms(1000);
  }

  D_UDP->logout();
  return 0;
}

Projet Qt : DS3231_UDP : Client UDP

Ce site étant aussi dédié à Qt, vous trouverez ci-après le code de la classe "CUDPConnection" d'un programme de test pour cette application sur le Pico.


Fichier cudpconnection.h


#ifndef CUDPCONNECTION_H
#define CUDPCONNECTION_H

#include ‹QObject›
#include ‹QtNetwork›
#include ‹QDateTime›

#define D_UDP CUDPConnection::UDPConnection()

class CUDPConnection : public QObject
{
  Q_OBJECT

public:
  static CUDPConnection* UDPConnection();

private:
  explicit CUDPConnection(QObject *parent = nullptr);
  static CUDPConnection* m_pInstance;

public:
  bool login();
  void logout();
  void setRTC();
  void readRTC();
  QDateTime getPicoDateTime();

private:
  static const QString m_strMyIPAddress;
  static const QString m_strPicoIPAddress;
  static const int m_nMyPort;
  static const int m_nPicoPort;

  QUdpSocket* m_pUDPSender;
  QUdpSocket* m_pUDPReceiver;
  bool m_bBound;

  QDateTime m_dtPicoDateTime;

signals:
  void connected();
  void picoDateTimeAvailable();

public slots:
  void readUDP();

};

#endif // CUDPCONNECTION_H

Fichier cudpconnection.cpp


#include "cudpconnection.h"

CUDPConnection* CUDPConnection::m_pInstance = nullptr;
const QString CUDPConnection::m_strMyIPAddress = "192.168.0.60";
const QString CUDPConnection::m_strPicoIPAddress = "192.168.0.202";
const int CUDPConnection::m_nMyPort = 2222;
const int CUDPConnection::m_nPicoPort = 3333;

CUDPConnection* CUDPConnection::UDPConnection()
{
  if(m_pInstance != nullptr) return m_pInstance;

  m_pInstance = new CUDPConnection();
  return m_pInstance;
}

CUDPConnection::CUDPConnection(QObject *parent)
  : QObject{parent}
{
  m_pUDPSender = nullptr;
  m_pUDPReceiver = nullptr;
  m_bBound = false;
}

bool CUDPConnection::login()
{
  // Sender
  m_pUDPSender = new QUdpSocket(this);
  m_pUDPSender->connectToHost(QHostAddress(m_strPicoIPAddress), m_nPicoPort);
  if(!m_pUDPSender->waitForConnected(10000))
  {
    delete m_pUDPSender;
    m_pUDPSender = nullptr;
    return false;
  }
  // Receiver
  m_pUDPReceiver = new QUdpSocket(this);
  m_bBound = m_pUDPReceiver->bind(QHostAddress(m_strMyIPAddress), m_nMyPort);
  if(!m_bBound)
  {
    m_pUDPSender->disconnectFromHost();
    delete m_pUDPSender;
    delete m_pUDPReceiver;
    m_pUDPSender = nullptr;
    m_pUDPReceiver = nullptr;
    return false;
  }
  connect(m_pUDPReceiver, SIGNAL(readyRead()), this, SLOT(readUDP()));

  emit connected();

  return true;
}

void CUDPConnection::logout()
{
  if(m_pUDPReceiver != nullptr)
  {
    disconnect(m_pUDPReceiver, SIGNAL(readyRead()), this, SLOT(ReadUDP()));
    delete m_pUDPReceiver;
    m_pUDPReceiver = nullptr;
  }
  if(m_pUDPSender != nullptr)
  {
    m_pUDPSender->disconnectFromHost();
    delete m_pUDPSender;
    m_pUDPSender = nullptr;
  }
}

void CUDPConnection::readUDP()
{
  while(m_pUDPReceiver->hasPendingDatagrams())
  {
    QNetworkDatagram datagram = m_pUDPReceiver->receiveDatagram();

    // Check if we received an understandable string
    QString strPayload = QString(datagram.data().data());

    QStringList lst = strPayload.split('!');

    if(lst.at(0) == "RTC")
    {
      m_dtPicoDateTime = QDateTime(QDate(lst.at(1).toInt(),
                      lst.at(2).toInt(),
                      lst.at(3).toInt()),
                    QTime(lst.at(4).toInt(),
                      lst.at(5).toInt(),
                      lst.at(6).toInt()));

      emit picoDateTimeAvailable();
    }
  }
}

void CUDPConnection::setRTC()
{
  QDateTime dt = QDateTime::currentDateTime();

  QStringList lst;
  lst.append("RTC");
  lst.append(QString::number(dt.date().year()));
  lst.append(QString::number(dt.date().month()));
  lst.append(QString::number(dt.date().day()));
  lst.append(QString::number(dt.date().dayOfWeek()));
  lst.append(QString::number(dt.time().hour()));
  lst.append(QString::number(dt.time().minute()));
  lst.append(QString::number(dt.time().second()));
  lst.append("END\0");

  QString strRTCString = lst.join("!");

  QNetworkDatagram dtRTCCommand;
  dtRTCCommand.setDestination(QHostAddress(m_strPicoIPAddress), m_nPicoPort);
  dtRTCCommand.setData(strRTCString.toUtf8());
  m_pUDPSender->writeDatagram(dtRTCCommand.data(), QHostAddress(m_strPicoIPAddress), m_nPicoPort);
}

void CUDPConnection::readRTC()
{
  QString strRTCString = "?RTC!END\0";

  QNetworkDatagram dtRTCCommand;
  dtRTCCommand.setDestination(QHostAddress(m_strPicoIPAddress), m_nPicoPort);
  dtRTCCommand.setData(strRTCString.toUtf8());
  m_pUDPSender->writeDatagram(dtRTCCommand.data(), QHostAddress(m_strPicoIPAddress), m_nPicoPort);
}

QDateTime CUDPConnection::getPicoDateTime()
{
  return m_dtPicoDateTime;
}