English Русский 中文 Deutsch 日本語 Português
preview
Desarrollando un cliente MQTT para MetaTrader 5: metodología de TDD (Parte 2)

Desarrollando un cliente MQTT para MetaTrader 5: metodología de TDD (Parte 2)

MetaTrader 5Integración | 20 febrero 2024, 10:22
212 0
Jocimar Lopes
Jocimar Lopes

Introducción

"La optimización prematura es la raíz de todos los males" (Donald Knuth)

En el artículo anterior, analizaremos el MQTT, un protocolo binario de mensajería de publicación/suscripción altamente eficiente. Ya hemos hablado sobre qué es el MQTT, por qué comenzó su desarrollo hace veinticinco años y por qué se utiliza hoy en muchas industrias: desde la automoción hasta el Internet de las cosas, y desde la industria aeroespacial hasta las simples aplicaciones de chat. Hemos visto que el MQTT puede resultar útil en cualquier contexto donde se requiera un protocolo de mensajería independiente del tipo de contenido, incluido el contexto de aplicaciones comerciales. Hemos observado los beneficios de incluir un cliente MQL5 nativo para MQTT en nuestro código base y establecido el entorno de desarrollo mínimo requerido utilizando el bróker MQTT de código abierto Mosquitto que se ejecuta en WSL (Subsistema de Windows para Linux).

Hemos comenzado a desarrollar nuestro propio cliente con una función de generador de encabezado estrictamente codificada y llegado al punto en el que podemos conectarnos al bróker local Mosquitto, pero el servidor restablece inmediatamente la conexión debido a un error de protocolo.

Cita del final del artículo anterior:

"Mosquitto ha reconocido nuestro encabezado CONNECT fijo, pero el cliente desconocido (<unknown>) se ha desconectado de inmediato "debido a un error de protocolo". El error se ha producido porque aún no hemos incluido un encabezado de variable con el nombre del protocolo, el nivel del mismo y otros metadatos vinculados. Corregiremos esto en el siguiente paso".

Fig. 01. Error de conexión

Fig. 01. La pestaña de asesores en el MetaEditor muestra un error de conexión


Como estamos aplicando el principio de desarrollo guiado por pruebas (TDD), comenzaremos escribiendo una prueba para el generador de paquetes CONNECT que incluya los metadatos correspondientes y no se desvíe "por errores de protocolo".

Escribir una prueba antes de escribir el código que vamos a probar puede parecer contradictorio para muchos, pero si pensamos en las pruebas como una descripción objetiva de los requisitos de un proyecto y las vemos como la definición más objetiva de una meta que puede tener el desarrollador, entonces todo encajará.

“Las pruebas unitarias son documentos que describen el diseño del sistema de nivel más bajo. Son inequívocos, precisos, escritos en un lenguaje que el lector puede entender y tan formales que se pueden ejecutar. Este es el mejor tipo de documentación de bajo nivel que puede existir. ¿Qué profesional no ofrecería dicha documentación? (Robert Martin "Código limpio", 2011)


Organización del código: archivos de encabezado y programación orientada a objetos

Como hemos indicado anteriormente, hemos comenzado a crear un encabezado fijo para nuestro paquete de conexión codificando un array de bytes con los valores "correctos". Luego hemos intentado conectar a nuestro cliente enviando este array de bytes codificado a nuestro bróker local. Nuestros intentos han fracasado estrepitosamente "debido a un error de protocolo". Pero al mismo tiempo hemos aprendido algo sobre nuestro entorno de desarrollo, sobre nuestra registro Mosquitto, hemos escrito nuestras primeras pruebas y, sobre todo, hemos comenzado a hacer algo que funcione.

Como puede ver, este fracaso ha sido intencional, pero sabemos que esta forma de desarrollar una aplicación compleja no es sostenible a largo plazo.

Crear los paquetes MQTT adecuados supone solo el primer (y más fácil) paso en el proceso de escritura de un cliente fiable y fácil de mantener. Cuando hablamos de las especificaciones de rendimiento, todas las complejidades del protocolo salen a la luz. Esta tarea necesitará de más trabajo de nuestra parte como desarrolladores. Además de enviar los paquetes correspondientes, deberemos lidiar con muchas respuestas diferentes del servidor y distintos estados de la aplicación. En este punto, los arrays de bytes codificados (o cualquier otra cosa codificada) no resultarán suficientes.

Afortunadamente, MQL5 es un lenguaje de programación orientado a objetos y no estamos trabajando en el entorno restringido de memoria/CPU para el que se diseñó originalmente MQTT, por lo que podemos aprovechar al máximo el paradigma orientado a objetos (POO) para:

  • Tomar fácilmente decisiones de protocolo eligiendo el nivel de abstracción correcto
  • Leer fácilmente código (recuerde que el código se lee muchas más veces de las que se escribe)
  • Mantener el código sin muchos problemas
  • Y realizar pruebas con facilidad

En la sección "Programación orientada a objetos" del libro de la guía de ayuda MQL5, se dedica una sección completa a este tema. 


Definiciones de protocolos

Un protocolo de intercambio de mensajes es un conjunto de reglas que establece una base común para la interacción entre dos o más objetos, en nuestro caso, entre dos o más dispositivos. Muchas de estas reglas tratan sobre qué hacer considerando lo que se ha hecho antes. Son estáticas. Para seleccionar la siguiente acción, nuestro código debe evaluar el estado actual de la aplicación. En el protocolo MQTT, esto se hace de acuerdo con las reglas de comportamiento operativo.

Además de las reglas de seguimiento del estado (de hecho, las preceden), existen definiciones de términos, valores y cálculos que son independientes del estado de la aplicación. Por lo general, se trata de constantes, enumeraciones y algoritmos de evaluación, en el caso del nombre del protocolo MQTT, los tipos de paquetes de control y el valor de bytes de longitud restante del encabezado fijo, respectivamente.

Recopilaremos estos dos conjuntos diferentes de reglas en dos archivos de encabezado por separado. El primero será solo para definiciones de términos y significados comunes a nuestros archivos. Lo llamaremos Defines.mqh. Estos términos y valores suelen ser constantes y este archivo prácticamente no debería cambiar.

El otro archivo de encabezado contendrá algunas enumeraciones, estructuras y funciones comunes. Lo llamaremos MQTT.mqh. Estas enumeraciones, estructuras y funciones cambiarán con frecuencia, y no solo mientras estemos desarrollando la primera versión. El archivo cambiará cada vez que realicemos mejoras, optimizaciones y correcciones de errores, y resulta probable que este archivo se divida en otros archivos más específicos.

La práctica de utilizar archivos de encabezado para organizar el código no se relaciona con la programación orientada a objetos. Se pueden encontrar notas útiles sobre estos archivos en el ahora clásico libro "The C Programming Language" de Brian Kernighan y Dennis Ritchie. 

"(…) definiciones y declaraciones comunes a todos los archivos. En la medida de lo posible, queremos centralizar el proceso para que solo haya una copia que pueda recuperarse y almacenarse a medida que evoluciona el programa. (...) Probablemente sea mejor, hasta un tamaño de programa moderado, tener un archivo de encabezado que contenga todo lo que debería ser compartido entre dos partes cualesquiera del programa (...). Un programa mucho más amplio requeriría más organización y más encabezados."

Pero es en la programación orientada a objetos donde la práctica de organizar el código en pequeñas unidades de compilación resulta especialmente notable. Además, como estamos creando una biblioteca, casi todo nuestro código estará en archivos de encabezado.

El encabezado Defines

En este punto, el nombre del protocolo y las definiciones de nivel de protocolo se utilizarán solo en los paquetes CONNECT. Entonces, si lo deseamos, podremos ponerlos en una clase CPktConnect concreta (mire más abajo). Pero los dejaremos en el encabezado Defines para mantener la coherencia, aunque actualmente solo se usan en paquetes CONNECT, se podrán usar en otros archivos más adelante.

Los comentarios del protocolo son citas de la descripción oficial del estándar.

//+------------------------------------------------------------------+
//|                                                      Defines.mqh |
//|            ********* WORK IN PROGRESS **********                 |
//| **** PART OF ARTICLE https://www.mql5.com/en/articles/13334 **** |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//|              PROTOCOL NAME AND VERSION                           |
//+------------------------------------------------------------------+
#define MQTT_PROTOCOL_NAME_LENGTH_MSB           0x00
#define MQTT_PROTOCOL_NAME_LENGTH_LSB           0x04
#define MQTT_PROTOCOL_NAME_BYTE_3               'M'
#define MQTT_PROTOCOL_NAME_BYTE_4               'Q'
#define MQTT_PROTOCOL_NAME_BYTE_5               'T'
#define MQTT_PROTOCOL_NAME_BYTE_6               'T'
#define MQTT_PROTOCOL_VERSION                   0x05
//+------------------------------------------------------------------+
//|              PROPERTIES                                          |
//+------------------------------------------------------------------+
/*
The last field in the Variable Header of the CONNECT, CONNACK, PUBLISH, PUBACK, PUBREC,
PUBREL, PUBCOMP, SUBSCRIBE, SUBACK, UNSUBSCRIBE, UNSUBACK, DISCONNECT, and
AUTH packet is a set of Properties. In the CONNECT packet there is also an optional set of Properties in
the Will Properties field with the Payload
*/
#define MQTT_PROPERTY_PAYLOAD_FORMAT_INDICATOR          0x01 // (1) Byte                  
#define MQTT_PROPERTY_MESSAGE_EXPIRY_INTERVAL           0x02 // (2) Four Byte Integer     
#define MQTT_PROPERTY_CONTENT_TYPE                      0x03 // (3) UTF-8 Encoded String  
#define MQTT_PROPERTY_RESPONSE_TOPIC                    0x08 // (8) UTF-8 Encoded String  
#define MQTT_PROPERTY_CORRELATION_DATA                  0x09 // (9) Binary Data           
#define MQTT_PROPERTY_SUBSCRIPTION_IDENTIFIER           0x0B // (11) Variable Byte Integer
#define MQTT_PROPERTY_SESSION_EXPIRY_INTERVAL           0x11 // (17) Four Byte Integer    
#define MQTT_PROPERTY_ASSIGNED_CLIENT_IDENTIFIER        0x12 // (18) UTF-8 Encoded String  
#define MQTT_PROPERTY_SERVER_KEEP_ALIVE                 0x13 // (19) Two Byte Integer      
#define MQTT_PROPERTY_AUTHENTICATION_METHOD             0x15 // (21) UTF-8 Encoded String 
#define MQTT_PROPERTY_AUTHENTICATION_DATA               0x16 // (22) Binary Data          
#define MQTT_PROPERTY_REQUEST_PROBLEM_INFORMATION       0x17 // (23) Byte                  
#define MQTT_PROPERTY_WILL_DELAY_INTERVAL               0x18 // (24) Four Byte Integer    
#define MQTT_PROPERTY_REQUEST_RESPONSE_INFORMATION      0x19 // (25) Byte                  
#define MQTT_PROPERTY_RESPONSE_INFORMATION              0x1A // (26) UTF-8 Encoded String  
#define MQTT_PROPERTY_SERVER_REFERENCE                  0x1C // (28) UTF-8 Encoded String 
#define MQTT_PROPERTY_REASON_STRING                     0x1F // (31) UTF-8 Encoded String
#define MQTT_PROPERTY_RECEIVE_MAXIMUM                   0x21 // (33) Two Byte Integer     
#define MQTT_PROPERTY_TOPIC_ALIAS_MAXIMUM               0x22 // (34) Two Byte Integer     
#define MQTT_PROPERTY_TOPIC_ALIAS                       0x23 // (35) Two Byte Integer     
#define MQTT_PROPERTY_MAXIMUM_QOS                       0x24 // (36) Byte                 
#define MQTT_PROPERTY_RETAIN_AVAILABLE                  0x25 // (37) Byte                 
#define MQTT_PROPERTY_USER_PROPERTY                     0x26 // (38) UTF-8 String Pair   
#define MQTT_PROPERTY_MAXIMUM_PACKET_SIZE               0x27 // (39) Four Byte Integer    
#define MQTT_PROPERTY_WILDCARD_SUBSCRIPTION_AVAILABLE   0x28 // (40) Byte                  
#define MQTT_PROPERTY_SUBSCRIPTION_IDENTIFIER_AVAILABLE 0x29 // (41) Byte                  
#define MQTT_PROPERTY_SHARED_SUBSCRIPTION_AVAILABLE     0x2A // (42) Byte 
//+------------------------------------------------------------------+
//|              REASON CODES                                        |
//+------------------------------------------------------------------+
/*
A Reason Code is a one byte unsigned value that indicates the result of an operation. Reason Codes less
than 0x80 indicate successful completion of an operation. The normal Reason Code for success is 0.
Reason Code values of 0x80 or greater indicate failure.

The CONNACK, PUBACK, PUBREC, PUBREL, PUBCOMP, DISCONNECT and AUTH Control Packets
have a single Reason Code as part of the Variable Header. The SUBACK and UNSUBACK packets
contain a list of one or more Reason Codes in the Payload.
*/
#define MQTT_REASON_CODE_SUCCESS                                0x00 // (0)
#define MQTT_REASON_CODE_NORMAL_DISCONNECTION                   0x00 // (0)
#define MQTT_REASON_CODE_GRANTED_QOS_0                          0x00 // (0)
#define MQTT_REASON_CODE_GRANTED_QOS_1                          0x01 // (1)
#define MQTT_REASON_CODE_GRANTED_QOS_2                          0x02 // (2)
#define MQTT_REASON_CODE_DISCONNECT_WITH_WILL_MESSAGE           0x04 // (4)
#define MQTT_REASON_CODE_NO_MATCHING_SUBSCRIBERS                0x10 // (16)
#define MQTT_REASON_CODE_NO_SUBSCRIPTION_EXISTED                0x11 // (17)
#define MQTT_REASON_CODE_CONTINUE_AUTHENTICATION                0x18 // (24)
#define MQTT_REASON_CODE_RE_AUTHENTICATE                        0x19 // (25)
#define MQTT_REASON_CODE_UNSPECIFIED_ERROR                      0x80 // (128)
#define MQTT_REASON_CODE_MALFORMED_PACKET                       0x81 // (129)
#define MQTT_REASON_CODE_PROTOCOL_ERROR                         0x82 // (130)
#define MQTT_REASON_CODE_IMPLEMENTATION_SPECIFIC_ERROR          0x83 // (131)
#define MQTT_REASON_CODE_UNSUPPORTED_PROTOCOL_VERSION           0x84 // (132)
#define MQTT_REASON_CODE_CLIENT_IDENTIFIER_NOT_VALID            0x85 // (133)
#define MQTT_REASON_CODE_BAD_USER_NAME_OR_PASSWORD              0x86 // (134)
#define MQTT_REASON_CODE_NOT_AUTHORIZED                         0x87 // (135)
#define MQTT_REASON_CODE_SERVER_UNAVAILABLE                     0x88 // (136)
#define MQTT_REASON_CODE_SERVER_BUSY                            0x89 // (137)
#define MQTT_REASON_CODE_BANNED                                 0x8A // (138)
#define MQTT_REASON_CODE_SERVER_SHUTTING_DOWN                   0x8B // (139)
#define MQTT_REASON_CODE_BAD_AUTHENTICATION_METHOD              0x8C // (140)
#define MQTT_REASON_CODE_KEEP_ALIVE_TIMEOUT                     0x8D // (141)
#define MQTT_REASON_CODE_SESSION_TAKEN_OVER                     0x8E // (142)
#define MQTT_REASON_CODE_TOPIC_FILTER_INVALID                   0x8F // (143)
#define MQTT_REASON_CODE_TOPIC_NAME_INVALID                     0x90 // (144)
#define MQTT_REASON_CODE_PACKET_IDENTIFIER_IN_USE               0x91 // (145)
#define MQTT_REASON_CODE_PACKET_IDENTIFIER_NOT_FOUND            0x92 // (146)
#define MQTT_REASON_CODE_RECEIVE_MAXIMUM_EXCEEDED               0x93 // (147)
#define MQTT_REASON_CODE_TOPIC_ALIAS_INVALID                    0x94 // (148)
#define MQTT_REASON_CODE_PACKET_TOO_LARGE                       0x95 // (149)
#define MQTT_REASON_CODE_MESSAGE_RATE_TOO_HIGH                  0x96 // (150)
#define MQTT_REASON_CODE_QUOTA_EXCEEDED                         0x97 // (151)
#define MQTT_REASON_CODE_ADMINISTRATIVE_ACTION                  0x98 // (152)
#define MQTT_REASON_CODE_PAYLOAD_FORMAT_INVALID                 0x99 // (153)
#define MQTT_REASON_CODE_RETAIN_NOT_SUPPORTED                   0x9A // (154)
#define MQTT_REASON_CODE_QOS_NOT_SUPPORTED                      0x9B // (155)
#define MQTT_REASON_CODE_USE_ANOTHER_SERVER                     0x9C // (156)
#define MQTT_REASON_CODE_SERVER_MOVED                           0x9D // (157)
#define MQTT_REASON_CODE_SHARED_SUBSCRIPTIONS_NOT_SUPPORTED     0x9E // (158)
#define MQTT_REASON_CODE_CONNECTION_RATE_EXCEEDED               0x9F // (159)
#define MQTT_REASON_CODE_MAXIMUM_CONNECT_TIME                   0xA0 // (160)
#define MQTT_REASON_CODE_SUBSCRIPTION_IDENTIFIERS_NOT_SUPPORTED 0xA1 // (161)
#define MQTT_REASON_CODE_WILDCARD_SUBSCRIPTIONS_NOT_SUPPORTED   0xA2 // (162)

Tenga en cuenta que añadiremos el prefijo MQTT a todas las definiciones específicas del protocolo. Esto sirve para diferenciarlas de nuestras propias definiciones, que se añadirán más adelante. Asimismo, deberá tener en cuenta que al principio de nuestro archivo Defines.mqh, en la sección PROTOCOL NAME AND VERSION, intentaremos indicar los nombres de los identificadores de la forma más explícita posible. Esto se hace así para cumplir con los principios del llamado "código limpio". Estos principios deberían ayudar a que nuestro código resulte más legible, más fácil de depurar y compatible con IDE, es decir, más fácil de buscar y más adecuado para utilizar la función de autocompletar de los IDE modernos.


Encabezado MQTT

//+------------------------------------------------------------------+
//|                                                         MQTT.mqh |
//|            ********* WORK IN PROGRESS **********                 |
//| **** PART OF ARTICLE https://www.mql5.com/en/articles/13334 **** |
//+------------------------------------------------------------------+
#include "Defines.mqh"
//+------------------------------------------------------------------+
//|              MQTT - CONTROL PACKET - TYPES                       |
//+------------------------------------------------------------------+
/*
Position: byte 1, bits 7-4.
Represented as a 4-bit unsigned value, the values are shown below.
*/
enum ENUM_PKT_TYPE
  {
   CONNECT     =  0x01, // Connection request
   CONNACK     =  0x02, // Connection Acknowledgment
   PUBLISH     =  0x03, // Publish message
   PUBACK      =  0x04, // Publish acknowledgment (QoS 1)
   PUBREC      =  0x05, // Publish received (QoS 2 delivery part 1)
   PUBREL      =  0x06, // Publish release (QoS 2 delivery part 2)
   PUBCOMP     =  0x07, // Publish complete (QoS 2 delivery part 3)
   SUBSCRIBE   =  0x08, // Subscribe request
   SUBACK      =  0x09, // Subscribe acknowledgment
   UNSUBSCRIBE =  0x0A, // Unsubscribe request
   UNSUBACK    =  0x0B, // Unsubscribe acknowledgment
   PINGREQ     =  0x0C, // PING request
   PINGRESP    =  0x0D, // PING response
   DISCONNECT  =  0x0E, // Disconnect notification
   AUTH        =  0x0F, // Authentication exchange
  };
//+------------------------------------------------------------------+
//|             CONNECT - VARIABLE HEADER - CONNECT FLAGS            |
//+------------------------------------------------------------------+
/*
The Connect Flags byte contains several parameters specifying the behavior of the MQTT connection. It
also indicates the presence or absence of fields in the Payload.
*/
enum ENUM_CONNECT_FLAGS
  {
   RESERVED       = 0x00,
   CLEAN_START    = 0x02,
   WILL_FLAG      = 0x04,
   WILL_QOS_1     = 0x08,
   WILL_QOS_2     = 0x10,
   WILL_RETAIN    = 0x20,
   PASSWORD_FLAG  = 0x40,
   USER_NAME_FLAG = 0x80
  };
//+------------------------------------------------------------------+
//|             CONNECT - VARIABLE HEADER - QoS LEVELS               |
//+------------------------------------------------------------------+
/*
Position: bits 4 and 3 of the Connect Flags.
These two bits specify the QoS level to be used when publishing the Will Message.
If the Will Flag is set to 0, then the Will QoS MUST be set to 0 (0x00) [MQTT-3.1.2-11].
If the Will Flag is set to 1, the value of Will QoS can be 0 (0x00), 1 (0x01), or 2 (0x02) [MQTT-3.1.2-12].
*/
enum ENUM_QOS_LEVEL
  {
   AT_MOST_ONCE   = 0x00,
   AT_LEAST_ONCE  = 0x01,
   EXACTLY_ONCE   = 0x02
  };
//+------------------------------------------------------------------+
//|                   SetProtocolVersion                             |
//+------------------------------------------------------------------+
void SetProtocolVersion(uchar& dest_buf[])
  {
   dest_buf[8] = MQTT_PROTOCOL_VERSION;
  }
//+------------------------------------------------------------------+
//|                     SetProtocolName                              |
//+------------------------------------------------------------------+
void SetProtocolName(uchar& dest_buf[])
  {
   dest_buf[2] = MQTT_PROTOCOL_NAME_LENGTH_MSB;
   dest_buf[3] = MQTT_PROTOCOL_NAME_LENGTH_LSB;
   dest_buf[4] = MQTT_PROTOCOL_NAME_BYTE_3;
   dest_buf[5] = MQTT_PROTOCOL_NAME_BYTE_4;
   dest_buf[6] = MQTT_PROTOCOL_NAME_BYTE_5;
   dest_buf[7] = MQTT_PROTOCOL_NAME_BYTE_6;
  }
//+------------------------------------------------------------------+
//|                     SetFixedHeader                               |
//+------------------------------------------------------------------+
void SetFixedHeader(ENUM_PKT_TYPE pkt_type, uchar& buf[], uchar& dest_buf[])
  {
   dest_buf[0] = (uchar)pkt_type << 4;
   dest_buf[1] = GetRemainingLength(buf);
  }
//+------------------------------------------------------------------+
//|                    GetRemainingLength                            |
//+------------------------------------------------------------------+
/*
Position: starts at byte 2.
The Remaining Length is a Variable Byte Integer that represents the number of bytes remaining within the
current Control Packet, including data in the Variable Header and the Payload. The Remaining Length
does not include the bytes used to encode the Remaining Length. The packet size is the total number of
bytes in an MQTT Control Packet, this is equal to the length of the Fixed Header plus the Remaining
Length.
*/
uchar GetRemainingLength(uchar &buf[])
  {
   uint x;
   x = ArraySize(buf);
   uint rem_len;
   do
     {
      rem_len = x % 128;
      x = (x / 128);
      if(x > 0)
        {
         rem_len = rem_len | 128;
        }
     }
   while(x > 0);
   return (uchar)rem_len;
  };

//+------------------------------------------------------------------+


Clases y estructuras

Interfaz del paquete de administración MQTT

Aquí deberemos tomar una decisión: empezar la jerarquía de objetos de Paquetes de control con una clase abstracta o con una interfaz. Podríamos comenzar con una clase básica común que funcione bien para cualquier paquete de control. Esta clase abstracta puede especializarse en clases derivadas de paquetes de control más específicas. O podríamos comenzar con una interfaz simple que implemente estas clases del paquete de control.

Comenzaremos con la interfaz IcontrolPacket. Tendrá un método sencillo. Esta elección la podemos cambiar a medida que se implementen las especificaciones de rendimiento del protocolo. Probablemente cambiemos esta interfaz a una clase abstracta con algunas funciones virtuales.

//+------------------------------------------------------------------+
//|                                               IControlPacket.mqh |
//|            ********* WORK IN PROGRESS **********                 |
//| **** PART OF ARTICLE https://www.mql5.com/en/articles/13334 **** |
//+------------------------------------------------------------------+
#include "MQTT.mqh"
//+------------------------------------------------------------------+
//|       Interface IControlPacket                                   |
//|       The root of object hierarchy                               |
//+------------------------------------------------------------------+
interface IControlPacket
  {

   bool              IsControlPacket();

  };
//+------------------------------------------------------------------+

Como hemos indicado, en este momento el único propósito de la presente interfaz es actuar como raíz de la jerarquía de objetos del paquete MQTT. De momento, esto no es más que un marcador de posición.

Clase de conexión de paquetes de control MQTT

El paquete de control CONNECT es el que lleva más tiempo desarrollar. Aparte del hecho de que todavía tenemos que familiarizarnos con el protocolo, es este paquete en particular el que ha recibido las mejoras más significativas en la versión 5.0, concretamente, propiedades de conexión (Connect Properties) y propiedades de usuario (User Properties).

//+------------------------------------------------------------------+
//|                                                   PktConnect.mqh |
//|            ********* WORK IN PROGRESS **********                 |
//| **** PART OF ARTICLE https://www.mql5.com/en/articles/13334 **** |
//+------------------------------------------------------------------+
#include "MQTT.mqh"
#include "Defines.mqh"
#include "IControlPacket.mqh"
//+------------------------------------------------------------------+
//|        CONNECT VARIABLE HEADER                                   |
//+------------------------------------------------------------------+
/*
The Variable Header for the CONNECT Packet contains the following fields in this order:
Protocol Name,Protocol Level, Connect Flags, Keep Alive, and Properties.
*/
struct MqttClientIdentifierLength
  {
   uchar             msb;
   uchar             lsb;
  } clientIdLen;
//---
struct MqttKeepAlive
  {
   uchar             msb;
   uchar             lsb;
  } keepAlive;
//---
struct MqttConnectProperties
  {
   uint              prop_len;
   uchar             session_expiry_interval_id;
   uint              session_expiry_interval;
   uchar             receive_maximum_id;
   ushort            receive_maximum;
   uchar             maximum_packet_size_id;
   ushort            maximum_packet_size;
   uchar             topic_alias_maximum_id;
   ushort            topic_alias_maximum;
   uchar             request_response_information_id;
   uchar             request_response_information;
   uchar             request_problem_information_id;
   uchar             request_problem_information;
   uchar             user_property_id;
   string            user_property_key;
   string            user_property_value;
   uchar             authentication_method_id;
   string            authentication_method;
   uchar             authentication_data_id;
  } connectProps;
//---
struct MqttConnectPayload
  {
   uchar             client_id_len;
   string            client_id;
   ushort            will_properties_len;
   uchar             will_delay_interval_id;
   uint              will_delay_interval;
   uchar             payload_format_indicator_id;
   uchar             payload_format_indicator;
   uchar             message_expiry_interval_id;
   uint              message_expiry_interval;
   uchar             content_type_id;
   string            content_type;
   uchar             response_topic_id; // for request/response
   string            response_topic;
   uchar             correlation_data_id; // for request/response
   ulong             correlation_data[]; // binary data
   uchar             user_property_id;
   string            user_property_key;
   string            user_property_value;
   uchar             will_topic_len;
   string            will_topic;
   uchar             will_payload_len;
   ulong             will_payload[]; // binary data
   uchar             user_name_len;
   string            user_name;
   uchar             password_len;
   ulong             password; // binary data
  } connectPayload;
//+------------------------------------------------------------------+
//| Class CPktConnect.                                               |
//| Purpose: Class of MQTT Connect Control Packets.                  |
//|          Implements IControlPacket                               |
//+------------------------------------------------------------------+
class CPktConnect : public IControlPacket
  {
private:
   bool              IsControlPacket() {return true;}
protected:
   void              InitConnectFlags() {ByteArray[9] = 0;}
   void              InitKeepAlive() {ByteArray[10] = 0; ByteArray[11] = 0;}
   void              InitPropertiesLength() {ByteArray[12] = 0;}
   uchar             m_connect_flags;

public:
                     CPktConnect();
                     CPktConnect(uchar &buf[]);
                    ~CPktConnect();
   //--- methods for setting Connect Flags
   void              SetCleanStart(const bool cleanStart);
   void              SetWillFlag(const bool willFlag);
   void              SetWillQoS_1(const bool willQoS_1);
   void              SetWillQoS_2(const bool willQoS_2);
   void              SetWillRetain(const bool willRetain);
   void              SetPasswordFlag(const bool passwordFlag);
   void              SetUserNameFlag(const bool userNameFlag);
   void              SetKeepAlive(ushort seconds);
   void              SetClientIdentifierLength(string clientId);
   void              SetClientIdentifier(string clientId);

   //--- member for getting the byte array
   uchar             ByteArray[];
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CPktConnect::CPktConnect(uchar &buf[])
  {
   ArrayFree(ByteArray);
   ArrayResize(ByteArray, buf.Size() + 2, UCHAR_MAX);
   SetFixedHeader(CONNECT, buf, ByteArray);
   SetProtocolName(ByteArray);
   SetProtocolVersion(ByteArray);
   InitConnectFlags();
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CPktConnect::SetClientIdentifier(string clientId)
  {
   SetClientIdentifierLength(clientId);
   StringToCharArray(clientId, ByteArray,
                     ByteArray.Size() - StringLen(clientId), StringLen(clientId));
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CPktConnect::SetClientIdentifierLength(string clientId)
  {
   clientIdLen.msb = (char)StringLen(clientId) >> 8;
   clientIdLen.lsb = (char)StringLen(clientId) % 256;
   ByteArray[12] = clientIdLen.msb;
   ByteArray[13] = clientIdLen.lsb;
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CPktConnect::SetKeepAlive(ushort seconds) // MQTT max is 65,535 sec
  {
   keepAlive.msb = (uchar)(seconds >> 8) & 255;
   keepAlive.lsb = (uchar)seconds & 255;
   ByteArray[10] = keepAlive.msb;
   ByteArray[11] = keepAlive.lsb;
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CPktConnect::SetPasswordFlag(const bool passwordFlag)
  {
   passwordFlag ? m_connect_flags |= PASSWORD_FLAG : m_connect_flags &= ~PASSWORD_FLAG;
   ArrayFill(ByteArray, sizeof(ByteArray), 1, m_connect_flags);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CPktConnect::SetUserNameFlag(const bool userNameFlag)
  {
   userNameFlag ? m_connect_flags |= USER_NAME_FLAG : m_connect_flags &= (uchar) ~USER_NAME_FLAG;
   ArrayFill(ByteArray, sizeof(ByteArray), 1, m_connect_flags);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CPktConnect::SetWillRetain(const bool willRetain)
  {
   willRetain ? m_connect_flags |= WILL_RETAIN : m_connect_flags &= ~WILL_RETAIN;
   ArrayFill(ByteArray, sizeof(ByteArray), 1, m_connect_flags);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CPktConnect::SetWillQoS_2(const bool willQoS_2)
  {
   willQoS_2 ? m_connect_flags |= WILL_QOS_2 : m_connect_flags &= ~WILL_QOS_2;
   ArrayFill(ByteArray, sizeof(ByteArray), 1, m_connect_flags);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CPktConnect::SetWillQoS_1(const bool willQoS_1)
  {
   willQoS_1 ? m_connect_flags |= WILL_QOS_1 : m_connect_flags &= ~WILL_QOS_1;
   ArrayFill(ByteArray, sizeof(ByteArray), 1, m_connect_flags);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CPktConnect::SetWillFlag(const bool willFlag)
  {
   willFlag ? m_connect_flags |= WILL_FLAG : m_connect_flags &= ~WILL_FLAG;
   ArrayFill(ByteArray, sizeof(ByteArray), 1, m_connect_flags);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CPktConnect::SetCleanStart(const bool cleanStart)
  {
   cleanStart ? m_connect_flags |= CLEAN_START : m_connect_flags &= ~CLEAN_START;
   ArrayFill(ByteArray, 9, 1, m_connect_flags);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CPktConnect::CPktConnect()
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CPktConnect::~CPktConnect()
  {
  }
//+------------------------------------------------------------------+


Probando nuestra primera clase

El único propósito de la clase CPktConnect es crear un paquete MQTT CONNECT bien formado. Entonces, para ponerla a prueba, deberemos comenzar creando un array de bytes de muestra que representará un paquete CONNECT bien formado. Pero, ¿cómo podemos estar seguros de que nuestro array de bytes formado supone un paquete CONNECT bien formado? Después de todo, esta es la prueba que usaremos para crear nuestra clase desde cero. Muchos de nuestros esfuerzos, si no todos, podrían desperdiciarse si nuestro “accesorio” es un paquete deficiente.

Aquí es donde el desarrollador del protocolo, OASIS, acude en nuestra ayuda. En la sección 3.1.2.12 se puede encontrar un ejemplo no normativo de un encabezado variable para el paquete CONNECT. Como ya hemos puesto a prueba nuestro generador de encabezados fijos (consulte el artículo anterior), este ejemplo de OASIS será suficiente para comenzar. Esto nos permitirá estar seguros de que nuestra clase genere un paquete bien formado con algunas configuraciones distintas, como la CleanSession lógica y el intervalo temporal de mantenimiento solicitado.

Este array de bytes codificado generado manualmente se comparará con el paquete CpktConnect generado.

//+------------------------------------------------------------------+
//|                                  TEST_CControlPacket_Connect.mq5 |
//|                                                                  |
//|            ********* WORK IN PROGRESS **********                 |
//| **** PART OF ARTICLE https://www.mql5.com/en/articles/13334 **** |
//+------------------------------------------------------------------+
#include <MQTT\CPktConnect.mqh>

//+------------------------------------------------------------------+
//| Tests for CControlPacketConnect class                            |
//+------------------------------------------------------------------+
void OnStart()
  {
   Print(TEST_SetCleanStart_KeepAlive_ClientIdentifier());
   Print(TEST_SetClientIdentifier());
   Print(TEST_SetClientIdentifierLength());
   Print(TEST_SetCleanStart_and_SetKeepAlive());
   Print(TEST_SetKeepAlive());
   Print(TEST_SetCleanStart());
  }
/* REFERENCE ARRAY (FIXTURE)
{16, 24, 0, 4, 77, 81, 84, 84, 5, 2, 0, 10, 0, 4, 7, 17, 0, 0, 0, 10, 25, 1, 77, 81, 76, 53}
*/
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool TEST_SetCleanStart_KeepAlive_ClientIdentifier()
  {
   Print(__FUNCTION__);
//--- Arrange
   static uchar expected[] =
     {16, 16, 0, 4, 77, 81, 84, 84, 5, 2, 0, 10, 0, 4, 77, 81, 76, 53};
   uchar buf[expected.Size() - 2];
   CPktConnect *cut = new CPktConnect(buf);
//--- Act
   cut.SetCleanStart(true);
   cut.SetKeepAlive(10);//10 sec
   cut.SetClientIdentifier("MQL5");
   uchar result[];
   ArrayCopy(result, cut.ByteArray);
//--- Assert
   bool isTrue = Assert(expected, result);
//--- cleanup
   delete cut;
   ZeroMemory(result);
   return  isTrue ? true : false;
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool TEST_SetClientIdentifier()
  {
   Print(__FUNCTION__);
//--- Arrange
   static uchar expected[] =
     {16, 16, 0, 4, 77, 81, 84, 84, 5, 0, 0, 0, 0, 4, 77, 81, 76, 53};
   uchar buf[expected.Size() - 2];
   CPktConnect *cut = new CPktConnect(buf);
//--- Act
   cut.SetClientIdentifier("MQL5");
   uchar result[];
   ArrayCopy(result, cut.ByteArray);
//--- Assert
   bool isTrue = Assert(expected, result);
//--- cleanup
   delete cut;
   ZeroMemory(result);
   return  isTrue ? true : false;
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool TEST_SetClientIdentifierLength()
  {
   Print(__FUNCTION__);
//--- Arrange
   static uchar expected[] =
     {16, 12, 0, 4, 77, 81, 84, 84, 5, 0, 0, 0, 0, 4};
   uchar buf[expected.Size() - 2];
   CPktConnect *cut = new CPktConnect(buf);
//--- Act
   cut.SetClientIdentifierLength("MQL5");
   uchar result[];
   ArrayCopy(result, cut.ByteArray);
//--- Assert
   bool isTrue = Assert(expected, result);
//--- cleanup
   delete cut;
   ZeroMemory(result);
   return  isTrue ? true : false;
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool TEST_SetCleanStart_and_SetKeepAlive()
  {
   Print(__FUNCTION__);
//--- Arrange
   static uchar expected[] =
     {16, 10, 0, 4, 77, 81, 84, 84, 5, 2, 0, 10};
   uchar buf[expected.Size() - 2];
   CPktConnect *cut = new CPktConnect(buf);
//--- Act
   cut.SetCleanStart(true);
   cut.SetKeepAlive(10); //10 secs
   uchar result[];
   ArrayCopy(result, cut.ByteArray);
//--- Assert
   bool isTrue = Assert(expected, result);
//--- cleanup
   delete cut;
   ZeroMemory(result);
   return  isTrue ? true : false;
  }
//+------------------------------------------------------------------+
bool TEST_SetKeepAlive()
  {
   Print(__FUNCTION__);
//--- Arrange
   static uchar expected[] =
     {16, 10, 0, 4, 77, 81, 84, 84, 5, 0, 0, 10};
   uchar buf[expected.Size() - 2];
   CPktConnect *cut = new CPktConnect(buf);
//--- Act
   cut.SetKeepAlive(10); //10 secs
   uchar result[];
   ArrayCopy(result, cut.ByteArray);
//--- Assert
   bool isTrue = Assert(expected, result);
//--- cleanup
   delete cut;
   ZeroMemory(result);
   return  isTrue ? true : false;
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool TEST_SetCleanStart()
  {
   Print(__FUNCTION__);
//--- Arrange
   static uchar expected[] =
     {16, 8, 0, 4, 77, 81, 84, 84, 5, 2};
   uchar buf[expected.Size() - 2];
   CPktConnect *cut = new CPktConnect(buf);
//--- Act
   cut.SetCleanStart(true);
   uchar result[];
   ArrayCopy(result, cut.ByteArray);
//--- Assert
   bool isTrue = Assert(expected, result);
//--- cleanup
   delete cut;
//ZeroMemory(result);
   return  isTrue ? true : false;
  }
//+------------------------------------------------------------------+
bool Assert(uchar& expected[], uchar& result[])
  {
   if(!ArrayCompare(expected, result) == 0)
     {
      for(uint i = 0; i < expected.Size(); i++)
        {
         printf("expected\t%d\t\t%d result", expected[i], result[i]);
        }
      printf("expected size %d <=> %d result size", expected.Size(), result.Size());
      Print("Expected");
      ArrayPrint(expected);
      Print("Result");
      ArrayPrint(result);
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+

Nota: Como ya sabrá, escribir pruebas del tipo "vamos a ver si el array de encabezado contiene los datos que acabo de introducir" puede parecer una pérdida de tiempo, pero eso no es así. Este conjunto de pruebas "obvias" siempre acompañará a nuestro código. Podemos considerarlas una herramienta de depuración continua y automatizada que demostrará su valor cuando descubra algún error de regresión o incluso algún descuido básico causado, por ejemplo, por una copia incorrecta. Por este motivo, no comprobaremos únicamente si la acción de conexión funciona como "caja negra". Queremos asegurarnos de que nuestro encabezado esté formado correctamente antes de probar la acción de conexión. Vale la pena recordar que el TDD es un proceso. Muchas de estas pruebas, si no todas, se reescribirán o incluso se borrarán antes de que obtengamos la primera versión funcional de nuestro código. Pero las que se permanezcan probablemente se queden para siempre.

Esta prueba solo se superará cuando el array de bytes generado por CPktConnect retorne 0 (cero) en el ArrayCompare(d) con nuestro array de bytes de referencia.

Una vez que hayamos probado algunas combinaciones de propiedades de conexión básicas, el paquete se enviará al bróker. Esta vez no debería ser rechazado "por un error de protocolo".

Fig. 02. Resultados de las pruebas de la clase CPktConnect

Fig. 02. Resultados de las pruebas de la clase CPktConnect en la pestaña de asesores del MetaEditor


Prueba con un bróker MQTT local

Ahora podemos ejecutar nuestro bróker Mosquitto local en WSL para verificar si nuestra conexión MQTT ha tenido éxito.

Si hemos ejecutado la instalación predeterminada, Mosquitto debería ejecutarse como un servicio en Linux. Por lo tanto, solo necesitaremos "redireccionar" los puertos (80 -> 1883) y habilitar el nombre de host para las URL permitidas en la configuración de MetaTrader 5.

Fig. 03. Registro de WSL Mosquitto sobre la conexión/desconexión exitosa

Fig. 03. Registro de Mosquitto en WSL mostrando el estado de la conexión/desconexión: Success (éxito)


 ¡Hurra! Nuestro intento de conexión no ha retornado un error de protocolo. Ahora podremos intentar intercambiar mensajes entre el cliente y el servidor.


Conclusión

En la siguiente etapa veremos las respuestas de CONNACK. En este punto ya tendremos una base sólida sobre la que publicar nuestro primer mensaje. Y, por supuesto, ¡comenzaremos a escribir una prueba! :)  ¡Espere los nuevos artículos!


Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/13334

Archivos adjuntos |
Defines.mqh (8.03 KB)
MQTT.mqh (5.01 KB)
CPktConnect.mqh (9.97 KB)
Teoría de categorías en MQL5 (Parte 20): Autoatención y transformador Teoría de categorías en MQL5 (Parte 20): Autoatención y transformador
Hoy nos apartaremos un poco de nuestros temas habituales y veremos parte del algoritmo de ChatGPT. ¿Tiene alguna similitud o concepto tomado de las transformaciones naturales? Intentaremos responder estas y otras preguntas usando nuestro código en formato de clase de señal.
Regresión neta elástica mediante descenso de coordenadas en MQL5 Regresión neta elástica mediante descenso de coordenadas en MQL5
En este artículo, analizaremos la implementación práctica de la regresión neta elástica para minimizar el sobreajuste y al mismo tiempo separar automáticamente los predictores útiles de aquellos que tienen poco poder de pronóstico.
Algoritmos de optimización de la población: Algoritmo de salto de rana aleatorio (Shuffled Frog-Leaping, SFL) Algoritmos de optimización de la población: Algoritmo de salto de rana aleatorio (Shuffled Frog-Leaping, SFL)
El artículo presenta una descripción detallada del algoritmo de salto de rana aleatorio (SFL) y sus capacidades para resolver problemas de optimización. El algoritmo SFL se inspira en el comportamiento de las ranas en su entorno natural y ofrece un enfoque innovador para la optimización de características. El algoritmo SFL supone una herramienta eficaz y flexible que puede gestionar una gran variedad de tipos de datos y alcanzar soluciones óptimas.
Marcado de datos en el análisis de series temporales (Parte 2): Creando conjuntos de datos con marcadores de tendencias utilizando Python Marcado de datos en el análisis de series temporales (Parte 2): Creando conjuntos de datos con marcadores de tendencias utilizando Python
En esta serie de artículos, presentaremos varias técnicas de marcado de series temporales que pueden producir datos que se ajusten a la mayoría de los modelos de inteligencia artificial (IA). El marcado dirigido de datos puede hacer que un modelo de IA entrenado resulte más relevante para las metas y objetivos del usuario, mejorando la precisión del modelo y ayudando a este a dar un salto de calidad.