diff --git a/CmakeLists.txt b/CmakeLists.txt index bf4391b..aae6fa9 100644 --- a/CmakeLists.txt +++ b/CmakeLists.txt @@ -41,3 +41,65 @@ target_link_libraries(wifi_scan ) pico_add_extra_outputs(wifi_scan) + + +# Define the host name of the MQTT server in an environment variable or pass it to cmake, +# e.g. cmake -DMQTT_SERVER=myserver .. + +if ((NOT MQTT_SERVER) OR (NOT MQTT_USERNAME) OR (NOT MQTT_PASSWORD)) + message("Missing MQTT_SERVER or MQTT_USERNAME or MQTT_PASSWORD") + return() +endif() + +set(MQTT_SERVER "${MQTT_SERVER}" CACHE INTERNAL "MQTT server") +set(MQTT_USERNAME "${MQTT_USERNAME}" CACHE INTERNAL "MQTT user name") +set(MQTT_PASSWORD "${MQTT_PASSWORD}" CACHE INTERNAL "MQTT password") +set(WIFI_SSID "${WIFI_SSID}" CACHE INTERNAL "wifi ssid") +set(WIFI_PASSWORD "${WIFI_PASSWORD}" CACHE INTERNAL "wifi password") + +# Set path to the certificate include file +if (NOT MQTT_CERT_PATH) + set(MQTT_CERT_PATH ${CMAKE_CURRENT_LIST_DIR}/certs/${MQTT_SERVER}) +endif() + +# Set the name of the certificate include file +if (NOT MQTT_CERT_INC) + set(MQTT_CERT_INC mqtt_client.inc) +endif() + +set(TARGET_NAME mqtt_client) + +add_executable(${TARGET_NAME} + mqtt_client.c + ) + +target_link_libraries(${TARGET_NAME} + pico_stdlib + hardware_adc + pico_cyw43_arch_lwip_threadsafe_background + pico_lwip_mqtt + pico_mbedtls + pico_lwip_mbedtls + ) + +target_include_directories(${TARGET_NAME} PRIVATE + ${CMAKE_CURRENT_LIST_DIR} + ${CMAKE_CURRENT_LIST_DIR}/.. # for our common lwipopts or any other standard includes, if required + ) + +target_compile_definitions(${TARGET_NAME} PRIVATE + WIFI_SSID=\"${WIFI_SSID}\" + WIFI_PASSWORD=\"${WIFI_PASSWORD}\" + MQTT_SERVER=\"${MQTT_SERVER}\" + MQTT_USERNAME=\"${MQTT_USERNAME}\" + MQTT_PASSWORD=\"${MQTT_PASSWORD}\" + ) + +pico_add_extra_outputs(${TARGET_NAME}) + +# Ignore warnings from lwip code +set_source_files_properties( + ${PICO_LWIP_PATH}/src/apps/altcp_tls/altcp_tls_mbedtls.c + PROPERTIES + COMPILE_OPTIONS "-Wno-unused-result" + ) diff --git a/lwipopts.h b/lwipopts.h index 8c1301e..c309d5c 100644 --- a/lwipopts.h +++ b/lwipopts.h @@ -1,10 +1,33 @@ #ifndef _LWIPOPTS_H #define _LWIPOPTS_H +// Need more memory for TLS +#ifdef MQTT_CERT_INC +#define MEM_SIZE 8000 +#endif + // Generally you would define your own explicit list of lwIP options // (see https://www.nongnu.org/lwip/2_1_x/group__lwip__opts.html) // // This example uses a common include to avoid repetition #include "lwipopts_examples_common.h" -#endif \ No newline at end of file +#define MEMP_NUM_SYS_TIMEOUT (LWIP_NUM_SYS_TIMEOUT_INTERNAL+1) + +#ifdef MQTT_CERT_INC +#define LWIP_ALTCP 1 +#define LWIP_ALTCP_TLS 1 +#define LWIP_ALTCP_TLS_MBEDTLS 1 +#ifndef NDEBUG +#define ALTCP_MBEDTLS_DEBUG LWIP_DBG_ON +#endif +/* TCP WND must be at least 16 kb to match TLS record size + or you will get a warning "altcp_tls: TCP_WND is smaller than the RX decrypion buffer, connection RX might stall!" */ +#undef TCP_WND +#define TCP_WND 16384 +#endif // MQTT_CERT_INC + +// This defaults to 4 +#define MQTT_REQ_MAX_IN_FLIGHT 5 + +#endif diff --git a/mbedtls_config.h b/mbedtls_config.h new file mode 100644 index 0000000..f182c56 --- /dev/null +++ b/mbedtls_config.h @@ -0,0 +1,6 @@ +#ifndef MBEDTLS_CONFIG_TLS_CLIENT_H +#define MBEDTLS_CONFIG_TLS_CLIENT_H + +#include "mbedtls_config_examples_common.h" + +#endif \ No newline at end of file diff --git a/mbedtls_config_examples_common.h b/mbedtls_config_examples_common.h new file mode 100644 index 0000000..d1968bd --- /dev/null +++ b/mbedtls_config_examples_common.h @@ -0,0 +1,74 @@ +#ifndef MBEDTLS_CONFIG_EXAMPLES_COMMON_H +#define MBEDTLS_CONFIG_EXAMPLES_COMMON_H + +/* Workaround for some mbedtls source files using INT_MAX without including limits.h */ +#include + +#define MBEDTLS_NO_PLATFORM_ENTROPY +#define MBEDTLS_ENTROPY_HARDWARE_ALT + +#define MBEDTLS_SSL_OUT_CONTENT_LEN 2048 + +#define MBEDTLS_ALLOW_PRIVATE_ACCESS +#define MBEDTLS_HAVE_TIME + +#define MBEDTLS_CIPHER_MODE_CBC +#define MBEDTLS_ECP_DP_SECP192R1_ENABLED +#define MBEDTLS_ECP_DP_SECP224R1_ENABLED +#define MBEDTLS_ECP_DP_SECP256R1_ENABLED +#define MBEDTLS_ECP_DP_SECP384R1_ENABLED +#define MBEDTLS_ECP_DP_SECP521R1_ENABLED +#define MBEDTLS_ECP_DP_SECP192K1_ENABLED +#define MBEDTLS_ECP_DP_SECP224K1_ENABLED +#define MBEDTLS_ECP_DP_SECP256K1_ENABLED +#define MBEDTLS_ECP_DP_BP256R1_ENABLED +#define MBEDTLS_ECP_DP_BP384R1_ENABLED +#define MBEDTLS_ECP_DP_BP512R1_ENABLED +#define MBEDTLS_ECP_DP_CURVE25519_ENABLED +#define MBEDTLS_KEY_EXCHANGE_RSA_ENABLED +#define MBEDTLS_PKCS1_V15 +#define MBEDTLS_SHA256_SMALLER +#define MBEDTLS_SSL_SERVER_NAME_INDICATION +#define MBEDTLS_AES_C +#define MBEDTLS_ASN1_PARSE_C +#define MBEDTLS_BIGNUM_C +#define MBEDTLS_CIPHER_C +#define MBEDTLS_CTR_DRBG_C +#define MBEDTLS_ENTROPY_C +#define MBEDTLS_ERROR_C +#define MBEDTLS_MD_C +#define MBEDTLS_MD5_C +#define MBEDTLS_OID_C +#define MBEDTLS_PKCS5_C +#define MBEDTLS_PK_C +#define MBEDTLS_PK_PARSE_C +#define MBEDTLS_PLATFORM_C +#define MBEDTLS_RSA_C +#define MBEDTLS_SHA1_C +#define MBEDTLS_SHA224_C +#define MBEDTLS_SHA256_C +#define MBEDTLS_SHA512_C +#define MBEDTLS_SSL_CLI_C +#define MBEDTLS_SSL_SRV_C +#define MBEDTLS_SSL_TLS_C +#define MBEDTLS_X509_CRT_PARSE_C +#define MBEDTLS_X509_USE_C +#define MBEDTLS_AES_FEWER_TABLES + +/* TLS 1.2 */ +#define MBEDTLS_SSL_PROTO_TLS1_2 +#define MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED +#define MBEDTLS_GCM_C +#define MBEDTLS_ECDH_C +#define MBEDTLS_ECP_C +#define MBEDTLS_ECDSA_C +#define MBEDTLS_ASN1_WRITE_C + +// The following is needed to parse a certificate +#define MBEDTLS_PEM_PARSE_C +#define MBEDTLS_BASE64_C + +// The following significantly speeds up mbedtls due to NIST optimizations. +#define MBEDTLS_ECP_NIST_OPTIM + +#endif \ No newline at end of file diff --git a/mqtt_client.c b/mqtt_client.c new file mode 100644 index 0000000..b70d688 --- /dev/null +++ b/mqtt_client.c @@ -0,0 +1,378 @@ +/** + * Copyright (c) 2022 Raspberry Pi (Trading) Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +// +// Created by elliot on 25/05/24. +// +#include "pico/stdlib.h" +#include "pico/cyw43_arch.h" +#include "pico/unique_id.h" +#include "hardware/gpio.h" +#include "hardware/irq.h" +#include "hardware/adc.h" +#include "lwip/apps/mqtt.h" +#include "lwip/apps/mqtt_priv.h" // needed to set hostname +#include "lwip/dns.h" +#include "lwip/altcp_tls.h" + +// Temperature +#ifndef TEMPERATURE_UNITS +#define TEMPERATURE_UNITS 'C' // Set to 'F' for Fahrenheit +#endif + +#ifndef MQTT_SERVER +#error Need to define MQTT_SERVER +#endif + +// This file includes your client certificate for client server authentication +#ifdef MQTT_CERT_INC +#include MQTT_CERT_INC +#endif + +#ifndef MQTT_TOPIC_LEN +#define MQTT_TOPIC_LEN 100 +#endif + +typedef struct { + mqtt_client_t* mqtt_client_inst; + struct mqtt_connect_client_info_t mqtt_client_info; + char data[MQTT_OUTPUT_RINGBUF_SIZE]; + char topic[MQTT_TOPIC_LEN]; + uint32_t len; + ip_addr_t mqtt_server_address; + bool connect_done; + int subscribe_count; + bool stop_client; +} MQTT_CLIENT_DATA_T; + +#ifndef DEBUG_printf +#ifndef NDEBUG +#define DEBUG_printf printf +#else +#define DEBUG_printf(...) +#endif +#endif + +#ifndef INFO_printf +#define INFO_printf printf +#endif + +#ifndef ERROR_printf +#define ERROR_printf printf +#endif + +// how often to measure our temperature +#define TEMP_WORKER_TIME_S 10 + +// keep alive in seconds +#define MQTT_KEEP_ALIVE_S 60 + +// qos passed to mqtt_subscribe +// At most once (QoS 0) +// At least once (QoS 1) +// Exactly once (QoS 2) +#define MQTT_SUBSCRIBE_QOS 1 +#define MQTT_PUBLISH_QOS 1 +#define MQTT_PUBLISH_RETAIN 0 + +// topic used for last will and testament +#define MQTT_WILL_TOPIC "/online" +#define MQTT_WILL_MSG "0" +#define MQTT_WILL_QOS 1 + +#ifndef MQTT_DEVICE_NAME +#define MQTT_DEVICE_NAME "pico" +#endif + +// Set to 1 to add the client name to topics, to support multiple devices using the same server +#ifndef MQTT_UNIQUE_TOPIC +#define MQTT_UNIQUE_TOPIC 0 +#endif + +/* References for this implementation: + * raspberry-pi-pico-c-sdk.pdf, Section '4.1.1. hardware_adc' + * pico-examples/adc/adc_console/adc_console.c */ +static float read_onboard_temperature(const char unit) { + + /* 12-bit conversion, assume max value == ADC_VREF == 3.3 V */ + const float conversionFactor = 3.3f / (1 << 12); + + float adc = (float)adc_read() * conversionFactor; + float tempC = 27.0f - (adc - 0.706f) / 0.001721f; + + if (unit == 'C' || unit != 'F') { + return tempC; + } else if (unit == 'F') { + return tempC * 9 / 5 + 32; + } + + return -1.0f; +} + +static void pub_request_cb(__unused void *arg, err_t err) { + if (err != 0) { + ERROR_printf("pub_request_cb failed %d", err); + } +} + +static const char *full_topic(MQTT_CLIENT_DATA_T *state, const char *name) { +#if MQTT_UNIQUE_TOPIC + static char full_topic[MQTT_TOPIC_LEN]; + snprintf(full_topic, sizeof(full_topic), "/%s%s", state->mqtt_client_info.client_id, name); + return full_topic; +#else + return name; +#endif +} + +static void control_led(MQTT_CLIENT_DATA_T *state, bool on) { + // Publish state on /state topic and on/off led board + const char* message = on ? "On" : "Off"; + if (on) + cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 1); + else + cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 0); + + mqtt_publish(state->mqtt_client_inst, full_topic(state, "/led/state"), message, strlen(message), MQTT_PUBLISH_QOS, MQTT_PUBLISH_RETAIN, pub_request_cb, state); +} + +static void publish_temperature(MQTT_CLIENT_DATA_T *state) { + static float old_temperature; + const char *temperature_key = full_topic(state, "/temperature"); + float temperature = read_onboard_temperature(TEMPERATURE_UNITS); + if (temperature != old_temperature) { + old_temperature = temperature; + // Publish temperature on /temperature topic + char temp_str[16]; + snprintf(temp_str, sizeof(temp_str), "%.2f", temperature); + INFO_printf("Publishing %s to %s\n", temp_str, temperature_key); + mqtt_publish(state->mqtt_client_inst, temperature_key, temp_str, strlen(temp_str), MQTT_PUBLISH_QOS, MQTT_PUBLISH_RETAIN, pub_request_cb, state); + } +} + +static void sub_request_cb(void *arg, err_t err) { + MQTT_CLIENT_DATA_T* state = (MQTT_CLIENT_DATA_T*)arg; + if (err != 0) { + panic("subscribe request failed %d", err); + } + state->subscribe_count++; +} + +static void unsub_request_cb(void *arg, err_t err) { + MQTT_CLIENT_DATA_T* state = (MQTT_CLIENT_DATA_T*)arg; + if (err != 0) { + panic("unsubscribe request failed %d", err); + } + state->subscribe_count--; + assert(state->subscribe_count >= 0); + + // Stop if requested + if (state->subscribe_count <= 0 && state->stop_client) { + mqtt_disconnect(state->mqtt_client_inst); + } +} + +static void sub_unsub_topics(MQTT_CLIENT_DATA_T* state, bool sub) { + mqtt_request_cb_t cb = sub ? sub_request_cb : unsub_request_cb; + mqtt_sub_unsub(state->mqtt_client_inst, full_topic(state, "/led"), MQTT_SUBSCRIBE_QOS, cb, state, sub); + mqtt_sub_unsub(state->mqtt_client_inst, full_topic(state, "/print"), MQTT_SUBSCRIBE_QOS, cb, state, sub); + mqtt_sub_unsub(state->mqtt_client_inst, full_topic(state, "/ping"), MQTT_SUBSCRIBE_QOS, cb, state, sub); + mqtt_sub_unsub(state->mqtt_client_inst, full_topic(state, "/exit"), MQTT_SUBSCRIBE_QOS, cb, state, sub); +} + +static void mqtt_incoming_data_cb(void *arg, const u8_t *data, u16_t len, u8_t flags) { + MQTT_CLIENT_DATA_T* state = (MQTT_CLIENT_DATA_T*)arg; +#if MQTT_UNIQUE_TOPIC + const char *basic_topic = state->topic + strlen(state->mqtt_client_info.client_id) + 1; +#else + const char *basic_topic = state->topic; +#endif + strncpy(state->data, (const char *)data, len); + state->len = len; + state->data[len] = '\0'; + + DEBUG_printf("Topic: %s, Message: %s\n", state->topic, state->data); + if (strcmp(basic_topic, "/led") == 0) + { + if (lwip_stricmp((const char *)state->data, "On") == 0 || strcmp((const char *)state->data, "1") == 0) + control_led(state, true); + else if (lwip_stricmp((const char *)state->data, "Off") == 0 || strcmp((const char *)state->data, "0") == 0) + control_led(state, false); + } else if (strcmp(basic_topic, "/print") == 0) { + INFO_printf("%.*s\n", len, data); + } else if (strcmp(basic_topic, "/ping") == 0) { + char buf[11]; + snprintf(buf, sizeof(buf), "%u", to_ms_since_boot(get_absolute_time()) / 1000); + mqtt_publish(state->mqtt_client_inst, full_topic(state, "/uptime"), buf, strlen(buf), MQTT_PUBLISH_QOS, MQTT_PUBLISH_RETAIN, pub_request_cb, state); + } else if (strcmp(basic_topic, "/exit") == 0) { + state->stop_client = true; // stop the client when ALL subscriptions are stopped + sub_unsub_topics(state, false); // unsubscribe + } +} + +static void mqtt_incoming_publish_cb(void *arg, const char *topic, u32_t tot_len) { + MQTT_CLIENT_DATA_T* state = (MQTT_CLIENT_DATA_T*)arg; + strncpy(state->topic, topic, sizeof(state->topic)); +} + +static void temperature_worker_fn(async_context_t *context, async_at_time_worker_t *worker) { + MQTT_CLIENT_DATA_T* state = (MQTT_CLIENT_DATA_T*)worker->user_data; + publish_temperature(state); + async_context_add_at_time_worker_in_ms(context, worker, TEMP_WORKER_TIME_S * 1000); +} +static async_at_time_worker_t temperature_worker = { .do_work = temperature_worker_fn }; + +static void mqtt_connection_cb(mqtt_client_t *client, void *arg, mqtt_connection_status_t status) { + MQTT_CLIENT_DATA_T* state = (MQTT_CLIENT_DATA_T*)arg; + if (status == MQTT_CONNECT_ACCEPTED) { + state->connect_done = true; + sub_unsub_topics(state, true); // subscribe; + + // indicate online + if (state->mqtt_client_info.will_topic) { + mqtt_publish(state->mqtt_client_inst, state->mqtt_client_info.will_topic, "1", 1, MQTT_WILL_QOS, true, pub_request_cb, state); + } + + // Publish temperature every 10 sec if it's changed + temperature_worker.user_data = state; + async_context_add_at_time_worker_in_ms(cyw43_arch_async_context(), &temperature_worker, 0); + } else if (status == MQTT_CONNECT_DISCONNECTED) { + if (!state->connect_done) { + panic("Failed to connect to mqtt server"); + } + } + else { + panic("Unexpected status"); + } +} + +static void start_client(MQTT_CLIENT_DATA_T *state) { +#if LWIP_ALTCP && LWIP_ALTCP_TLS + const int port = MQTT_TLS_PORT; + INFO_printf("Using TLS\n"); +#else + const int port = MQTT_PORT; + INFO_printf("Warning: Not using TLS\n"); +#endif + + state->mqtt_client_inst = mqtt_client_new(); + if (!state->mqtt_client_inst) { + panic("MQTT client instance creation error"); + } + INFO_printf("IP address of this device %s\n", ipaddr_ntoa(&(netif_list->ip_addr))); + INFO_printf("Connecting to mqtt server at %s\n", ipaddr_ntoa(&state->mqtt_server_address)); + + cyw43_arch_lwip_begin(); + if (mqtt_client_connect(state->mqtt_client_inst, &state->mqtt_server_address, port, mqtt_connection_cb, state, &state->mqtt_client_info) != ERR_OK) { + panic("MQTT broker connection error"); + } +#if LWIP_ALTCP && LWIP_ALTCP_TLS + // This is important for MBEDTLS_SSL_SERVER_NAME_INDICATION + mbedtls_ssl_set_hostname(altcp_tls_context(state->mqtt_client_inst->conn), MQTT_SERVER); +#endif + mqtt_set_inpub_callback(state->mqtt_client_inst, mqtt_incoming_publish_cb, mqtt_incoming_data_cb, state); + cyw43_arch_lwip_end(); +} + +// Call back with a DNS result +static void dns_found(const char *hostname, const ip_addr_t *ipaddr, void *arg) { + MQTT_CLIENT_DATA_T *state = (MQTT_CLIENT_DATA_T*)arg; + if (ipaddr) { + state->mqtt_server_address = *ipaddr; + start_client(state); + } else { + panic("dns request failed"); + } +} + +int main(void) { + stdio_init_all(); + INFO_printf("mqtt client starting\n"); + + adc_init(); + adc_set_temp_sensor_enabled(true); + adc_select_input(4); + + static MQTT_CLIENT_DATA_T state; + + if (cyw43_arch_init()) { + panic("Failed to inizialize CYW43"); + } + + // Use board unique id + char unique_id_buf[5]; + pico_get_unique_board_id_string(unique_id_buf, sizeof(unique_id_buf)); + for(int i=0; i < sizeof(unique_id_buf) - 1; i++) { + unique_id_buf[i] = tolower(unique_id_buf[i]); + } + + // Generate a unique name, e.g. pico1234 + char client_id_buf[sizeof(MQTT_DEVICE_NAME) + sizeof(unique_id_buf) - 1]; + memcpy(&client_id_buf[0], MQTT_DEVICE_NAME, sizeof(MQTT_DEVICE_NAME) - 1); + memcpy(&client_id_buf[sizeof(MQTT_DEVICE_NAME) - 1], unique_id_buf, sizeof(unique_id_buf) - 1); + client_id_buf[sizeof(client_id_buf) - 1] = 0; + INFO_printf("Device name %s\n", client_id_buf); + + state.mqtt_client_info.client_id = client_id_buf; + state.mqtt_client_info.keep_alive = MQTT_KEEP_ALIVE_S; // Keep alive in sec +#if defined(MQTT_USERNAME) && defined(MQTT_PASSWORD) + state.mqtt_client_info.client_user = MQTT_USERNAME; + state.mqtt_client_info.client_pass = MQTT_PASSWORD; +#else + state.mqtt_client_info.client_user = NULL; + state.mqtt_client_info.client_pass = NULL; +#endif + static char will_topic[MQTT_TOPIC_LEN]; + strncpy(will_topic, full_topic(&state, MQTT_WILL_TOPIC), sizeof(will_topic)); + state.mqtt_client_info.will_topic = will_topic; + state.mqtt_client_info.will_msg = MQTT_WILL_MSG; + state.mqtt_client_info.will_qos = MQTT_WILL_QOS; + state.mqtt_client_info.will_retain = true; +#if LWIP_ALTCP && LWIP_ALTCP_TLS + // TLS enabled +#ifdef MQTT_CERT_INC + static const uint8_t ca_cert[] = TLS_ROOT_CERT; + static const uint8_t client_key[] = TLS_CLIENT_KEY; + static const uint8_t client_cert[] = TLS_CLIENT_CERT; + // This confirms the indentity of the server and the client + state.mqtt_client_info.tls_config = altcp_tls_create_config_client_2wayauth(ca_cert, sizeof(ca_cert), + client_key, sizeof(client_key), NULL, 0, client_cert, sizeof(client_cert)); +#if ALTCP_MBEDTLS_AUTHMODE != MBEDTLS_SSL_VERIFY_REQUIRED + WARN_printf("Warning: tls without verification is insecure\n"); +#endif +#else + state->client_info.tls_config = altcp_tls_create_config_client(NULL, 0); + WARN_printf("Warning: tls without a certificate is insecure\n"); +#endif +#endif + + cyw43_arch_enable_sta_mode(); + if (cyw43_arch_wifi_connect_timeout_ms(WIFI_SSID, WIFI_PASSWORD, CYW43_AUTH_WPA2_AES_PSK, 30000)) { + panic("Failed to connect"); + } + INFO_printf("\nConnected to Wifi\n"); + + // We are not in a callback so locking is needed when calling lwip + // Make a DNS request for the MQTT server IP address + cyw43_arch_lwip_begin(); + int err = dns_gethostbyname(MQTT_SERVER, &state.mqtt_server_address, dns_found, &state); + cyw43_arch_lwip_end(); + + if (err == ERR_OK) { + // We have the address, just start the client + start_client(&state); + } else if (err != ERR_INPROGRESS) { // ERR_INPROGRESS means expect a callback + panic("dns request failed"); + } + + while (!state.connect_done || mqtt_client_is_connected(state.mqtt_client_inst)) { + cyw43_arch_poll(); + cyw43_arch_wait_for_work_until(make_timeout_time_ms(10000)); + } + + INFO_printf("mqtt client exiting\n"); + return 0; +}