From febaa067991db4ff298539baa916293aeec9ca00 Mon Sep 17 00:00:00 2001 From: Eduard Iten Date: Thu, 17 Jul 2025 08:49:06 +0200 Subject: [PATCH] Initial commit: Set up Zephyr MQTT project --- .gitignore | 13 + .vscode/c_cpp_properties.json | 16 + CMakeLists.txt | 7 + Kconfig | 46 +++ README.rst | 97 +++++ boards/esp32c6_devkitc_esp32c6_hpcore.overkay | 5 + boards/native_sim.conf | 4 + boards/native_sim.overlay | 13 + prj.conf | 78 ++++ run_sim.sh | 7 + sample.yaml | 12 + src/main.c | 389 ++++++++++++++++++ src/mqtt_client.c | 331 +++++++++++++++ src/mqtt_client.h | 46 +++ src/mqtt_client_shell.c | 119 ++++++ src/mqtt_client_shell.h | 8 + 16 files changed, 1191 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/c_cpp_properties.json create mode 100644 CMakeLists.txt create mode 100644 Kconfig create mode 100644 README.rst create mode 100644 boards/esp32c6_devkitc_esp32c6_hpcore.overkay create mode 100644 boards/native_sim.conf create mode 100644 boards/native_sim.overlay create mode 100644 prj.conf create mode 100755 run_sim.sh create mode 100644 sample.yaml create mode 100644 src/main.c create mode 100644 src/mqtt_client.c create mode 100644 src/mqtt_client.h create mode 100644 src/mqtt_client_shell.c create mode 100644 src/mqtt_client_shell.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..77fcd80 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# Zephyr build artifacts +build/ +run/ +samples/ +flash.bin +*.bin +*.hex +*.elf +*.map +*.lst +*.obj +*.o +*.d diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..f5b1fa3 --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,16 @@ +{ + "configurations": [ + { + "name": "Linux", + "includePath": [ + "${workspaceFolder}/**" + ], + "defines": [], + "cStandard": "c17", + "cppStandard": "gnu++17", + "intelliSenseMode": "linux-gcc-x64", + "compileCommands": "${workspaceFolder}/build/compile_commands.json" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..ff9d58d --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(blinky) + +target_sources(app PRIVATE src/main.c src/mqtt_client.c src/mqtt_client_shell.c) diff --git a/Kconfig b/Kconfig new file mode 100644 index 0000000..54a1e3b --- /dev/null +++ b/Kconfig @@ -0,0 +1,46 @@ +menu "Home Assistant MQTT Options" +config HA_MQTT_BROKER_HOSTNAME + string "MQTT broker hostname" + default "homeassistant.local" + help + Hostname or IP address of the MQTT broker. + +config HA_MQTT_BROKER_PORT + int "MQTT broker port" + default 1883 + help + Port of the MQTT broker. + +config HA_MQTT_USERNAME + string "MQTT username" + default "test" + help + Username for MQTT authentication. + +config HA_MQTT_PASSWORD + string "MQTT password" + default "pytest" + help + Password for MQTT authentication. + +config HA_MQTT_NAME + string "Device name for Home Assistant device info (name)" + default "Irrigation System Modbus MQTT Gateway" + help + Sets the product name string in the Home Assistant discovery message. + +config HA_MQTT_MANUFACTURER + string "Manufacturer for Home Assistant device info (mf)" + default "Iten Engineering" + help + Sets the manufacturer string in the Home Assistant discovery message. + +config HA_MQTT_MODEL + string "Product name for Home Assistant device info (model)" + default "Modbus MQTT Gateway" + help + Sets the product name string in the Home Assistant discovery message. + +endmenu + +source "Kconfig.zephyr" \ No newline at end of file diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..ec23fe5 --- /dev/null +++ b/README.rst @@ -0,0 +1,97 @@ +.. zephyr:code-sample:: blinky + :name: Blinky + :relevant-api: gpio_interface + + Blink an LED forever using the GPIO API. + +Overview +******** + +The Blinky sample blinks an LED forever using the :ref:`GPIO API `. + +The source code shows how to: + +#. Get a pin specification from the :ref:`devicetree ` as a + :c:struct:`gpio_dt_spec` +#. Configure the GPIO pin as an output +#. Toggle the pin forever + +See :zephyr:code-sample:`pwm-blinky` for a similar sample that uses the PWM API instead. + +.. _blinky-sample-requirements: + +Requirements +************ + +Your board must: + +#. Have an LED connected via a GPIO pin (these are called "User LEDs" on many of + Zephyr's :ref:`boards`). +#. Have the LED configured using the ``led0`` devicetree alias. + +Building and Running +******************** + +Build and flash Blinky as follows, changing ``reel_board`` for your board: + +.. zephyr-app-commands:: + :zephyr-app: samples/basic/blinky + :board: reel_board + :goals: build flash + :compact: + +After flashing, the LED starts to blink and messages with the current LED state +are printed on the console. If a runtime error occurs, the sample exits without +printing to the console. + +Build errors +************ + +You will see a build error at the source code line defining the ``struct +gpio_dt_spec led`` variable if you try to build Blinky for an unsupported +board. + +On GCC-based toolchains, the error looks like this: + +.. code-block:: none + + error: '__device_dts_ord_DT_N_ALIAS_led_P_gpios_IDX_0_PH_ORD' undeclared here (not in a function) + +Adding board support +******************** + +To add support for your board, add something like this to your devicetree: + +.. code-block:: DTS + + / { + aliases { + led0 = &myled0; + }; + + leds { + compatible = "gpio-leds"; + myled0: led_0 { + gpios = <&gpio0 13 GPIO_ACTIVE_LOW>; + }; + }; + }; + +The above sets your board's ``led0`` alias to use pin 13 on GPIO controller +``gpio0``. The pin flags :c:macro:`GPIO_ACTIVE_HIGH` mean the LED is on when +the pin is set to its high state, and off when the pin is in its low state. + +Tips: + +- See :dtcompatible:`gpio-leds` for more information on defining GPIO-based LEDs + in devicetree. + +- If you're not sure what to do, check the devicetrees for supported boards which + use the same SoC as your target. See :ref:`get-devicetree-outputs` for details. + +- See :zephyr_file:`include/zephyr/dt-bindings/gpio/gpio.h` for the flags you can use + in devicetree. + +- If the LED is built in to your board hardware, the alias should be defined in + your :ref:`BOARD.dts file `. Otherwise, you can + define one in a :ref:`devicetree overlay `. diff --git a/boards/esp32c6_devkitc_esp32c6_hpcore.overkay b/boards/esp32c6_devkitc_esp32c6_hpcore.overkay new file mode 100644 index 0000000..8bca6c9 --- /dev/null +++ b/boards/esp32c6_devkitc_esp32c6_hpcore.overkay @@ -0,0 +1,5 @@ +&flash0 { + reg = <0x0 0x400000>; /* 4MB flash */ +}; + +#include "espressif/partitions_0x0_default_4M.dtsi" \ No newline at end of file diff --git a/boards/native_sim.conf b/boards/native_sim.conf new file mode 100644 index 0000000..423b2e5 --- /dev/null +++ b/boards/native_sim.conf @@ -0,0 +1,4 @@ +CONFIG_FLASH_SIMULATOR=y + +# Disable native logging backends so we don't get conflicts with the shell if we start it with --uart_stdinout +CONFIG_LOG_BACKEND_NATIVE_POSIX=n \ No newline at end of file diff --git a/boards/native_sim.overlay b/boards/native_sim.overlay new file mode 100644 index 0000000..af5808b --- /dev/null +++ b/boards/native_sim.overlay @@ -0,0 +1,13 @@ +/ { + switches { + compatible = "gpio-keys"; + sw0: switch_0 { + label = "User switch"; + gpios = <&gpio0 11 GPIO_ACTIVE_LOW>; + }; + }; + + aliases { + switch0 = &sw0; + }; +}; diff --git a/prj.conf b/prj.conf new file mode 100644 index 0000000..2b73aa6 --- /dev/null +++ b/prj.conf @@ -0,0 +1,78 @@ +# ============================================================================= +# LOGGING AND DEBUGGING +# ============================================================================= +CONFIG_LOG=y +#CONFIG_MQTT_LOG_LEVEL_DBG=y + +# ============================================================================= +# SYSTEM AND HARDWARE +# ============================================================================= +CONFIG_GPIO=y +CONFIG_POSIX_API=y +CONFIG_MAIN_STACK_SIZE=4096 +CONFIG_REBOOT=y + +# ============================================================================= +# SHELL AND COMMAND LINE INTERFACE +# ============================================================================= +CONFIG_SHELL=y +CONFIG_NET_SHELL=y +CONFIG_SHELL_PROMPT_UART="(MQTT GW)> " +CONFIG_SHELL_BACKEND_TELNET=y +CONFIG_SHELL_TELNET_PORT=23 +CONFIG_SHELL_PROMPT_TELNET="(MQTT GW)> " + +# ============================================================================= +# NETWORKING CORE +# ============================================================================= +CONFIG_NETWORKING=y +CONFIG_NET_SOCKETS=y +CONFIG_NET_TCP=y +CONFIG_NET_LOG=y +CONFIG_NET_CONFIG_SETTINGS=y +CONFIG_ZVFS_POLL_MAX=10 + + +# ============================================================================= +# IPv4 NETWORKING +# ============================================================================= +CONFIG_NET_IPV4=y +CONFIG_NET_DHCPV4=y + +# ============================================================================= +# MQTT PROTOCOL +# ============================================================================= +CONFIG_MQTT_LIB=y + +# ============================================================================= +# ENTROPY AND RANDOM NUMBER GENERATION +# ============================================================================= +CONFIG_ENTROPY_GENERATOR=y +CONFIG_TEST_RANDOM_GENERATOR=y + +# ============================================================================= +# HWINFO FOR UUID +# ============================================================================= +CONFIG_HWINFO=y + +# ============================================================================= +# JSON ENCODING SUPPORT +# ============================================================================= +CONFIG_JSON_LIBRARY=y + +# ============================================================================= +# SETTINGS SUBSYSTEM +# ============================================================================= +CONFIG_FLASH=y +CONFIG_FLASH_MAP=y +CONFIG_SETTINGS=y +CONFIG_NVS=y +CONFIG_SETTINGS_NVS=y + +# ============================================================================= +# DNS/mDNS RESOLVER SUPPORT FOR .local HOSTNAMES +# ============================================================================= +CONFIG_DNS_RESOLVER=y +CONFIG_MDNS_RESOLVER=y +CONFIG_DNS_RESOLVER_MAX_SERVERS=2 +CONFIG_DNS_RESOLVER_LOG_LEVEL_DBG=y \ No newline at end of file diff --git a/run_sim.sh b/run_sim.sh new file mode 100755 index 0000000..98818d3 --- /dev/null +++ b/run_sim.sh @@ -0,0 +1,7 @@ +#!/bin/bash +while true; do + build/zephyr/zephyr.exe --flash=flash.bin --uart_stdinout + if [ $? -eq 0 ]; then + break + fi +done diff --git a/sample.yaml b/sample.yaml new file mode 100644 index 0000000..de71191 --- /dev/null +++ b/sample.yaml @@ -0,0 +1,12 @@ +sample: + name: Blinky Sample +tests: + sample.basic.blinky: + tags: + - LED + - gpio + filter: dt_enabled_alias_with_parent_compat("led0", "gpio-leds") + depends_on: gpio + harness: led + integration_platforms: + - frdm_k64f diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..02ee229 --- /dev/null +++ b/src/main.c @@ -0,0 +1,389 @@ +/* + * Copyright (c) 2023-2024 Golioth, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +LOG_MODULE_REGISTER(ha_mqtt_switch, CONFIG_LOG_DEFAULT_LEVEL); + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "mqtt_client.h" +#include "mqtt_client_shell.h" + +#define HA_DISCOVERY_PREFIX "homeassistant" + +#define MQTT_CLIENTID "ha-switch-zephyr" + +#define MQTT_BROKER_HOSTNAME CONFIG_HA_MQTT_BROKER_HOSTNAME +#define MQTT_BROKER_PORT CONFIG_HA_MQTT_BROKER_PORT + +#define APP_CONNECT_TIMEOUT_MS 2000 +#define APP_SLEEP_MSECS 500 +#define APP_MQTT_BUFFER_SIZE 2048 + +#define SWITCH_NODE DT_ALIAS(switch0) + +#define UUID_MAX_LEN 40 +#define HA_MQTT_STR_MAX_LEN 48 +// name, uuid, state, state_changed, mqtt_running, broker_host are now extern in mqtt_client.h + +#define TOPIC_BUF_LEN 128 + +static const struct gpio_dt_spec sw = GPIO_DT_SPEC_GET_OR(SWITCH_NODE, gpios, {0}); +static struct gpio_callback switch_cb_data; + +static uint8_t rx_buffer[APP_MQTT_BUFFER_SIZE]; +static struct sockaddr_storage broker; + +// connected is only used locally in main.c +static bool connected; + + + +void switch_callback(const struct device *dev, struct gpio_callback *cb, uint32_t pins) +{ + state = !state; + state_changed = true; + LOG_INF("Switch state: %s (via GPIO)", state ? "ON" : "OFF"); +} + +// Settings handler für uuid, name, broker +static int settings_set_ha(const char *key, size_t len, settings_read_cb read_cb, void *cb_arg) +{ + if (strcmp(key, "uuid") == 0 && len < UUID_MAX_LEN) { + ssize_t rc = read_cb(cb_arg, uuid, len); + if (rc > 0) uuid[rc] = '\0'; + return 0; + } + if (strcmp(key, "name") == 0 && len < HA_MQTT_STR_MAX_LEN) { + ssize_t rc = read_cb(cb_arg, name, len); + if (rc > 0) name[rc] = '\0'; + return 0; + } + if (strcmp(key, "broker") == 0 && len < BROKER_HOST_MAX_LEN) { + ssize_t rc = read_cb(cb_arg, broker_host, len); + if (rc > 0) broker_host[rc] = '\0'; + return 0; + } + return -ENOENT; +} + +static int settings_export_ha(int (*cb)(const char *name, const void *val, size_t val_len)) +{ + int ret = 0; + ret |= cb("uuid", uuid, strlen(uuid)); + ret |= cb("name", name, strlen(name)); + ret |= cb("broker", broker_host, strlen(broker_host)); + return ret; +} + +SETTINGS_STATIC_HANDLER_DEFINE(ha, "ha", NULL, settings_set_ha, NULL, settings_export_ha); + +// Shell command: ha set name +static int cmd_ha_set_name(const struct shell *shell, size_t argc, char **argv) +{ + if (argc != 2) { + shell_print(shell, "Usage: ha set name "); + return -EINVAL; + } + if (strlen(argv[1]) >= HA_MQTT_STR_MAX_LEN) { + shell_print(shell, "Error: name too long (max %d chars)", HA_MQTT_STR_MAX_LEN - 1); + return -EINVAL; + } + strncpy(name, argv[1], HA_MQTT_STR_MAX_LEN - 1); + name[HA_MQTT_STR_MAX_LEN - 1] = '\0'; + settings_save_one("ha/name", name, strlen(name)); + shell_print(shell, "Name set to: %s", name); + return 0; +} + + +// Shell command nur noch für uuid +static int cmd_ha_set_uuid(const struct shell *shell, size_t argc, char **argv) +{ + if (argc != 2) { + shell_print(shell, "Usage: ha set uuid "); + return -EINVAL; + } + if (strlen(argv[1]) >= UUID_MAX_LEN) { + shell_print(shell, "Error: uuid too long (max %d chars)", UUID_MAX_LEN - 1); + return -EINVAL; + } + strncpy(uuid, argv[1], UUID_MAX_LEN - 1); + uuid[UUID_MAX_LEN - 1] = '\0'; + settings_save_one("ha/uuid", uuid, strlen(uuid)); + shell_print(shell, "UUID set to: %s", uuid); + return 0; +} + +static int cmd_ha_set_broker(const struct shell *shell, size_t argc, char **argv) +{ + if (mqtt_running) { + shell_print(shell, "Cannot change broker while MQTT client is running. Stop the client first."); + return -EBUSY; + } + if (argc != 2) { + shell_print(shell, "Usage: ha set broker "); + return -EINVAL; + } + if (strlen(argv[1]) >= BROKER_HOST_MAX_LEN) { + shell_print(shell, "Error: broker hostname too long (max %d chars)", BROKER_HOST_MAX_LEN - 1); + return -EINVAL; + } + strncpy(broker_host, argv[1], BROKER_HOST_MAX_LEN - 1); + broker_host[BROKER_HOST_MAX_LEN - 1] = '\0'; + settings_save_one("ha/broker", broker_host, strlen(broker_host)); + shell_print(shell, "Broker hostname set to: %s", broker_host); + return 0; +} + +static int cmd_ha_show(const struct shell *shell, size_t argc, char **argv) +{ + ARG_UNUSED(argc); + ARG_UNUSED(argv); + shell_print(shell, "UUID: %s", uuid); + shell_print(shell, "Name: %s", name); + shell_print(shell, "Broker: %s", broker_host); + return 0; +} + +static int cmd_ha_stop(const struct shell *shell, size_t argc, char **argv) +{ + int err = ha_mqtt_stop(); + if (err) { + shell_print(shell, "Failed to stop MQTT client: %d", err); + return err; + } + shell_print(shell, "MQTT client stopped."); + return 0; +} + +static int cmd_ha_start(const struct shell *shell, size_t argc, char **argv) +{ + if (mqtt_running) { + shell_print(shell, "MQTT client is already running."); + return 0; + } + int err = ha_mqtt_start(); + if (err) { + shell_print(shell, "Failed to start MQTT client: %d", err); + return err; + } + shell_print(shell, "MQTT client started."); + return 0; +} + +SHELL_STATIC_SUBCMD_SET_CREATE(ha_set_cmds, + SHELL_CMD(uuid, NULL, "Set Home Assistant UUID", cmd_ha_set_uuid), + SHELL_CMD(name, NULL, "Set Home Assistant Name", cmd_ha_set_name), + SHELL_CMD(broker, NULL, "Set MQTT broker hostname", cmd_ha_set_broker), + SHELL_SUBCMD_SET_END +); + +SHELL_STATIC_SUBCMD_SET_CREATE(ha_cmds, + SHELL_CMD(set, &ha_set_cmds, "Set settings", NULL), + SHELL_CMD(show, NULL, "Show settings", cmd_ha_show), + SHELL_CMD(stop, NULL, "Stop MQTT client", cmd_ha_stop), + SHELL_CMD(start, NULL, "Start MQTT client and send discovery", cmd_ha_start), + SHELL_SUBCMD_SET_END +); + +SHELL_CMD_REGISTER(ha, &ha_cmds, "Home Assistant commands", NULL); + +int main(void) +{ + int err; + + settings_subsys_init(); + settings_load(); + LOG_INF("Loaded uuid: '%s'", uuid); + if (strlen(uuid) == 0) { + // UUID initialisieren, falls leer: HW-UUID holen + uint8_t hwid[16]; + ssize_t len = hwinfo_get_device_id(hwid, sizeof(hwid)); + if (len > 0) { + char *p = uuid; + for (ssize_t i = 0; i < len && (p - uuid) < UUID_MAX_LEN - 2; ++i) { + p += snprintf(p, UUID_MAX_LEN - (p - uuid), "%02X", hwid[i]); + } + *p = '\0'; + settings_save_one("ha/uuid", uuid, strlen(uuid)); + LOG_INF("Generated default UUID from HW: %s", uuid); + } else { + /* Fallback for native_sim oder Plattformen ohne HW UUID: random value */ + uint32_t rnd1 = sys_rand32_get(); + uint32_t rnd2 = sys_rand32_get(); + snprintf(uuid, UUID_MAX_LEN, "SIM-%08X%08X", rnd1, rnd2); + settings_save_one("ha/uuid", uuid, strlen(uuid)); + LOG_WRN("No HW UUID available, generated random UUID: %s", uuid); + } + } + if (strlen(uuid) == 0) { + LOG_ERR("uuid is not set! Use 'ha set uuid ' in the shell."); + return -EINVAL; + } + if (strlen(uuid) >= UUID_MAX_LEN) { + LOG_ERR("uuid too long (%zu >= %d), MQTT client will not start!", strlen(uuid), UUID_MAX_LEN); + return -EINVAL; + } + + LOG_INF("HA MQTT Switch sample started"); + + if (!sw.port) { + LOG_ERR("Switch GPIO device not found in device tree!"); + return 0; + } + if (!gpio_is_ready_dt(&sw)) { + LOG_ERR("Switch device %s is not ready", sw.port->name); + return 0; + } + + err = gpio_pin_configure_dt(&sw, GPIO_INPUT); + if (err) { + LOG_ERR("Failed to configure switch pin: %d", err); + return 0; + } + + err = gpio_pin_interrupt_configure_dt(&sw, GPIO_INT_EDGE_TO_ACTIVE); + if (err) { + LOG_ERR("Failed to configure switch interrupt: %d", err); + return 0; + } + + gpio_init_callback(&switch_cb_data, switch_callback, BIT(sw.pin)); + gpio_add_callback(sw.port, &switch_cb_data); + + // Wait for DHCP to complete and get an IPv4 address + struct net_if *iface = net_if_get_default(); + LOG_INF("Waiting for IPv4 address via DHCP..."); + for (int i = 0; i < 200; ++i) { // Wait up to ~20s + const struct in_addr *addr = net_if_ipv4_get_global_addr(iface, NET_ADDR_PREFERRED); + if (addr && addr->s_addr != 0) { + char buf[NET_IPV4_ADDR_LEN]; + LOG_INF("Got IPv4 address: %s", net_addr_ntop(AF_INET, addr, buf, sizeof(buf))); + break; + } + k_sleep(K_MSEC(100)); + if (i == 199) { + LOG_ERR("No IPv4 address after DHCP timeout"); + return 0; + } + } + + // Im Main-Thread: MQTT-Start zentralisiert + err = ha_mqtt_start(); + if (err) { + LOG_ERR("Failed to start MQTT client: %d", err); + return 0; + } + + while (true) { + err = wait(APP_CONNECT_TIMEOUT_MS); + if (err) { + LOG_ERR("Failed to wait for MQTT client: %d", err); + err = mqtt_disconnect(&client_ctx, NULL); + if (err) { + LOG_ERR("Failed to disconnect MQTT client: %d", err); + } + clear_fds(); + return 0; + } + + err = mqtt_input(&client_ctx); + if (err) { + LOG_ERR("Failed to process MQTT input: %d", err); + continue; + } + + if (!connected) { + continue; + } + + err = ha_publish_discovery_document(&client_ctx); + if (err) { + LOG_ERR("Failed to publish discovery document: %d", err); + } else { + LOG_INF("Published discovery document"); + } + + char cmd_topic[TOPIC_BUF_LEN]; + build_topic(cmd_topic, sizeof(cmd_topic), "zephyr/%s/switch/set"); + err = subscribe(&client_ctx, cmd_topic); + if (err) { + LOG_ERR("Failed to subscribe to topic: %d", err); + } else { + LOG_INF("Subscribed to topic %s", cmd_topic); + } + + // Break if MQTT client is stopped externally (e.g., via shell) + if (!mqtt_running) { + break; + } + + break; + } + + while (true) { + if (!mqtt_running) { + break; + } + err = wait(APP_SLEEP_MSECS); + if (err && err != -EAGAIN) { + LOG_ERR("Failed to wait for MQTT client: %d", err); + break; + } + + err = mqtt_input(&client_ctx); + if (err) { + LOG_ERR("Failed to process MQTT input: %d", err); + continue; + } + + err = mqtt_live(&client_ctx); + if (err && err != -EAGAIN) { + LOG_ERR("Failed to send MQTT keepalive: %d", err); + break; + } else if (err == 0) { + LOG_DBG("Sent MQTT keepalive (PINGREQ)"); + } + + if (state_changed) { + char state_topic[TOPIC_BUF_LEN]; + snprintf(state_topic, sizeof(state_topic), HA_DISCOVERY_PREFIX "/switch/%s/state", uuid); + err = publish(&client_ctx, MQTT_QOS_0_AT_MOST_ONCE, state_topic, state ? "ON" : "OFF"); + if (err) { + LOG_ERR("Failed to publish switch state: %d", err); + } else { + LOG_INF("Published switch state: %s", state ? "ON" : "OFF"); + state_changed = false; + } + } + } + + err = mqtt_disconnect(&client_ctx, NULL); + if (err) { + LOG_ERR("Failed to disconnect MQTT client: %d", err); + } + + mqtt_running = false; + + clear_fds(); + + return 0; +} diff --git a/src/mqtt_client.c b/src/mqtt_client.c new file mode 100644 index 0000000..4a95b53 --- /dev/null +++ b/src/mqtt_client.c @@ -0,0 +1,331 @@ +// ...existing includes... + +#include +#include +#include + +#include +#include +#include +#include +#include +#include "mqtt_client.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +LOG_MODULE_REGISTER(mqtt_client, LOG_LEVEL_DBG); +#define MQTT_CLIENTID "ha-switch-zephyr" +#define APP_CONNECT_TIMEOUT_MS 2000 +#define APP_SLEEP_MSECS 500 +#define APP_MQTT_BUFFER_SIZE 2048 +#define UUID_MAX_LEN 40 +#define HA_MQTT_STR_MAX_LEN 48 +#define TOPIC_BUF_LEN 128 +#define BROKER_HOST_MAX_LEN 64 + +static struct pollfd fds[1]; +static int nfds; +static bool connected; +static struct mqtt_utf8 mqtt_username; +static struct mqtt_utf8 mqtt_password; +static uint8_t rx_buffer[APP_MQTT_BUFFER_SIZE]; +static uint8_t tx_buffer[APP_MQTT_BUFFER_SIZE]; +struct mqtt_client client_ctx; +static struct sockaddr_storage broker; +// Only one definition for each global variable +char broker_host[BROKER_HOST_MAX_LEN] = "127.0.0.1"; // Default broker host, replace as needed +bool mqtt_running = false; +void mqtt_evt_handler(struct mqtt_client *client, const struct mqtt_evt *evt) { + // TODO: Implement event handler logic or leave as stub if implemented elsewhere +} +bool state = false; +bool state_changed = false; +// ...existing includes... +static void prepare_fds(struct mqtt_client *client) +{ + if (client->transport.type == MQTT_TRANSPORT_NON_SECURE) { + fds[0].fd = client->transport.tcp.sock; + } +#if defined(CONFIG_MQTT_LIB_TLS) + else if (client->transport.type == MQTT_TRANSPORT_SECURE) { + fds[0].fd = client->transport.tls.sock; + } +#endif + else { + nfds = 0; + return; + } + fds[0].events = POLLIN; + nfds = 1; +} + +void clear_fds(void) +{ + nfds = 0; +} + +int wait(int timeout) +{ + int ret = -EAGAIN; + if (nfds > 0) { + ret = poll(fds, nfds, timeout); + if (ret > 0) { + if (fds[0].revents & (POLLIN | POLLERR)) { + ret = 0; + } else { + ret = -EIO; + } + } + } + return ret; +} + +void mqtt_auth_init(void) +{ + static char username_buf[64]; + static char password_buf[64]; + strncpy(username_buf, CONFIG_HA_MQTT_USERNAME, sizeof(username_buf) - 1); + username_buf[sizeof(username_buf) - 1] = '\0'; + strncpy(password_buf, CONFIG_HA_MQTT_PASSWORD, sizeof(password_buf) - 1); + password_buf[sizeof(password_buf) - 1] = '\0'; + mqtt_username.utf8 = (uint8_t *)username_buf; + mqtt_username.size = strlen(username_buf); + mqtt_password.utf8 = (uint8_t *)password_buf; + mqtt_password.size = strlen(password_buf); +} + +// ...existing code... + +#include "mqtt_client.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// ...existing code... + +#define HA_DISCOVERY_PREFIX "homeassistant" +#define MQTT_CLIENTID "ha-switch-zephyr" +#define APP_CONNECT_TIMEOUT_MS 2000 +#define APP_SLEEP_MSECS 500 +#define APP_MQTT_BUFFER_SIZE 2048 +#define UUID_MAX_LEN 40 +#define HA_MQTT_STR_MAX_LEN 48 +#define TOPIC_BUF_LEN 128 +#define BROKER_HOST_MAX_LEN 64 + +char name[HA_MQTT_STR_MAX_LEN] = CONFIG_HA_MQTT_NAME; +char uuid[UUID_MAX_LEN] = {0}; + +// ...existing code... + +// ...existing code... + +int mqtt_client_init_all(void) { + // Placeholder for future expansion + return 0; +} + +int client_init(void) +{ + int err; + LOG_DBG("Initializing MQTT client..."); + mqtt_client_init(&client_ctx); + client_ctx.broker = &broker; + client_ctx.evt_cb = mqtt_evt_handler; + client_ctx.client_id.utf8 = (uint8_t *)MQTT_CLIENTID; + client_ctx.client_id.size = strlen(MQTT_CLIENTID); + mqtt_auth_init(); + client_ctx.user_name = &mqtt_username; + client_ctx.password = &mqtt_password; + client_ctx.protocol_version = MQTT_VERSION_3_1_1; + client_ctx.transport.type = MQTT_TRANSPORT_NON_SECURE; + client_ctx.rx_buf = rx_buffer; + client_ctx.rx_buf_size = sizeof(rx_buffer); + client_ctx.tx_buf = tx_buffer; + client_ctx.tx_buf_size = sizeof(tx_buffer); + struct sockaddr_in *broker4 = (struct sockaddr_in *)&broker; + broker4->sin_family = AF_INET; + broker4->sin_port = htons(CONFIG_HA_MQTT_BROKER_PORT); + err = zsock_inet_pton(AF_INET, broker_host, &broker4->sin_addr); + if (err < 0) { + LOG_ERR("Failed to set broker address: %d", err); + return err; + } + LOG_DBG("MQTT client initialized."); + return 0; +} + +int publish(struct mqtt_client *client, enum mqtt_qos qos, const char *topic, const char *payload) +{ + struct mqtt_publish_param param; + param.message.topic.qos = qos; + param.message.topic.topic.utf8 = (uint8_t *)topic; + param.message.topic.topic.size = strlen(topic); + param.message.payload.data = (uint8_t *)payload; + param.message.payload.len = strlen(payload); + param.message_id = sys_rand32_get(); + param.dup_flag = 0U; + param.retain_flag = 0U; + return mqtt_publish(client, ¶m); +} + +int subscribe(struct mqtt_client *client, const char *topic) +{ + struct mqtt_topic sub_topic = { + .topic = { + .utf8 = (uint8_t *)topic, + .size = strlen(topic) + }, + .qos = MQTT_QOS_0_AT_MOST_ONCE + }; + const struct mqtt_subscription_list sub_list = { + .list = &sub_topic, + .list_count = 1U, + .message_id = sys_rand32_get(), + }; + return mqtt_subscribe(client, &sub_list); +} + + +void build_topic(char *buf, size_t buflen, const char *fmt) +{ + int n = snprintf(buf, buflen, fmt, uuid); + if (n < 0 || n >= buflen) { + LOG_ERR("Topic buffer overflow or encoding error! fmt='%s', uuid='%s'", fmt, uuid); + } +} + +int ha_publish_discovery_document(struct mqtt_client *client) +{ + struct device_info { + const char *identifiers; + const char *name; + const char *model; + const char *mf; + }; + struct config_info { + const char *name; + const char *cmd_t; + const char *stat_t; + const char *uniq_id; + struct device_info device; + }; + static const struct json_obj_descr device_descr[] = { + JSON_OBJ_DESCR_PRIM(struct device_info, identifiers, JSON_TOK_STRING), + JSON_OBJ_DESCR_PRIM(struct device_info, name, JSON_TOK_STRING), + JSON_OBJ_DESCR_PRIM(struct device_info, model, JSON_TOK_STRING), + JSON_OBJ_DESCR_PRIM(struct device_info, mf, JSON_TOK_STRING), + }; + static const struct json_obj_descr config_descr[] = { + JSON_OBJ_DESCR_PRIM(struct config_info, name, JSON_TOK_STRING), + JSON_OBJ_DESCR_PRIM(struct config_info, cmd_t, JSON_TOK_STRING), + JSON_OBJ_DESCR_PRIM(struct config_info, stat_t, JSON_TOK_STRING), + JSON_OBJ_DESCR_PRIM(struct config_info, uniq_id, JSON_TOK_STRING), + JSON_OBJ_DESCR_OBJECT(struct config_info, device, device_descr), + }; + char cmd_topic[TOPIC_BUF_LEN]; + char state_topic[TOPIC_BUF_LEN]; + char uniq_id[UUID_MAX_LEN]; + char ident[TOPIC_BUF_LEN]; + char disc_topic[TOPIC_BUF_LEN]; + snprintf(cmd_topic, sizeof(cmd_topic), HA_DISCOVERY_PREFIX "/switch/%s/set", uuid); + snprintf(state_topic, sizeof(state_topic), HA_DISCOVERY_PREFIX "/switch/%s/state", uuid); + strncpy(uniq_id, uuid, sizeof(uniq_id) - 1); + uniq_id[sizeof(uniq_id) - 1] = '\0'; + snprintf(ident, sizeof(ident), "zephyr-ha-%s", uuid); + snprintf(disc_topic, sizeof(disc_topic), HA_DISCOVERY_PREFIX "/switch/%s/config", uuid); + LOG_DBG("Publishing discovery document to topic: %s", disc_topic); + LOG_DBG("Command topic: %s", cmd_topic); + LOG_DBG("State topic: %s", state_topic); + LOG_DBG("Unique ID: %s", uniq_id); + LOG_DBG("Device identifier: %s", ident); + char model[HA_MQTT_STR_MAX_LEN]; + strncpy(model, CONFIG_HA_MQTT_MODEL, HA_MQTT_STR_MAX_LEN - 1); + model[HA_MQTT_STR_MAX_LEN - 1] = '\0'; + char mf[HA_MQTT_STR_MAX_LEN]; + strncpy(mf, CONFIG_HA_MQTT_MANUFACTURER, HA_MQTT_STR_MAX_LEN - 1); + mf[HA_MQTT_STR_MAX_LEN - 1] = '\0'; + struct device_info dev = { + .identifiers = ident, + .name = name, + .model = model, + .mf = mf + }; + struct config_info cfg = { + .name = name, + .cmd_t = cmd_topic, + .stat_t = state_topic, + .uniq_id = uniq_id, + .device = dev + }; + char payload[APP_MQTT_BUFFER_SIZE]; + int err; + LOG_DBG("Encoding JSON discovery document..."); + err = json_obj_encode_buf(config_descr, ARRAY_SIZE(config_descr), &cfg, payload, sizeof(payload)); + if (err < 0) { + LOG_ERR("Failed to encode JSON: %d", err); + return err; + } + LOG_DBG("Discovery document: %s", payload); + LOG_DBG("Publishing discovery document..."); + return publish(client, MQTT_QOS_0_AT_MOST_ONCE, disc_topic, payload); +} + +int ha_mqtt_start(void) +{ + int err = client_init(); + if (err) { + LOG_ERR("Failed to initialize MQTT client: %d", err); + return err; + } + err = mqtt_connect(&client_ctx); + if (err) { + LOG_ERR("Failed to connect to MQTT broker: %d", err); + return err; + } + mqtt_running = true; + struct sockaddr_in *broker4 = (struct sockaddr_in *)&broker; + char ip_str[NET_IPV4_ADDR_LEN]; + if (zsock_inet_ntop(AF_INET, &broker4->sin_addr, ip_str, sizeof(ip_str))) { + LOG_INF("Connected to MQTT broker at %s:%d", ip_str, ntohs(broker4->sin_port)); + } + prepare_fds(&client_ctx); + return 0; +} + +int ha_mqtt_stop(void) +{ + if (!mqtt_running) { + LOG_INF("MQTT client is not running."); + return 0; + } + int err = mqtt_disconnect(&client_ctx, NULL); + if (err) { + LOG_ERR("Failed to disconnect MQTT client: %d", err); + return err; + } + clear_fds(); + mqtt_running = false; + LOG_INF("MQTT client stopped."); + return 0; +} diff --git a/src/mqtt_client.h b/src/mqtt_client.h new file mode 100644 index 0000000..66b6c23 --- /dev/null +++ b/src/mqtt_client.h @@ -0,0 +1,46 @@ + +#ifndef MQTT_CLIENT_H +#define MQTT_CLIENT_H + +#define HA_MQTT_STR_MAX_LEN 48 +#define UUID_MAX_LEN 40 +#define BROKER_HOST_MAX_LEN 64 + +extern char broker_host[BROKER_HOST_MAX_LEN]; +extern bool mqtt_running; +extern bool state; +extern bool state_changed; + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +int mqtt_client_init_all(void); +int ha_mqtt_start(void); +int ha_mqtt_stop(void); +int ha_publish_discovery_document(struct mqtt_client *client); +int subscribe(struct mqtt_client *client, const char *topic); +void mqtt_evt_handler(struct mqtt_client *client, const struct mqtt_evt *evt); + +void build_topic(char *buf, size_t buflen, const char *fmt); +void clear_fds(void); +int wait(int timeout); +int publish(struct mqtt_client *client, enum mqtt_qos qos, const char *topic, const char *payload); + +extern bool mqtt_running; +extern bool state; +extern bool state_changed; +extern char uuid[]; +extern char name[]; +extern char broker_host[]; + +extern struct mqtt_client client_ctx; + +#ifdef __cplusplus +} +#endif + +#endif // MQTT_CLIENT_H diff --git a/src/mqtt_client_shell.c b/src/mqtt_client_shell.c new file mode 100644 index 0000000..8d0d3d7 --- /dev/null +++ b/src/mqtt_client_shell.c @@ -0,0 +1,119 @@ +#include "mqtt_client_shell.h" +#include "mqtt_client.h" +#include +#include +#include + +// Shell command: ha set name +static int cmd_ha_set_name(const struct shell *shell, size_t argc, char **argv) +{ + if (argc != 2) { + shell_print(shell, "Usage: ha set name "); + return -EINVAL; + } + if (strlen(argv[1]) >= HA_MQTT_STR_MAX_LEN) { + shell_print(shell, "Error: name too long (max %d chars)", HA_MQTT_STR_MAX_LEN - 1); + return -EINVAL; + } + strncpy(name, argv[1], HA_MQTT_STR_MAX_LEN - 1); + name[HA_MQTT_STR_MAX_LEN - 1] = '\0'; + settings_save_one("ha/name", name, strlen(name)); + shell_print(shell, "Name set to: %s", name); + return 0; +} + +// Shell command: ha set uuid +static int cmd_ha_set_uuid(const struct shell *shell, size_t argc, char **argv) +{ + if (argc != 2) { + shell_print(shell, "Usage: ha set uuid "); + return -EINVAL; + } + if (strlen(argv[1]) >= UUID_MAX_LEN) { + shell_print(shell, "Error: uuid too long (max %d chars)", UUID_MAX_LEN - 1); + return -EINVAL; + } + strncpy(uuid, argv[1], UUID_MAX_LEN - 1); + uuid[UUID_MAX_LEN - 1] = '\0'; + settings_save_one("ha/uuid", uuid, strlen(uuid)); + shell_print(shell, "UUID set to: %s", uuid); + return 0; +} + +// Shell command: ha set broker +static int cmd_ha_set_broker(const struct shell *shell, size_t argc, char **argv) +{ + if (mqtt_running) { + shell_print(shell, "Cannot change broker while MQTT client is running. Stop the client first."); + return -EBUSY; + } + if (argc != 2) { + shell_print(shell, "Usage: ha set broker "); + return -EINVAL; + } + if (strlen(argv[1]) >= BROKER_HOST_MAX_LEN) { + shell_print(shell, "Error: broker hostname too long (max %d chars)", BROKER_HOST_MAX_LEN - 1); + return -EINVAL; + } + strncpy(broker_host, argv[1], BROKER_HOST_MAX_LEN - 1); + broker_host[BROKER_HOST_MAX_LEN - 1] = '\0'; + settings_save_one("ha/broker", broker_host, strlen(broker_host)); + shell_print(shell, "Broker hostname set to: %s", broker_host); + return 0; +} + +static int cmd_ha_show(const struct shell *shell, size_t argc, char **argv) +{ + ARG_UNUSED(argc); + ARG_UNUSED(argv); + shell_print(shell, "UUID: %s", uuid); + shell_print(shell, "Name: %s", name); + shell_print(shell, "Broker: %s", broker_host); + return 0; +} + +static int cmd_ha_stop(const struct shell *shell, size_t argc, char **argv) +{ + int err = ha_mqtt_stop(); + if (err) { + shell_print(shell, "Failed to stop MQTT client: %d", err); + return err; + } + shell_print(shell, "MQTT client stopped."); + return 0; +} + +static int cmd_ha_start(const struct shell *shell, size_t argc, char **argv) +{ + if (mqtt_running) { + shell_print(shell, "MQTT client is already running."); + return 0; + } + int err = ha_mqtt_start(); + if (err) { + shell_print(shell, "Failed to start MQTT client: %d", err); + return err; + } + shell_print(shell, "MQTT client started."); + return 0; +} + +SHELL_STATIC_SUBCMD_SET_CREATE(ha_set_cmds, + SHELL_CMD(uuid, NULL, "Set Home Assistant UUID", cmd_ha_set_uuid), + SHELL_CMD(name, NULL, "Set Home Assistant Name", cmd_ha_set_name), + SHELL_CMD(broker, NULL, "Set MQTT broker hostname", cmd_ha_set_broker), + SHELL_SUBCMD_SET_END +); + +SHELL_STATIC_SUBCMD_SET_CREATE(ha_cmds, + SHELL_CMD(set, &ha_set_cmds, "Set settings", NULL), + SHELL_CMD(show, NULL, "Show settings", cmd_ha_show), + SHELL_CMD(stop, NULL, "Stop MQTT client", cmd_ha_stop), + SHELL_CMD(start, NULL, "Start MQTT client and send discovery", cmd_ha_start), + SHELL_SUBCMD_SET_END +); + +void mqtt_client_shell_register(void) +{ + SHELL_CMD_REGISTER(ha, &ha_cmds, "Home Assistant commands", NULL); +} diff --git a/src/mqtt_client_shell.h b/src/mqtt_client_shell.h new file mode 100644 index 0000000..372fec9 --- /dev/null +++ b/src/mqtt_client_shell.h @@ -0,0 +1,8 @@ +#ifndef MQTT_CLIENT_SHELL_H +#define MQTT_CLIENT_SHELL_H + +#include + +void mqtt_client_shell_register(void); + +#endif // MQTT_CLIENT_SHELL_H