przy komunikacji z ESP8266 który zapewni stos TCP/IP , a z którym STM32 będzie się komunikował przy pomocy najprostszego mozliwego sposobu
czyli połączenia UART i komend AT czyli z firmware np od AIthinkera
Co to jest MQTT?
MQTT (Message Queuing Telemetry Transport) to lekki protokół komunikacyjny opracowany z myślą o systemach o ograniczonych zasobach oraz komunikacji M2M (Machine-to-Machine). Jego główne cechy to:
Model publikuj-subskrybuj:
Publikujący (publisher) wysyła wiadomości do tematów (topics).
Subskrybenci (subscribers) odbierają wiadomości, subskrybując konkretne tematy.
Pośrednikiem między nimi jest broker MQTT.
Efektywność:
Protokół działa na TCP/IP, ale jest zoptymalizowany pod kątem małych ilości przesyłanych danych.
Stosuje mechanizm QoS (Quality of Service), który pozwala na kontrolę niezawodności przesyłanych wiadomości.
Zastosowanie:
IoT (Internet of Things), urządzenia smart home, przemysłowe systemy SCADA, monitorowanie zdrowia, itd.
Dlaczego MQTT dla STM32?
STM32 to rodzina mikrokontrolerów firmy STMicroelectronics, często stosowanych w projektach IoT. Dzięki niskim wymaganiom sprzętowym MQTT, jest on idealnym wyborem dla mikrokontrolerów takich jak STM32. Dzięki czemu możemy liczyć na szereg korzyści implementacji MQTT na STM32:
-- Minimalne wykorzystanie pamięci.
-- Niskie zużycie energii.
-- Możliwość łatwej komunikacji z usługami chmurowymi (np. AWS IoT, Azure IoT).
Podstawy implementacji MQTT w języku C
Wymagania sprzętowe i programowe
Sprzęt:
Mikrokontroler STM32 (np.STM32F1, STM32F4, STM32F7, STM32H7).
Moduł Wi-Fi (np. ESP8266/ESP32) lub Ethernet (np. moduł W5500).
Oprogramowanie:
IDE: STM32CubeIDE lub Keil uVision.
Stos TCP/IP (np. LwIP).
Biblioteka MQTT (np. Paho MQTT, umqtt, lub własna implemetacja protokołu).
Kroki implementacji MQTT
1. Konfiguracja środowiska
STM32CubeMX:
Wybierz odpowiedni mikrokontroler STM32.
Skonfiguruj zegary systemowe, GPIO oraz interfejsy komunikacyjne (UART, SPI, Ethernet).
Jeśli korzystasz z modułu Ethernet, dodaj stos LwIP i skonfiguruj DHCP lub statyczny adres IP.
Dodanie biblioteki MQTT:
Wybierz bibliotekę dostosowaną do STM32. Popularnym wyborem jest Paho Embedded C.
lub własną implementację czyli funkcje CONNECT, PUBLISH, SUBSCRIBE
2. Integracja stosu TCP/IP
W przypadku Ethernet:
Użyj LwIP do zarządzania połączeniami TCP/IP.
Skonfiguruj stos LwIP w STM32CubeMX.
W przypadku Wi-Fi:
Skonfiguruj komunikację UART lub SPI z modułem Wi-Fi (np. ESP8266).
Użyj poleceń AT lub dedykowanych bibliotek do zarządzania połączeniami Wi-Fi.
3. Połączenie z brokerem MQTT
Inicjalizacja protokołu MQTT:
Zainicjalizuj strukturę klienta MQTT (np. w Paho: MQTTClient).
Ustaw wymagane parametry, takie jak adres brokera, port (domyślnie 1883), oraz dane logowania, jeśli wymagane.
- MQTTClient client;
- Network network;
- MQTTClientInit(&client, &network, 1000, sendBuffer, sizeof(sendBuffer), readBuffer, sizeof(readBuffer));
Połącz się z brokerem za pomocą gniazda TCP/IP.
- int rc = NetworkConnect(&network, "mqtt.example.com", 1883);
- if (rc != 0) {
- printf("Failed to connect to broker.\n");
- return rc;
- }
Subskrybuj temat, aby odbierać wiadomości
- MQTTSubscribe(&client, "sensor/temperature", QOS1, messageArrived);
Wysyłaj dane do brokera.
- MQTTMessage message;
- message.qos = QOS1;
- message.retained = 0;
- message.payload = "Hello, MQTT!";
- message.payloadlen = strlen("Hello, MQTT!");
- MQTTPublish(&client, "sensor/temperature", &message);
MQTT wymaga ciągłego przetwarzania wiadomości. W pętli głównej programu należy wywoływać funkcję obsługującą komunikację z brokerem.
- while (1) {
- MQTTYield(&client, 1000); // Przetwarza przychodzące wiadomości.
- }
inaczej mówiąc wtedy robimy urządzenie typu Client-Publish -- działa tak więklszość urzadzeń IoT np Czujniki temperatury , które łaczą się z brokerem tylko wtedy gdy wysyłają komunikaty o zmianach parametrów, co pozwala na zminimalizowanie ilości pobieranej energii. wiec mogą działać długi czas na bateriach.
Przykładowy kod programu
Oto uproszczony przykład komunikacji MQTT na STM32 z użyciem biblioteki Paho MQTT, która zwalnia nas z karkołomnego implementowania
funkcji CONNECT , PUBLISH, SUBSCRIBE
- #include "MQTTClient.h"
- #include "lwip/sockets.h"
- void messageArrived(MessageData* data) {
- printf("Message received: %.*s\n", data->message->payloadlen, (char*)data->message->payload);
- }
- int main() {
- Network network;
- MQTTClient client;
- unsigned char sendbuf[80], readbuf[80];
- NetworkInit(&network);
- MQTTClientInit(&client, &network, 1000, sendbuf, sizeof(sendbuf), readbuf, sizeof(readbuf));
- if (NetworkConnect(&network, "mqtt.example.com", 1883) != 0) {
- printf("Failed to connect to MQTT broker\n");
- return -1;
- }
- MQTTPacket_connectData connectData = MQTTPacket_connectData_initializer;
- connectData.MQTTVersion = 3;
- connectData.clientID.cstring = "STM32Client";
- if (MQTTConnect(&client, &connectData) != 0) {
- printf("MQTT connection failed\n");
- return -1;
- }
- MQTTSubscribe(&client, "sensor/temperature", QOS1, messageArrived);
- while (1) {
- MQTTYield(&client, 1000); // Keep connection alive and process messages.
- }
- return 0;
- }
Monitorowanie pamięci:
STM32 ma ograniczone zasoby, więc należy śledzić zużycie pamięci RAM i FLASH.
Optymalizacja QoS:
Używaj poziomu QoS 0, jeśli szybkość i niskie opóźnienia są ważniejsze od niezawodności.
Wybierz QoS 1 lub 2 dla krytycznych danych.
Zabezpieczenia:
W miarę mozliwości korzystaj z połączeń TLS/SSL, jeśli broker obsługuje port 8883.
Dodaj autoryzację za pomocą nazw użytkowników i haseł.
Testowanie:
Kod możecie przetestować z popularnymi brokerami MQTT, np. HiveMQ, Mosquitto.
Własna Implementacja protokołu:
Tymczasem własna implementacja protokołu jest również prosta i wygodna z czego sam chętnie korzystam by nie używać opasłych bibliotek
gdy nie jest to konieczne a urządzenie niema za wiele procy i ograniczone zasoby jak np STM32F100. W tym przypadku całość polega
na manualnym tworzeniu ramek protokołu MQTT i wysyłaniu ich przez gniazdo TCP.
Założenia sprzętowe zostawmy wiec jak wyżej czyli STM32 komunikuje się przez Ethernet lub moduł Wi-Fi.
Korzystamy z surowego stosu TCP/IP (np. LwIP) do obsługi połączeń TCP.
będziemy implementować protokół MQTT w wersji 3.1.1.
Struktura MQTT
W protokole MQTT ramki są kodowane w formacie binarnym przez co musimy ręcznie zadbać o właściwe kodowanie i format:
Funkcja Connect:
Zawierać musi Nagłówek MQTT i polecenia dotyczące połączenia.
- // Parametry połączenia
- #define MQTT_KEEPALIVE 60 // Czas keep-alive w sekundach
- int MQTT_Connect(int socket, const char *client_id) {
- uint8_t buffer[128];
- uint16_t offset = 0;
- // Nagłówek zmienny MQTT (protocol name, level, flags, keepalive)
- buffer[offset++] = 0x10; // CONNECT fixed header
- buffer[offset++] = 0; // Placeholder for remaining length
- // Payload MQTT: Protocol Name "MQTT"
- buffer[offset++] = 0x00;
- buffer[offset++] = 0x04;
- memcpy(&buffer[offset], "MQTT", 4);
- offset += 4;
- buffer[offset++] = 0x04; // MQTT Protocol Level (4 = MQTT 3.1.1)
- buffer[offset++] = 0x02; // Connect Flags: Clean Session = 1
- buffer[offset++] = (MQTT_KEEPALIVE >> 8) & 0xFF; // Keep-Alive MSB
- buffer[offset++] = MQTT_KEEPALIVE & 0xFF; // Keep-Alive LSB
- // Payload: Client ID
- uint16_t client_id_len = strlen(client_id);
- buffer[offset++] = (client_id_len >> 8) & 0xFF;
- buffer[offset++] = client_id_len & 0xFF;
- memcpy(&buffer[offset], client_id, client_id_len);
- offset += client_id_len;
- // Ustawienie długości całkowitej (remaining length)
- buffer[1] = offset - 2;
- // Wyślij ramkę
- int sent_bytes = send(socket, buffer, offset, 0);
- if (sent_bytes < 0) {
- return -1; // Błąd wysyłania
- }
- // Odbierz odpowiedź (CONNACK)
- uint8_t response[4];
- int received_bytes = recv(socket, response, sizeof(response), 0);
- if (received_bytes != 4 || response[0] != 0x20 || response[1] != 0x02) {
- return -1; // Błąd w odpowiedzi brokera
- }
- if (response[3] != 0x00) {
- return -1; // Kod powrotu inny niż sukces
- }
- return 0; // Sukces
- }
Musi zawierać Nagłówek i payload wiadomości (temat + dane).
- int MQTT_Publish(int socket, const char *topic, const char *message) {
- uint8_t buffer[256];
- uint16_t offset = 0;
- // Nagłówek stały (Fixed Header)
- buffer[offset++] = 0x30; // PUBLISH, QoS 0, DUP 0, Retain 0
- buffer[offset++] = 0; // Placeholder for remaining length
- // Temat wiadomości (Topic Name)
- uint16_t topic_len = strlen(topic);
- buffer[offset++] = (topic_len >> 8) & 0xFF;
- buffer[offset++] = topic_len & 0xFF;
- memcpy(&buffer[offset], topic, topic_len);
- offset += topic_len;
- // Payload wiadomości
- uint16_t message_len = strlen(message);
- memcpy(&buffer[offset], message, message_len);
- offset += message_len;
- // Ustawienie długości całkowitej (remaining length)
- buffer[1] = offset - 2;
- // Wyślij ramkę
- int sent_bytes = send(socket, buffer, offset, 0);
- if (sent_bytes < 0) {
- return -1; // Błąd wysyłania
- }
- return 0; // Sukces
- }
- #include "lwip/sockets.h"
- #include <stdio.h>
- #define BROKER_IP "192.168.1.100"
- #define BROKER_PORT 1883
- int main() {
- int socket_fd;
- struct sockaddr_in broker_addr;
- // Utwórz gniazdo TCP
- socket_fd = socket(AF_INET, SOCK_STREAM, 0);
- if (socket_fd < 0) {
- printf("Error creating socket\n");
- return -1;
- }
- // Konfiguracja adresu brokera
- broker_addr.sin_family = AF_INET;
- broker_addr.sin_port = htons(BROKER_PORT);
- broker_addr.sin_addr.s_addr = inet_addr(BROKER_IP);
- // Połącz z brokerem
- if (connect(socket_fd, (struct sockaddr *)&broker_addr, sizeof(broker_addr)) < 0) {
- printf("Error connecting to broker\n");
- return -1;
- }
- // Nawiąż połączenie MQTT
- if (MQTT_Connect(socket_fd, "STM32Client") != 0) {
- printf("MQTT Connect failed\n");
- close(socket_fd);
- return -1;
- }
- printf("MQTT Connected successfully\n");
- // Publikuj wiadomość
- if (MQTT_Publish(socket_fd, "sensor/temperature", "23.5") != 0) {
- printf("MQTT Publish failed\n");
- } else {
- printf("Message published successfully\n");
- }
- // Zamknij połączenie
- close(socket_fd);
- return 0;
- }
temperaturę, W programie nie uwzględniłem obsługi żadnego czujnika - a temperatura jest wpisana na stałe i wysyłana via MQTT
do brokera , niemniej DS18B20 czy DHT lub inny każdy sobie może łatwo dodać do kodu. Kod ten może stanowić inspirację i ewentualną bazę
do innych zadań. W ESP-01 powinien być wgrany firmware obsługujący komendy AT i pracujący jako UART-WIFI.
- #include "stm32f1xx_hal.h"
- #include <string.h>
- #include <stdio.h>
- // Parametry Wi-Fi i MQTT
- #define WIFI_SSID "TwojaSiecWiFi" // Nazwa sieci Wi-Fi
- #define WIFI_PASSWORD "TwojeHaslo" // Hasło Wi-Fi
- #define BROKER_IP "192.168.1.100" // Adres IP brokera MQTT
- #define BROKER_PORT 1883 // Port MQTT
- #define CLIENT_ID "STM32Client" // Identyfikator klienta MQTT
- #define TOPIC "sensor/temperature" // Temat publikacji
- // Bufory do komunikacji z ESP-01
- #define UART_BUFFER_SIZE 512
- char uart_rx_buffer[UART_BUFFER_SIZE];
- char uart_tx_buffer[UART_BUFFER_SIZE];
- // Deklaracja UART
- UART_HandleTypeDef huart1;
- // Prototypy funkcji
- void SystemClock_Config(void);
- void Error_Handler(void);
- void ESP_SendCommand(const char *cmd, const char *expected_response, uint32_t timeout);
- void ESP_Init(void);
- void ESP_ConnectToWiFi(const char *ssid, const char *password);
- void ESP_ConnectToBroker(const char *broker_ip, uint16_t port);
- void ESP_Publish(const char *topic, const char *message);
- int main(void) {
- HAL_Init();
- SystemClock_Config();
- // Inicjalizacja UART
- __HAL_RCC_USART1_CLK_ENABLE();
- __HAL_RCC_GPIOA_CLK_ENABLE();
- GPIO_InitTypeDef GPIO_InitStruct = {0};
- GPIO_InitStruct.Pin = GPIO_PIN_9; // USART1_TX
- GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
- GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
- HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
- GPIO_InitStruct.Pin = GPIO_PIN_10; // USART1_RX
- GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
- GPIO_InitStruct.Pull = GPIO_NOPULL;
- HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
- huart1.Instance = USART1;
- huart1.Init.BaudRate = 115200;
- huart1.Init.WordLength = UART_WORDLENGTH_8B;
- huart1.Init.StopBits = UART_STOPBITS_1;
- huart1.Init.Parity = UART_PARITY_NONE;
- huart1.Init.Mode = UART_MODE_TX_RX;
- huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
- huart1.Init.OverSampling = UART_OVERSAMPLING_16;
- if (HAL_UART_Init(&huart1) != HAL_OK) {
- Error_Handler();
- }
- // Inicjalizacja ESP-01
- ESP_Init();
- ESP_ConnectToWiFi(WIFI_SSID, WIFI_PASSWORD);
- ESP_ConnectToBroker(BROKER_IP, BROKER_PORT);
- // Publikowanie wiadomości do tematu
- const char *message = "23.5"; // Przykładowa temperatura
- ESP_Publish(TOPIC, message);
- while (1) {
- // Główna pętla
- }
- }
- // Funkcja wysyłania komendy AT
- void ESP_SendCommand(const char *cmd, const char *expected_response, uint32_t timeout) {
- memset(uart_rx_buffer, 0, UART_BUFFER_SIZE);
- HAL_UART_Transmit(&huart1, (uint8_t *)cmd, strlen(cmd), HAL_MAX_DELAY);
- HAL_UART_Transmit(&huart1, (uint8_t *)"\r\n", 2, HAL_MAX_DELAY);
- uint32_t start_time = HAL_GetTick();
- while ((HAL_GetTick() - start_time) < timeout) {
- HAL_UART_Receive(&huart1, (uint8_t *)uart_rx_buffer, UART_BUFFER_SIZE, 100);
- if (strstr(uart_rx_buffer, expected_response)) {
- return; // Oczekiwana odpowiedź została otrzymana
- }
- }
- printf("Error: No response or unexpected response for: %s\n", cmd);
- Error_Handler();
- }
- // Funkcja inicjalizacji ESP-01
- void ESP_Init(void) {
- ESP_SendCommand("AT", "OK", 2000); // Test połączenia z ESP
- ESP_SendCommand("AT+RST", "OK", 5000); // Reset modułu
- ESP_SendCommand("AT+CWMODE=1", "OK", 2000); // Ustawienie modułu w trybie klienta Wi-Fi
- ESP_SendCommand("AT+CIPMUX=0", "OK", 2000); // Tryb połączenia pojedynczego
- }
- // Funkcja łączenia z Wi-Fi
- void ESP_ConnectToWiFi(const char *ssid, const char *password) {
- sprintf(uart_tx_buffer, "AT+CWJAP=\"%s\",\"%s\"", ssid, password);
- ESP_SendCommand(uart_tx_buffer, "OK", 10000); // Połączenie z Wi-Fi
- }
- // Funkcja łączenia z brokerem MQTT
- void ESP_ConnectToBroker(const char *broker_ip, uint16_t port) {
- sprintf(uart_tx_buffer, "AT+CIPSTART=\"TCP\",\"%s\",%d", broker_ip, port);
- ESP_SendCommand(uart_tx_buffer, "OK", 5000); // Połączenie z brokerem
- // Budowanie ramki CONNECT
- uint8_t buffer[128];
- uint16_t offset = 0;
- buffer[offset++] = 0x10; // CONNECT fixed header
- buffer[offset++] = 0; // Placeholder for remaining length
- buffer[offset++] = 0x00;
- buffer[offset++] = 0x04;
- memcpy(&buffer[offset], "MQTT", 4);
- offset += 4;
- buffer[offset++] = 0x04; // MQTT Protocol Level
- buffer[offset++] = 0x02; // Clean session
- buffer[offset++] = 0x00; // Keepalive MSB
- buffer[offset++] = 0x3C; // Keepalive LSB (60 sek)
- uint16_t client_id_len = strlen(CLIENT_ID);
- buffer[offset++] = (client_id_len >> 8) & 0xFF;
- buffer[offset++] = client_id_len & 0xFF;
- memcpy(&buffer[offset], CLIENT_ID, client_id_len);
- offset += client_id_len;
- buffer[1] = offset - 2; // Obliczenie remaining length
- // Wysyłanie CONNECT
- sprintf(uart_tx_buffer, "AT+CIPSEND=%d", offset);
- ESP_SendCommand(uart_tx_buffer, ">", 2000);
- HAL_UART_Transmit(&huart1, buffer, offset, HAL_MAX_DELAY);
- ESP_SendCommand("", "CONNACK", 5000); // Oczekiwanie na odpowiedź
- }
- // Funkcja publikowania wiadomości
- void ESP_Publish(const char *topic, const char *message) {
- uint8_t buffer[256];
- uint16_t offset = 0;
- buffer[offset++] = 0x30; // PUBLISH fixed header
- buffer[offset++] = 0; // Placeholder for remaining length
- uint16_t topic_len = strlen(topic);
- buffer[offset++] = (topic_len >> 8) & 0xFF;
- buffer[offset++] = topic_len & 0xFF;
- memcpy(&buffer[offset], topic, topic_len);
- offset += topic_len;
- uint16_t message_len = strlen(message);
- memcpy(&buffer[offset], message, message_len);
- offset += message_len;
- buffer[1] = offset - 2; // Obliczenie remaining length
- // Wysyłanie PUBLISH
- sprintf(uart_tx_buffer, "AT+CIPSEND=%d", offset);
- ESP_SendCommand(uart_tx_buffer, ">", 2000);
- HAL_UART_Transmit(&huart1, buffer, offset, HAL_MAX_DELAY);
- }
Komunikacja AT:
Komendy AT są wysyłane do ESP-01, który realizuje połączenie z Wi-Fi i brokerem MQTT.
Funkcja ESP_SendCommand weryfikuje odpowiedzi ESP-01.
Funkcje MQTT:
ESP_ConnectToBroker i ESP_Publish budują ramki MQTT ręcznie i wysyłają je przez ESP-01.
ESP-01 jest konfigurowany w trybie klienta Wi-Fi i pojedynczego połączenia TCP.
UART1 obsługuje komunikację z ESP-01.
Oczywiście to najprostsza opcja i nie jest doskonała należałoby jeszcze wprowadzić
Obsługę wyjątków: (np. ponowne łączenie w przypadku zerwania połączenia).
Subskrypcja: Implementacja funkcji SUBSCRIBE i odbioru wiadomości -- jeśli urządzenie ma odbierać informacje
TLS: Jeśli broker wymaga zabezpieczonego połączenia TLS/SSL
To by było na tyle , od teraz nikt nie będzie miał problemu z postawieniem klienta MQTT czy to ręcznie czy z użyciem bibliotek dedykowanych.