diff --git a/.gitmodules b/.gitmodules index 51d2e5a9..60cf1860 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,7 +3,7 @@ url = https://github.com/eclipse-threadx/threadx.git [submodule "MXChip/AZ3166/deps/lib/netxduo"] path = MXChip/AZ3166/deps/lib/netxduo - url = https://github.com/eclipse-threadx/netxduo.git + url = git@github.com:Eclipse-SDV-Hackathon-Chapter-Four/netxduo.git [submodule "libs/threadx"] path = libs/threadx url = https://github.com/eclipse-threadx/threadx.git diff --git a/MXChip/AZ3166/README.md b/MXChip/AZ3166/README.md index ea79dd30..309d3dfc 100644 --- a/MXChip/AZ3166/README.md +++ b/MXChip/AZ3166/README.md @@ -112,7 +112,7 @@ winget install --id=Kitware.CMake -e To compile the application, use the provided scripts in the `MXChip/AZ3166/scripts` folder. ### Windows (PowerShell) -You can build the application using `build.ps1`. It accepts a `-Config` parameter to select the application version (`starter`, `arcade`, `telemetry`, or `mqtt`). +You can build the application using `build.ps1`. It accepts a `-Config` parameter to select the application version (`starter`, `arcade`, `telemetry`, `mqtt`, or `uprotocol-mqtt`). ```powershell .\scripts\build.ps1 -Config starter ``` @@ -176,7 +176,8 @@ The application supports multiple configurations that you can select at build ti - `starter`: Initiates the board and WiFi connectivity. - `telemetry`: Adds code to read the on-board sensors and print the output. -- `mqtt`: Adds code to publish the telemetry over MQTT. Also creates a second thread subscribing to an MQTT topic; the received messages will be printed. (Note: This configuration builds upon `telemetry`). +- `mqtt`: The original upstream MQTT telemetry demo. +- `uprotocol-mqtt`: Publishes telemetry and receives powertrain events using uProtocol over MQTT 5. See [README-UPROTOCOL.md](MXChip/AZ3166/app/uprotocol-mqtt/README-UPROTOCOL.md) for setup and deployment. - `arcade`: A collection of arcade games. Thanks to Sébastien Heurtematte for this contribution! diff --git a/MXChip/AZ3166/app/CMakeLists.txt b/MXChip/AZ3166/app/CMakeLists.txt index 79a0fa4e..7f815f7c 100644 --- a/MXChip/AZ3166/app/CMakeLists.txt +++ b/MXChip/AZ3166/app/CMakeLists.txt @@ -63,6 +63,18 @@ elseif(APP_CONFIG STREQUAL "mqtt") ${CONFIG_DIR}/main.c ${CONFIG_DIR}/cloud_config.h ) +elseif(APP_CONFIG STREQUAL "uprotocol-mqtt") + # uProtocol over MQTT 5, with the powertrain display demo. + list(APPEND SOURCES + ${COMMON_DIR}/board_init.c + ${COMMON_DIR}/screen.c + ${CONFIG_DIR}/telemetry.c + ${CONFIG_DIR}/mqtt_client.c + ${CONFIG_DIR}/powertrain_display.c + ${CONFIG_DIR}/main.c + ${CONFIG_DIR}/cloud_config.h + ${CONFIG_DIR}/uprotocol_config.h + ) else() # starter list(APPEND SOURCES ${COMMON_DIR}/board_init.c @@ -92,7 +104,7 @@ target_include_directories(${PROJECT_NAME} ${CONFIG_DIR} ) -if(APP_CONFIG STREQUAL "mqtt") +if(APP_CONFIG STREQUAL "mqtt" OR APP_CONFIG STREQUAL "uprotocol-mqtt") # Also include telemetry directory for MQTT target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/telemetry) endif() diff --git a/MXChip/AZ3166/app/common/sntp_client.c b/MXChip/AZ3166/app/common/sntp_client.c index 698349d2..52b728bc 100644 --- a/MXChip/AZ3166/app/common/sntp_client.c +++ b/MXChip/AZ3166/app/common/sntp_client.c @@ -1,6 +1,7 @@ /* * Copyright (c) Microsoft * Copyright (c) 2024 Eclipse Foundation + * Copyright (c) 2026 Eclipse ThreadX contributors * * This program and the accompanying materials are made available * under the terms of the MIT license which is available at @@ -12,7 +13,7 @@ * Microsoft - Initial version * Frédéric Desbiens - 2024 version. */ - +// Portions of this file were generated with AI assistance. #include "sntp_client.h" #include @@ -47,6 +48,9 @@ static TX_EVENT_FLAGS_GROUP sntp_flags; // Variables to keep track of time static ULONG sntp_last_time = 0; static ULONG tx_last_ticks = 0; +static uint64_t sntp_epoch_ms; +static uint64_t sntp_elapsed_ticks; +static ULONG sntp_ms_last_tick; static VOID time_update_callback(NX_SNTP_TIME_MESSAGE* time_update_ptr, NX_SNTP_TIME* local_time) { @@ -54,28 +58,35 @@ static VOID time_update_callback(NX_SNTP_TIME_MESSAGE* time_update_ptr, NX_SNTP_ tx_event_flags_set(&sntp_flags, SNTP_UPDATE_EVENT, TX_OR); } -static void set_sntp_time() +static UINT set_sntp_time() { UINT status; ULONG seconds; - ULONG milliseconds; + ULONG fraction; + TX_INTERRUPT_SAVE_AREA CHAR time_buffer[64]; - status = nx_sntp_client_get_local_time(&sntp_client, &seconds, &milliseconds, NX_NULL); + status = nx_sntp_client_get_local_time(&sntp_client, &seconds, &fraction, NX_NULL); if (status != NX_SUCCESS) { printf("ERROR: Internal error with getting local time (0x%08x)\n", status); - return; + return status; } // Stash the Unix and ThreadX times sntp_last_time = seconds - UNIX_TO_NTP_EPOCH_SECS; tx_last_ticks = tx_time_get(); + TX_DISABLE + sntp_epoch_ms = (uint64_t)sntp_last_time * 1000u + (((uint64_t)fraction * 1000u) >> 32); + sntp_elapsed_ticks = 0; + sntp_ms_last_tick = tx_last_ticks; + TX_RESTORE nx_sntp_client_utility_display_date_time(&sntp_client, time_buffer, sizeof(time_buffer)); printf("\tSNTP time update: %s\r\n", time_buffer); printf("SUCCESS: SNTP initialized\r\n"); + return NX_SUCCESS; } static UINT sntp_client_run() @@ -83,7 +94,7 @@ static UINT sntp_client_run() UINT status; NXD_ADDRESS sntp_address; - if (sntp_server_count > (sizeof(SNTP_SERVER) / sizeof(&SNTP_SERVER))) + if (sntp_server_count >= (sizeof(SNTP_SERVER) / sizeof(SNTP_SERVER[0]))) { // We rotated through all the servers, fail out. return NX_SNTP_SERVER_NOT_AVAILABLE; @@ -133,6 +144,22 @@ ULONG sntp_time_get() return sntp_time; } +/* Read/advance the millisecond clock atomically on this single-core board. + * Frequent calls retain sub-millisecond ticks instead of rounding each delta. */ +uint64_t sntp_time_get_ms(void) +{ + ULONG current; + uint64_t result; + TX_INTERRUPT_SAVE_AREA + TX_DISABLE + current = tx_time_get(); + sntp_elapsed_ticks += (ULONG)(current - sntp_ms_last_tick); + sntp_ms_last_tick = current; + result = sntp_epoch_ms ? sntp_epoch_ms + sntp_elapsed_ticks * 1000u / TX_TIMER_TICKS_PER_SECOND : 0; + TX_RESTORE + return result; +} + UINT sntp_time(ULONG* unix_time) { *unix_time = sntp_time_get(); @@ -185,7 +212,15 @@ UINT sntp_sync() while (NX_TRUE) { // Run the client - sntp_client_run(); + status = sntp_client_run(); + if (status != NX_SUCCESS) + { + if (sntp_server_count >= sizeof(SNTP_SERVER) / sizeof(SNTP_SERVER[0])) + { + break; + } + continue; + } // Wait for new events events = 0; @@ -197,11 +232,15 @@ UINT sntp_sync() nx_sntp_client_receiving_updates(&sntp_client, &server_status); // New time, update our local time and we are done - set_sntp_time(); - status = NX_SUCCESS; + status = set_sntp_time(); break; } + if (sntp_server_count >= sizeof(SNTP_SERVER) / sizeof(SNTP_SERVER[0])) + { + status = NX_SNTP_SERVER_NOT_AVAILABLE; + break; + } // Otherwise we rotate around to the next server } diff --git a/MXChip/AZ3166/app/common/sntp_client.h b/MXChip/AZ3166/app/common/sntp_client.h index 820572eb..19e97f52 100644 --- a/MXChip/AZ3166/app/common/sntp_client.h +++ b/MXChip/AZ3166/app/common/sntp_client.h @@ -1,6 +1,7 @@ /* * Copyright (c) Microsoft * Copyright (c) 2024 Eclipse Foundation + * Copyright (c) 2026 Eclipse ThreadX contributors * * This program and the accompanying materials are made available * under the terms of the MIT license which is available at @@ -12,13 +13,16 @@ * Microsoft - Initial version * Frédéric Desbiens - 2024 version. */ - +// Portions of this file were generated with AI assistance. #ifndef _SNTP_CLIENT_H #define _SNTP_CLIENT_H #include +#include ULONG sntp_time_get(); +/* Unix milliseconds, or zero until a successful sync. Call at least once per tick wrap. */ +uint64_t sntp_time_get_ms(void); UINT sntp_time(ULONG* unix_time); UINT sntp_init(); diff --git a/MXChip/AZ3166/app/common/wwd_networking.c b/MXChip/AZ3166/app/common/wwd_networking.c index 834695e3..427c9dec 100644 --- a/MXChip/AZ3166/app/common/wwd_networking.c +++ b/MXChip/AZ3166/app/common/wwd_networking.c @@ -1,6 +1,7 @@ /* * Copyright (c) Microsoft * Copyright (c) 2024 Eclipse Foundation + * Copyright (c) 2026 Eclipse ThreadX contributors * * This program and the accompanying materials are made available * under the terms of the MIT license which is available at @@ -13,8 +14,12 @@ * Frédéric Desbiens - 2024 version. */ +// Portions of this file were generated with AI assistance. + #include "wwd_networking.h" +#include + #include "nx_api.h" #include "nx_secure_tls_api.h" #include "nxd_dhcp_client.h" @@ -24,6 +29,14 @@ #include "sntp_client.h" +/* The imported WICED archive accesses these NX_IP fields at fixed offsets. + * Reject NetX configurations that would silently corrupt its IP instance. */ +typedef char wiced_netx_ip_layout_check[ + (offsetof(NX_IP, nx_ip_interface[0].nx_interface_address_mapping_needed) == 0x72d && + offsetof(NX_IP, nx_ip_interface[0].nx_interface_physical_address_msw) == 0x734 && + offsetof(NX_IP, nx_ip_interface[0].nx_interface_physical_address_lsw) == 0x738 && + offsetof(NX_IP, nx_ip_interface[0].nx_interface_ip_mtu_size) == 0x74c) ? 1 : -1]; + #define NETX_IP_STACK_SIZE 2048 #define NETX_TX_PACKET_COUNT 16 #define NETX_RX_PACKET_COUNT 12 @@ -50,6 +63,7 @@ static CHAR* netx_password; static wiced_security_t netx_mode; static NX_DHCP nx_dhcp_client; +static UINT dhcp_started = NX_FALSE; NX_IP nx_ip; NX_PACKET_POOL nx_pool[2]; // 0=TX, 1=RX. @@ -68,7 +82,8 @@ static void print_address(CHAR* preable, ULONG address) /* Join Network. */ static UINT wifi_init() { - wiced_mac_t mac; + wiced_mac_t mac = {{0}}; + wwd_result_t mac_status; printf("\r\nInitializing WiFi\r\n"); @@ -92,7 +107,8 @@ static UINT wifi_init() return NX_NOT_SUCCESSFUL; } - wwd_wifi_get_mac_address(&mac, WWD_STA_INTERFACE); + mac_status = wwd_wifi_get_mac_address(&mac, WWD_STA_INTERFACE); + printf("\tMAC read status: %ld\r\n", (long)mac_status); printf("\tMAC address: %02X:%02X:%02X:%02X:%02X:%02X\r\n", mac.octet[0], mac.octet[1], @@ -106,33 +122,88 @@ static UINT wifi_init() return NX_SUCCESS; } +/* Keep diagnostics bounded: this callback runs in the DHCP thread. */ +static VOID dhcp_state_changed(NX_DHCP* client, UCHAR state) +{ + const char* name; + NX_PARAMETER_NOT_USED(client); + + switch (state) + { + case NX_DHCP_STATE_NOT_STARTED: name = "NOT_STARTED"; break; + case NX_DHCP_STATE_BOOT: name = "BOOT"; break; + case NX_DHCP_STATE_INIT: name = "INIT"; break; + case NX_DHCP_STATE_SELECTING: name = "SELECTING (waiting for offer)"; break; + case NX_DHCP_STATE_REQUESTING: name = "REQUESTING (waiting for ACK)"; break; + case NX_DHCP_STATE_BOUND: name = "BOUND"; break; + case NX_DHCP_STATE_RENEWING: name = "RENEWING"; break; + case NX_DHCP_STATE_REBINDING: name = "REBINDING"; break; + case NX_DHCP_STATE_FORCERENEW: name = "FORCERENEW"; break; + case NX_DHCP_STATE_ADDRESS_PROBING: name = "ADDRESS_PROBING"; break; + default: name = "UNKNOWN"; break; + } + printf("DHCP state: %u %s\r\n", (unsigned int)state, name); +} + static UINT dhcp_connect(void) { - UINT status; - ULONG actual_status; + UINT status = NX_NOT_SUCCESSFUL; + ULONG actual_status = 0; ULONG ip_address; ULONG network_mask; ULONG gateway_address; + ULONG timeout; + UCHAR state; + UINT valid; + UINT interval; + ULONG discoveries; + ULONG offers; + ULONG errors; + + printf("\r\nInitializing DHCP (diagnostics enabled)\r\n"); + + /* Take a snapshot after Wi-Fi joins; earlier state changes may be missed on the console. */ + for (interval = 0; interval < 6; ++interval) + { + tx_mutex_get(&nx_dhcp_client.nx_dhcp_mutex, TX_WAIT_FOREVER); + valid = nx_dhcp_client.nx_dhcp_interface_record[0].nx_dhcp_record_valid; + state = nx_dhcp_client.nx_dhcp_interface_record[0].nx_dhcp_state; + timeout = nx_dhcp_client.nx_dhcp_interface_record[0].nx_dhcp_timeout; + discoveries = nx_dhcp_client.nx_dhcp_interface_record[0].nx_dhcp_discoveries_sent; + offers = nx_dhcp_client.nx_dhcp_interface_record[0].nx_dhcp_offers_received; + errors = nx_dhcp_client.nx_dhcp_interface_record[0].nx_dhcp_internal_errors; + tx_mutex_put(&nx_dhcp_client.nx_dhcp_mutex); + printf("DHCP at %u s: state=%u valid=%u next retry ticks=%lu WiFi ready=%u\r\n", + (unsigned int)(interval * 5), (unsigned int)state, (unsigned int)valid, + timeout, (unsigned int)(wwd_wifi_is_ready_to_transceive(WWD_STA_INTERFACE) == WWD_SUCCESS)); + printf("DHCP traffic: discover=%lu offer=%lu errors=%lu ip_tx=%lu ip_rx=%lu link=%u\r\n", + discoveries, offers, errors, nx_ip.nx_ip_total_packets_sent, + nx_ip.nx_ip_total_packets_received, (unsigned int)nx_ip.nx_ip_driver_link_up); + printf("DHCP send path: udp_tx=%lu dropped=%lu mtu=%lu pool_free=%lu\r\n", + nx_ip.nx_ip_udp_packets_sent, nx_ip.nx_ip_send_packets_dropped, + nx_ip.nx_ip_interface[0].nx_interface_ip_mtu_size, + nx_dhcp_client.nx_dhcp_packet_pool_ptr->nx_packet_pool_available); + + status = nx_ip_status_check(&nx_ip, NX_IP_ADDRESS_RESOLVED, &actual_status, + 5 * TX_TIMER_TICKS_PER_SECOND); + if (status == NX_SUCCESS) + { + break; + } + } - printf("\r\nInitializing DHCP\r\n"); - - // Wait until address is solved. - if ((status = nx_ip_status_check(&nx_ip, NX_IP_ADDRESS_RESOLVED, &actual_status, DHCP_WAIT_TIME_TICKS))) + if (status != NX_SUCCESS) { - // DHCP Failed... no IP address! - printf("ERROR: Can't resolve DHCP address (0x%08x\r\n", status); + printf("ERROR: Can't resolve DHCP address after 30 seconds (0x%08x, status=0x%08lx)\r\n", + status, actual_status); return status; } - // Get IP address and gateway address nx_ip_address_get(&nx_ip, &ip_address, &network_mask); nx_ip_gateway_address_get(&nx_ip, &gateway_address); - - // Output IP address and gateway address print_address("IP address", ip_address); print_address("Mask", network_mask); print_address("Gateway", gateway_address); - printf("SUCCESS: DHCP initialized\r\n"); return NX_SUCCESS; @@ -288,14 +359,14 @@ UINT wwd_network_init(CHAR* ssid, CHAR* password, WiFi_Mode mode) printf("ERROR: nx_dhcp_create (0x%08x)\r\n", status); } - // Start the DHCP Client. - else if ((status = nx_dhcp_start(&nx_dhcp_client))) + // Register diagnostics before DHCP starts so early transitions are visible. + else if ((status = nx_dhcp_state_change_notify(&nx_dhcp_client, dhcp_state_changed))) { nx_dhcp_delete(&nx_dhcp_client); nx_ip_delete(&nx_ip); nx_packet_pool_delete(&nx_pool[0]); nx_packet_pool_delete(&nx_pool[1]); - printf("ERROR: nx_dhcp_start (0x%08x)\r\n", status); + printf("ERROR: nx_dhcp_state_change_notify (0x%08x)\r\n", status); } // Create DNS @@ -379,6 +450,82 @@ UINT wwd_network_connect() printf("SUCCESS: WiFi connected\r\n"); } + { + wiced_mac_t associated_mac = {{0}}; + wwd_result_t mac_status = wwd_wifi_get_mac_address(&associated_mac, WWD_STA_INTERFACE); + printf("\tMAC after join (status=%ld): %02X:%02X:%02X:%02X:%02X:%02X\r\n", + (long)mac_status, + associated_mac.octet[0], associated_mac.octet[1], associated_mac.octet[2], + associated_mac.octet[3], associated_mac.octet[4], associated_mac.octet[5]); + if (mac_status != WWD_SUCCESS || + !(associated_mac.octet[0] | associated_mac.octet[1] | + associated_mac.octet[2] | associated_mac.octet[3] | + associated_mac.octet[4] | associated_mac.octet[5])) + { + printf("ERROR: WICED did not provide a valid MAC address\r\n"); + return NX_NOT_SUCCESSFUL; + } + + printf("\tNetX driver init: MAC=%04lX:%08lX MTU=%lu mapping=%u\r\n", + nx_ip.nx_ip_interface[0].nx_interface_physical_address_msw, + nx_ip.nx_ip_interface[0].nx_interface_physical_address_lsw, + nx_ip.nx_ip_interface[0].nx_interface_ip_mtu_size, + (unsigned int)nx_ip.nx_ip_interface[0].nx_interface_address_mapping_needed); + + ULONG msw = ((ULONG)associated_mac.octet[0] << 8) | associated_mac.octet[1]; + ULONG lsw = ((ULONG)associated_mac.octet[2] << 24) | + ((ULONG)associated_mac.octet[3] << 16) | + ((ULONG)associated_mac.octet[4] << 8) | associated_mac.octet[5]; + /* The imported WICED driver leaves NetX's interface MAC unset. The radio + * already has its real MAC, so update only NetX's interface record. */ + status = nx_ip_interface_physical_address_set(&nx_ip, 0, msw, lsw, NX_FALSE); + if (status != NX_SUCCESS) + { + printf("ERROR: nx_ip_interface_physical_address_set (0x%08x)\r\n", status); + return status; + } + msw = nx_ip.nx_ip_interface[0].nx_interface_physical_address_msw; + lsw = nx_ip.nx_ip_interface[0].nx_interface_physical_address_lsw; + printf("\tNetX MAC after join: %02lX:%02lX:%02lX:%02lX:%02lX:%02lX\r\n", + (msw >> 8) & 0xff, msw & 0xff, (lsw >> 24) & 0xff, + (lsw >> 16) & 0xff, (lsw >> 8) & 0xff, lsw & 0xff); + } + + /* The WICED link reports no MTU; NetX drops all outgoing IPv4 packets in that state. */ + if (nx_ip.nx_ip_interface[0].nx_interface_ip_mtu_size == 0) + { + status = nx_ip_interface_mtu_set(&nx_ip, 0, WICED_PAYLOAD_MTU); + if (status != NX_SUCCESS) + { + printf("ERROR: nx_ip_interface_mtu_set (0x%08x)\r\n", status); + return status; + } + printf("WICED IPv4 MTU set to %u\r\n", (unsigned int)WICED_PAYLOAD_MTU); + } + + /* This Wi-Fi link carries Ethernet frames. Without address mapping, + * NetX sends unicast IP packets without first resolving the peer by ARP. */ + status = nx_ip_interface_address_mapping_configure(&nx_ip, 0, NX_TRUE); + if (status != NX_SUCCESS) + { + printf("ERROR: nx_ip_interface_address_mapping_configure (0x%08x)\r\n", status); + return status; + } + printf("NetX Ethernet address mapping enabled: %u\r\n", + (unsigned int)nx_ip.nx_ip_interface[0].nx_interface_address_mapping_needed); + + // Start DHCP only after Wi-Fi association, so the first discovery can reach the network. + if (!dhcp_started) + { + status = nx_dhcp_start(&nx_dhcp_client); + if (status != NX_SUCCESS) + { + printf("ERROR: nx_dhcp_start (0x%08x)\r\n", status); + return status; + } + dhcp_started = NX_TRUE; + } + // Fetch IP details if ((status = dhcp_connect())) { @@ -395,6 +542,9 @@ UINT wwd_network_connect() else if ((status = sntp_sync())) { printf("ERROR: Failed to sync SNTP time (0x%08x)\r\n", status); + printf("ARP traffic: requests sent=%lu received=%lu responses sent=%lu received=%lu\r\n", + nx_ip.nx_ip_arp_requests_sent, nx_ip.nx_ip_arp_requests_received, + nx_ip.nx_ip_arp_responses_sent, nx_ip.nx_ip_arp_responses_received); } return status; diff --git a/MXChip/AZ3166/app/starter/cloud_config.h b/MXChip/AZ3166/app/starter/cloud_config.h index 9c4acf9e..b070a926 100644 --- a/MXChip/AZ3166/app/starter/cloud_config.h +++ b/MXChip/AZ3166/app/starter/cloud_config.h @@ -28,8 +28,8 @@ typedef enum // WiFi connection config // ---------------------------------------------------------------------------- #define HOSTNAME "eclipse-threadx" //Change to unique hostname. -#define WIFI_SSID "" -#define WIFI_PASSWORD "" +#define WIFI_SSID "SDVTeam1" +#define WIFI_PASSWORD "SDVTeam1-123456" #define WIFI_MODE WPA2_PSK_AES // ---------------------------------------------------------------------------- diff --git a/MXChip/AZ3166/app/uprotocol-mqtt/README-UPROTOCOL.md b/MXChip/AZ3166/app/uprotocol-mqtt/README-UPROTOCOL.md new file mode 100644 index 00000000..e2813999 --- /dev/null +++ b/MXChip/AZ3166/app/uprotocol-mqtt/README-UPROTOCOL.md @@ -0,0 +1,63 @@ +# Build and deploy the AZ3166 MQTT demos + +SampleX has two independent AZ3166 applications: `app/mqtt` is the original +upstream MQTT 3.1.1 demo, and `app/uprotocol-mqtt` publishes and receives +uProtocol 1.6 messages over MQTT 5. The latter uses the AZ3166 NetX Duo +submodule pinned to fork commit `0edae5f` and the uProtocol specification at +up-spec commit `3130bc5bfe661f60509b9d5e1d03995d608a18b2`. + +Run these commands from the SampleX repository root. Initialize the board +submodules before building: + +```sh +git submodule update --init MXChip/AZ3166/deps/lib/threadx MXChip/AZ3166/deps/lib/netxduo +``` + +## Build + +Use the existing build script. Its `rebuild` argument clears the shared +`MXChip/AZ3166/build` directory, which is necessary when switching app +configurations. + +```sh +# Original upstream MQTT demo +MXChip/AZ3166/scripts/build.sh mqtt rebuild + +# uProtocol MQTT 5 demo (run this command when you want to deploy uProtocol) +MXChip/AZ3166/scripts/build.sh uprotocol-mqtt rebuild +``` + +Both commands produce `MXChip/AZ3166/build/app/mxchip_threadx.bin`. +Subsequent builds of the same configuration can omit `rebuild`. +The original MQTT app has its upstream defaults in `app/mqtt/cloud_config.h`. +The uProtocol app's Wi-Fi and broker settings are in +`app/uprotocol-mqtt/cloud_config.h`; its demo identities are in +`app/uprotocol-mqtt/uprotocol_config.h`. + +The imported WICED archive uses fixed offsets in NetX's `NX_IP` structure. +Keep `NX_ENABLE_EXTENDED_NOTIFY_SUPPORT` disabled in `lib/netxduo/nx_user.h`; +`app/common/wwd_networking.c` checks those offsets at compile time. + +## Deploy + +Attach the AZ3166 to WSL with `usbipd` and mount its drive at `/mnt/AZ3166`. +The existing deployment script copies the binary from the standard build +folder: + +```sh +sudo MXChip/AZ3166/scripts/deploy.sh /mnt/AZ3166 +``` + +Check its output for a copy error: the script can print success even when +`cp` fails. Wait for DAPLink to remount its drive before resetting the board. +`FAIL.TXT` on the remounted drive indicates a programming failure. +If the board disappears from WSL after a USB reset, attach it again with +`usbipd` before the next deployment. + +For serial output, open `sudo screen /dev/ttyACM0 115200` and reset the +board. Exit `screen` with `Ctrl-A`, `K`, then `Y`. + +The original MQTT demo uses its configured raw MQTT topics. The uProtocol +app publishes telemetry to `az3166/AB/0/1/8001` and receives powertrain +events on `powertrain/BC/0/1/8001`, with attributes in MQTT 5 properties. +See [UPROTOCOL.adoc](UPROTOCOL.adoc) for its peer contract and host tests. diff --git a/MXChip/AZ3166/app/uprotocol-mqtt/UPROTOCOL.adoc b/MXChip/AZ3166/app/uprotocol-mqtt/UPROTOCOL.adoc new file mode 100644 index 00000000..1e826dfb --- /dev/null +++ b/MXChip/AZ3166/app/uprotocol-mqtt/UPROTOCOL.adoc @@ -0,0 +1,107 @@ += AZ3166 uProtocol MQTT demo + +The `uprotocol-mqtt` application uses the NetX Duo uProtocol addon at up-spec commit +`3130bc5bfe661f60509b9d5e1d03995d608a18b2`, over MQTT 5 with E2E topic mapping. +The implementation was prepared with Codex (GPT-6); human review is required before submission. + +== Build and deploy + +The AZ3166 NetX Duo submodule is pinned to the uProtocol fork commit `0edae5f`. +Initialize it with `git submodule update --init MXChip/AZ3166/deps/lib/netxduo`. +From the SampleX repository root: + +[source,sh] +---- +MXChip/AZ3166/scripts/build.sh uprotocol-mqtt rebuild +---- + +Deploy with `sudo MXChip/AZ3166/scripts/deploy.sh /mnt/AZ3166` after mounting +the board. See `README-UPROTOCOL.md` for both app configurations and deployment. +Use a clean build when switching from an older NetX Duo checkout because the MQTT client control block changed. + +== Configuration and peer contract + +Wi-Fi and broker address/port are in `app/uprotocol-mqtt/cloud_config.h`. +The demo uses plain TCP and the existing no-authentication broker configuration. +It requires a reachable MQTT 5 broker and successful SNTP synchronization. + +uProtocol settings are in `app/uprotocol-mqtt/uprotocol_config.h`: + +[cols="2,3,3"] +|=== +|Purpose |uProtocol URI |MQTT topic +|Local identity |`up://az3166/AB/1/0` |Not a publication +|Telemetry source |`up://az3166/AB/1/8001` |`az3166/AB/0/1/8001` +|Powertrain mode source |`up://powertrain/BC/1/8001` |`powertrain/BC/0/1/8001` +|=== + +These are demo identifiers, not assigned production service IDs. +Coordinate changes with the peer and keep the telemetry source's authority/entity/version equal to the local identity. +The old raw MQTT topic macros are no longer used by this demo. +Telemetry JSON and the display's JSON `Mode` field are unchanged, with uProtocol payload format JSON (3). +Raw JSON on the old `vehicle/powertrain/mode` topic will not update the display. +The publisher must include uProtocol attributes as MQTT 5 properties. + +To publish a test event using installed Python and `mosquitto_pub`: + +[source,sh] +---- +python3 MXChip/AZ3166/scripts/publish_uprotocol_powertrain.py \ + --host BROKER_IP --mode Sport +---- + +`--dry-run` prints the command without connecting. `--authority`, `--entity`, +`--version` and `--resource` allow the helper to follow changed demo identities; +numeric URI fields are hexadecimal. +The helper generates a fresh UUIDv7, includes the uProtocol MQTT properties and publishes non-retained JSON with QoS 1 and a 10-second TTL. +It does not introduce a firmware dependency. + +== Runtime + +Network startup initializes Wi-Fi, DHCP, DNS and SNTP before the transport starts. +The SNTP adapter preserves the fractional timestamp and advances Unix milliseconds +using ThreadX ticks, including tick-counter rollover while called regularly. +Startup stops on time-sync failure instead of sending invalid wall-clock timestamps. +The STM32 HAL RNG supplies fresh UUID entropy; RNG errors prevent publication. + +The MQTT thread waits at most 100 ms between processing passes when idle. +Each pass drains up to four messages and services reconnect deadlines. +The addon restores the registered listener after session loss. +Telemetry events coalesce while disconnected, so reconnect publishes current sensor data. +A failed publication is reported; the next telemetry event creates a fresh message. +The display callback keeps the original I2C mutex and parses bounded JSON payloads. + +Large transport/message buffers are static. The application MQTT stack is 8 KiB; +the internal MQTT stack remains 5 KiB. The maximum MQTT packet is 2048 bytes and +the application receive buffer is 512 bytes. Oversized input is discarded with +an explicit error by the addon. Confirm stack high-water marks and packet-pool +capacity on the physical board under expected traffic. + +The code contains no additional worker thread and uses no new firmware library dependency. +Cross-compilation is not evidence of board execution; validate Wi-Fi/SNTP/RNG, +telemetry reception, display updates, stale/malformed message rejection and broker +restart/reconnection on the board before presenting this as hardware-verified. + +== Validation + +The MQTT firmware has cross-compiled with Arm GNU 13.3.1. The linker reported +the existing RWX load-segment warning (the original demo ELF has the same +segment permissions), plus serial LTO notices. The uProtocol build has reached +DHCP and DNS on the physical board; end-to-end MQTT 5/uProtocol traffic still +needs confirmation. + +Host tests check SNTP fractional conversion, frequent clock reads, 32-bit tick +rollover and resynchronization, plus the test publisher's URI/UUID/JSON/MQTT properties. +On Linux with GCC multilib, CMake, Ninja and Python available: + +[source,sh] +---- +cmake -S MXChip/AZ3166/tests/uprotocol -B MXChip/AZ3166/build-uprotocol/host-tests -G Ninja +cmake --build MXChip/AZ3166/build-uprotocol/host-tests +ctest --test-dir MXChip/AZ3166/build-uprotocol/host-tests --output-on-failure +---- + +The clock test compiles the actual SNTP source, replacing only time, interrupt +control and SNTP timestamp access; it does not simulate networking or the board RNG. +The publisher properties were also accepted by the production uProtocol decoder +in a separate host check. diff --git a/MXChip/AZ3166/app/uprotocol-mqtt/cloud_config.h b/MXChip/AZ3166/app/uprotocol-mqtt/cloud_config.h new file mode 100644 index 00000000..9be6b688 --- /dev/null +++ b/MXChip/AZ3166/app/uprotocol-mqtt/cloud_config.h @@ -0,0 +1,57 @@ +/*************************************************************************** + * Copyright (c) 2026 Eclipse ThreadX contributors + * + * This program and the accompanying materials are made available under the + * terms of the MIT License which is available at + * https://opensource.org/licenses/MIT. + * + * AI Disclosure: Portions of this file were generated with Codex (GPT-6). + * The AI-generated portions may be considered public domain (CC0-1.0) + * and not subject to the project's licence. Human review is required. + * + * SPDX-License-Identifier: MIT and CC0-1.0 + **************************************************************************/ + +#ifndef _CLOUD_CONFIG_H +#define _CLOUD_CONFIG_H + +#include "nx_api.h" + +typedef enum +{ + None = 0, + WEP = 1, + WPA_PSK_TKIP = 2, + WPA2_PSK_AES = 3 +} WiFi_Mode; + +// ---------------------------------------------------------------------------- +// WiFi connection config +// ---------------------------------------------------------------------------- +#define HOSTNAME "eclipse-threadx" //Change to unique hostname. +#define WIFI_SSID "SDV-imx95-A" +#define WIFI_PASSWORD "eclipsesdv" +#define WIFI_MODE WPA2_PSK_AES + +// ---------------------------------------------------------------------------- +// MQTT Config +// ---------------------------------------------------------------------------- +#define MQTT_CLIENT_NAME "ThreadXAZ3166" //Change to unique name. +// imx95-a.local: use its IPv4 address directly; no username or password required. +#define MQTT_LOCAL_BROKER_IP (IP_ADDRESS(192, 168, 60, 1)) +#define MQTT_BROKER_PORT 1883 +#define MQTT_SUBSCRIBE_TOPIC "vehicle/powertrain/mode" +#define MQTT_PUBLISH_TOPIC MQTT_CLIENT_NAME "/telemetry" + +// ---------------------------------------------------------------------------- +// MQTT Support infrastructure +// ---------------------------------------------------------------------------- +// The OLED and telemetry sensors share the same I2C bus. +extern TX_MUTEX mqtt_i2c_mutex; +extern TX_EVENT_FLAGS_GROUP mqtt_app_flag; +#define MQTT_RECEIVE_EVENT 1 +#define MQTT_MESSAGE_READY 2 +#define MQTT_ALL_EVENTS 3 + + +#endif // _CLOUD_CONFIG_H diff --git a/MXChip/AZ3166/app/uprotocol-mqtt/main.c b/MXChip/AZ3166/app/uprotocol-mqtt/main.c new file mode 100644 index 00000000..a63c7db4 --- /dev/null +++ b/MXChip/AZ3166/app/uprotocol-mqtt/main.c @@ -0,0 +1,98 @@ +/*************************************************************************** + * Copyright (c) 2026 Eclipse ThreadX contributors + * + * This program and the accompanying materials are made available under the + * terms of the MIT License which is available at + * https://opensource.org/licenses/MIT. + * + * AI Disclosure: Portions of this file were generated with Codex (GPT-6). + * The AI-generated portions may be considered public domain (CC0-1.0) + * and not subject to the project's licence. Human review is required. + * + * SPDX-License-Identifier: MIT and CC0-1.0 + **************************************************************************/ +#include + +#include "tx_api.h" + +#include "board_init.h" +#include "cmsis_utils.h" +#include "mqtt_client.h" +#include "screen.h" +#include "sntp_client.h" +#include "telemetry.h" +#include "wwd_networking.h" + +#include "cloud_config.h" + +#define ECLIPSETX_THREAD_STACK_SIZE 4096 +#define ECLIPSETX_THREAD_PRIORITY 4 +#define MQTT_APP_THREAD_STACK_SIZE 8192 +TX_MUTEX mqtt_i2c_mutex; +TX_EVENT_FLAGS_GROUP mqtt_app_flag; +TX_THREAD telemetry_thread; +TX_THREAD mqtt_thread; +ULONG telemetry_thread_stack[ECLIPSETX_THREAD_STACK_SIZE / sizeof(ULONG)]; +ULONG mqtt_thread_stack[MQTT_APP_THREAD_STACK_SIZE / sizeof(ULONG)]; + +void tx_application_define(void* first_unused_memory) +{ + systick_interval_set(TX_TIMER_TICKS_PER_SECOND); + /* Both threads need these objects before they can start. */ + UINT status = tx_event_flags_create(&mqtt_app_flag, "MQTT event"); + if (status != TX_SUCCESS) + { + printf("ERROR: MQTT event creation failed (0x%02x)\r\n", status); + return; + } + status = tx_mutex_create(&mqtt_i2c_mutex, "MQTT sensor/display I2C", TX_INHERIT); + if (status != TX_SUCCESS) + { + printf("ERROR: I2C mutex creation failed (0x%02x)\r\n", status); + return; + } + + // Create Telemetry thread + status = tx_thread_create(&telemetry_thread, + "Eclipse ThreadX telemetry Thread", + telemetry_thread_entry, + 0, + telemetry_thread_stack, + ECLIPSETX_THREAD_STACK_SIZE, + ECLIPSETX_THREAD_PRIORITY, + ECLIPSETX_THREAD_PRIORITY, + TX_NO_TIME_SLICE, + TX_AUTO_START); + + if (status != TX_SUCCESS) + { + printf("ERROR: Eclipse ThreadX telemetry thread creation failed\r\n"); + } + + // Create MQTT thread + status = tx_thread_create(&mqtt_thread, + "Eclipse ThreadX MQTT Thread", + mqtt_thread_entry, + 0, + mqtt_thread_stack, + MQTT_APP_THREAD_STACK_SIZE, + ECLIPSETX_THREAD_PRIORITY, + ECLIPSETX_THREAD_PRIORITY, + TX_NO_TIME_SLICE, + TX_AUTO_START); + + if (status != TX_SUCCESS) + { + printf("ERROR: Eclipse ThreadX MQTT thread creation failed\r\n"); + }} + +int main(void) +{ + // Initialize the board + board_init(); + + // Enter the ThreadX kernel + tx_kernel_enter(); + + return 0; +} diff --git a/MXChip/AZ3166/app/uprotocol-mqtt/mqtt_client.c b/MXChip/AZ3166/app/uprotocol-mqtt/mqtt_client.c new file mode 100644 index 00000000..85977cd2 --- /dev/null +++ b/MXChip/AZ3166/app/uprotocol-mqtt/mqtt_client.c @@ -0,0 +1,259 @@ +/*************************************************************************** + * Copyright (c) 2026 Eclipse ThreadX contributors + * + * This program and the accompanying materials are made available under the + * terms of the MIT License which is available at + * https://opensource.org/licenses/MIT. + * + * AI Disclosure: Portions of this file were generated with Codex (GPT-6). + * The AI-generated portions may be considered public domain (CC0-1.0) + * and not subject to the project's licence. Human review is required. + * + * SPDX-License-Identifier: MIT and CC0-1.0 + **************************************************************************/ +/* MISRA C:2012/2023 Rule 15.5 deviation: early returns keep startup cleanup + * adjacent to failure checks. Rule 17.7 deviation: cleanup calls on valid + * ThreadX/HAL objects deliberately discard their return status. */ +#include "cloud_config.h" +#include "mqtt_client.h" +#include "nxd_uprotocol.h" +#include "powertrain_display.h" +#include "sntp_client.h" +#include "stm32f4xx_hal.h" +#include "telemetry.h" +#include "uprotocol_config.h" +#include "wwd_networking.h" + +#include +#include + +#define STRLEN(s) (sizeof(s) - 1u) +#define UP_WAIT (5u * TX_TIMER_TICKS_PER_SECOND) +#define UP_POLL ((TX_TIMER_TICKS_PER_SECOND + 9u) / 10u) + +/* Large control blocks and buffers stay off the application thread stack. */ +static ULONG mqtt_client_stack[MQTT_CLIENT_STACK_SIZE / sizeof(ULONG)]; +static NXD_MQTT_CLIENT mqtt_client; +static NXD_UPROTOCOL_CLIENT transport; +static NXD_UPROTOCOL_CONFIG configuration; +static NXD_UPROTOCOL_MESSAGE outgoing; +static NXD_UPROTOCOL_URI telemetry_uri; +static NXD_UPROTOCOL_URI powertrain_uri; +static UCHAR receive_buffer[UPROTOCOL_RECEIVE_SIZE]; +static CHAR telemetry_buffer[TELEMETRY_BUFFER_SIZE]; +static RNG_HandleTypeDef uuid_rng; + +/* The network startup has synchronized this clock before transport creation. */ +static uint64_t epoch_milliseconds(VOID *context) +{ + NX_PARAMETER_NOT_USED(context); + return sntp_time_get_ms(); +} + +/* Generate fresh UUID entropy; never substitute predictable bytes on failure. + * This handle is used only by the MQTT application thread. */ +static UINT uuid_random(UCHAR bytes[10]) +{ + UINT i; + uint32_t word = 0; + for (i = 0; i < 10u; i++) + { + if ((i % 4u) == 0u && HAL_RNG_GenerateRandomNumber(&uuid_rng, &word) != HAL_OK) + { + return NXD_UPROTOCOL_INTERNAL; + } + bytes[i] = (UCHAR)(word >> (8u * (i % 4u))); + } + return NXD_UPROTOCOL_OK; +} + +/* Listener payload is a bounded borrowed byte span, not a C string. */ +static VOID powertrain_received(const NXD_UPROTOCOL_MESSAGE *message, VOID *context) +{ + bool displayed; + NX_PARAMETER_NOT_USED(context); + if (message->type != NXD_UPROTOCOL_PUBLISH || + message->payload_format != NXD_UPROTOCOL_PAYLOAD_JSON) + { + printf("Ignoring powertrain message without JSON publish attributes\r\n"); + return; + } + if (tx_mutex_get(&mqtt_i2c_mutex, TX_WAIT_FOREVER) != TX_SUCCESS) + { + return; + } + displayed = powertrain_display_update((const char *)message->payload, message->payload_length); + tx_mutex_put(&mqtt_i2c_mutex); + if (!displayed) + { + printf("Ignoring powertrain JSON without a valid Mode string\r\n"); + } +} + +/* JSON content stays unchanged; uProtocol supplies identity, UUID and TTL. */ +static UINT send_telemetry(VOID) +{ + UCHAR entropy[10]; + UINT status = uuid_random(entropy); + if (status != NXD_UPROTOCOL_OK) + { + return status; + } + memset(&outgoing, 0, sizeof(outgoing)); + memset(telemetry_buffer, 0, sizeof(telemetry_buffer)); + get_current_telemetry_string(telemetry_buffer); + status = nxd_uprotocol_uuid_create(&outgoing.id, epoch_milliseconds(NX_NULL), entropy); + if (status != NXD_UPROTOCOL_OK) + { + return status; + } + outgoing.type = NXD_UPROTOCOL_PUBLISH; + outgoing.source = telemetry_uri; + outgoing.priority = NXD_UPROTOCOL_CS1; + outgoing.present = NXD_UPROTOCOL_HAS_TTL; + outgoing.ttl = UPROTOCOL_TELEMETRY_TTL_MS; + outgoing.payload_format = NXD_UPROTOCOL_PAYLOAD_JSON; + outgoing.payload = (const UCHAR *)telemetry_buffer; + outgoing.payload_length = (UINT)strlen(telemetry_buffer); + return nxd_uprotocol_send(&transport, &outgoing, UP_WAIT); +} + +/* Polling services both incoming publications and the addon's reconnect timer. + * Each pass drains a bounded batch so incoming traffic cannot starve telemetry. */ +static VOID transport_loop(VOID) +{ + UINT registered = 0; + UINT pending = 0; + UINT previous_status = NXD_UPROTOCOL_OK; + UINT status; + UINT i; + ULONG events; + for (;;) + { + events = 0; + tx_event_flags_get(&mqtt_app_flag, MQTT_MESSAGE_READY, TX_OR_CLEAR, &events, UP_POLL); + if (events & MQTT_MESSAGE_READY) + { + pending = 1; + } + for (i = 0; i < 4u; i++) + { + status = nxd_uprotocol_process(&transport, receive_buffer, sizeof(receive_buffer), UP_WAIT); + if (status == NXD_UPROTOCOL_NOT_FOUND) + { + status = NXD_UPROTOCOL_OK; + break; + } + if (status != NXD_UPROTOCOL_OK) + { + break; + } + } + if (status != NXD_UPROTOCOL_OK && status != previous_status) + { + printf("uProtocol process: UCode=%u MQTT=0x%x\r\n", status, transport.last_mqtt_status); + } + previous_status = status; + if (!transport.connected) + { + continue; + } + if (!registered) + { + status = nxd_uprotocol_register_listener(&transport, &powertrain_uri, NX_NULL, + powertrain_received, NX_NULL, UP_WAIT); + if (status != NXD_UPROTOCOL_OK) + { + printf("uProtocol listener registration failed: %u\r\n", status); + tx_thread_sleep(TX_TIMER_TICKS_PER_SECOND); + continue; + } + registered = 1; + printf("Listening for %s\r\n", UPROTOCOL_POWERTRAIN_URI); + } + if (pending) + { + /* A failed send may have reached the broker; publish fresh sensor + * data with a fresh UUID on the next telemetry event. */ + pending = 0; + status = send_telemetry(); + printf("uProtocol telemetry: UCode=%u\r\n", status); + } + } +} + +/* Attach the transport to an otherwise unused MQTT client. The addon owns its + * acknowledgement/disconnect callbacks; do not install raw MQTT callbacks. */ +static VOID mqtt_thread_work(NX_IP *ip, NX_PACKET_POOL *pool) +{ + UINT status; + memset(&configuration, 0, sizeof(configuration)); + if (nxd_uprotocol_uri_parse(UPROTOCOL_LOCAL_URI, &configuration.local_uri) || + nxd_uprotocol_uri_parse(UPROTOCOL_TELEMETRY_URI, &telemetry_uri) || + nxd_uprotocol_uri_parse(UPROTOCOL_POWERTRAIN_URI, &powertrain_uri)) + { + printf("Invalid uProtocol URI configuration\r\n"); + return; + } + configuration.mode = NXD_UPROTOCOL_E2E; + configuration.server.nxd_ip_version = NX_IP_VERSION_V4; + configuration.server.nxd_ip_address.v4 = MQTT_LOCAL_BROKER_IP; + configuration.port = MQTT_BROKER_PORT; + configuration.keepalive = MQTT_KEEP_ALIVE_TIMER; + configuration.clean_start = 1; + configuration.session_expiry = 0; + configuration.maximum_packet_size = UPROTOCOL_MAXIMUM_PACKET_SIZE; + configuration.clock = epoch_milliseconds; + + __HAL_RCC_RNG_CLK_ENABLE(); + uuid_rng.Instance = RNG; + if (HAL_RNG_Init(&uuid_rng) != HAL_OK) + { + printf("UUID RNG initialization failed\r\n"); + return; + } + status = nxd_mqtt_client_create(&mqtt_client, MQTT_CLIENT_NAME, MQTT_CLIENT_NAME, + STRLEN(MQTT_CLIENT_NAME), ip, pool, + mqtt_client_stack, sizeof(mqtt_client_stack), + MQTT_THREAD_PRIORTY, NX_NULL, 0); + if (status != NX_SUCCESS) + { + printf("MQTT create failed: 0x%x\r\n", status); + HAL_RNG_DeInit(&uuid_rng); + return; + } + status = nxd_uprotocol_create(&transport, &mqtt_client, &configuration); + if (status != NXD_UPROTOCOL_OK) + { + printf("uProtocol create failed: %u\r\n", status); + nxd_mqtt_client_delete(&mqtt_client); + HAL_RNG_DeInit(&uuid_rng); + return; + } + status = nxd_uprotocol_connect(&transport, UP_WAIT); + printf("uProtocol MQTT 5 connect: UCode=%u; telemetry=%s\r\n", status, UPROTOCOL_TELEMETRY_URI); + /* A failed initial connection is retried by process with backoff. */ + transport_loop(); +} + +/* Reuse the demo's Wi-Fi, DHCP, DNS and SNTP startup before using uProtocol. */ +void mqtt_thread_entry(ULONG parameter) +{ + UINT status; + NX_PARAMETER_NOT_USED(parameter); + printf("Starting Eclipse ThreadX uProtocol MQTT 5 demo\r\n"); + tx_mutex_get(&mqtt_i2c_mutex, TX_WAIT_FOREVER); + powertrain_display_waiting(); + tx_mutex_put(&mqtt_i2c_mutex); + status = wwd_network_init(WIFI_SSID, WIFI_PASSWORD, WIFI_MODE); + if (status == NX_SUCCESS) + { + status = wwd_network_connect(); + } + if (status != NX_SUCCESS || sntp_time_get_ms() == 0) + { + printf("Network/time initialization failed: 0x%x\r\n", status); + return; + } + mqtt_thread_work(&nx_ip, nx_pool); +} diff --git a/MXChip/AZ3166/app/uprotocol-mqtt/mqtt_client.h b/MXChip/AZ3166/app/uprotocol-mqtt/mqtt_client.h new file mode 100644 index 00000000..9dbc09ce --- /dev/null +++ b/MXChip/AZ3166/app/uprotocol-mqtt/mqtt_client.h @@ -0,0 +1,24 @@ +/*************************************************************************** + * Copyright (c) 2026 Eclipse ThreadX contributors + * + * This program and the accompanying materials are made available under the + * terms of the MIT License which is available at + * https://opensource.org/licenses/MIT. + * + * AI Disclosure: Portions of this file were generated with Codex (GPT-6). + * The AI-generated portions may be considered public domain (CC0-1.0) + * and not subject to the project's licence. Human review is required. + * + * SPDX-License-Identifier: MIT and CC0-1.0 + **************************************************************************/ +#ifndef MQTT_CLIENT_H +#define MQTT_CLIENT_H + +#include "tx_api.h" + +#define MQTT_CLIENT_STACK_SIZE 5120 +#define MQTT_THREAD_PRIORTY 2 +#define MQTT_KEEP_ALIVE_TIMER 300 +void mqtt_thread_entry(ULONG thread_input); + +#endif diff --git a/MXChip/AZ3166/app/uprotocol-mqtt/powertrain_display.c b/MXChip/AZ3166/app/uprotocol-mqtt/powertrain_display.c new file mode 100644 index 00000000..8cdf6162 --- /dev/null +++ b/MXChip/AZ3166/app/uprotocol-mqtt/powertrain_display.c @@ -0,0 +1,123 @@ +/*************************************************************************** + * Copyright (c) 2026 Eclipse ThreadX contributors + * + * This program and the accompanying materials are made available under the + * terms of the MIT License which is available at + * https://opensource.org/licenses/MIT. + * + * SPDX-License-Identifier: MIT + **************************************************************************/ +#include "powertrain_display.h" + +#include "jsmn.h" +#include "ssd1306.h" + +#include +#include + +static void draw_heading(void) +{ + ssd1306_Fill(Black); + ssd1306_SetCursor(2, 0); + ssd1306_WriteString("Powertrain mode", Font_7x10, White); +} + +void powertrain_display_waiting(void) +{ + draw_heading(); + ssd1306_SetCursor(2, 26); + ssd1306_WriteString("Waiting for mode", Font_7x10, White); + ssd1306_UpdateScreen(); +} + +static void draw_mode(const char *mode, size_t length) +{ + /* Short names, including "Performance", fit on one large-text line. + * Longer names wrap in the smaller font, with ellipsis if necessary. */ + FontDef font = length <= (SSD1306_WIDTH - 4) / Font_11x18.FontWidth + ? Font_11x18 : Font_7x10; + size_t columns = (SSD1306_WIDTH - 4) / font.FontWidth; + size_t capacity = columns * ((SSD1306_HEIGHT - 22) / font.FontHeight); + size_t visible = length < capacity ? length : capacity; + + draw_heading(); + for (size_t i = 0; i < visible; ++i) + { + if (i % columns == 0) + { + ssd1306_SetCursor(2, 22 + (i / columns) * font.FontHeight); + } + unsigned char ch = (unsigned char)mode[i]; + if (length > capacity && i >= capacity - 3) + { + ch = '.'; + } + /* The OLED font contains printable ASCII only. */ + ssd1306_WriteChar(ch >= 32 && ch <= 126 ? (char)ch : '?', font, White); + } + ssd1306_UpdateScreen(); +} + +bool powertrain_display_update(const char *payload, size_t payload_length) +{ + jsmn_parser parser; + jsmntok_t tokens[16]; + + if (payload == NULL || payload_length == 0) + { + return false; + } + + jsmn_init(&parser); + int count = jsmn_parse(&parser, payload, payload_length, tokens, + sizeof(tokens) / sizeof(tokens[0])); + if (count < 3 || tokens[0].type != JSMN_OBJECT || + tokens[count - 1].end > tokens[0].end) + { + return false; + } + + /* Walk top-level key/value pairs, skipping nested metadata. */ + for (int i = 1; i + 1 < count;) + { + const jsmntok_t *key = &tokens[i]; + const jsmntok_t *value = &tokens[i + 1]; + if (key->type != JSMN_STRING || key->size != 1) + { + return false; + } + if (key->end - key->start == 4 && + memcmp(payload + key->start, "Mode", 4) == 0) + { + if (value->type != JSMN_STRING) + { + return false; + } + const char *mode = payload + value->start; + size_t length = (size_t)(value->end - value->start); + while (length > 0 && isspace((unsigned char)*mode)) + { + ++mode; + --length; + } + while (length > 0 && isspace((unsigned char)mode[length - 1])) + { + --length; + } + if (length == 0) + { + return false; + } + draw_mode(mode, length); + return true; + } + + int value_end = value->end; + i += 2; + while (i < count && tokens[i].start < value_end) + { + ++i; + } + } + return false; +} diff --git a/MXChip/AZ3166/app/uprotocol-mqtt/powertrain_display.h b/MXChip/AZ3166/app/uprotocol-mqtt/powertrain_display.h new file mode 100644 index 00000000..8b2dbfce --- /dev/null +++ b/MXChip/AZ3166/app/uprotocol-mqtt/powertrain_display.h @@ -0,0 +1,23 @@ +/*************************************************************************** + * Copyright (c) 2026 Eclipse ThreadX contributors + * + * This program and the accompanying materials are made available under the + * terms of the MIT License which is available at + * https://opensource.org/licenses/MIT. + * + * SPDX-License-Identifier: MIT + **************************************************************************/ +#ifndef POWERTRAIN_DISPLAY_H +#define POWERTRAIN_DISPLAY_H + +#include +#include + +/* The caller holds mqtt_i2c_mutex while updating the display. */ +void powertrain_display_waiting(void); + +/* Display the top-level "Mode" string in a powertrain MQTT JSON payload. + * Invalid messages leave the last displayed mode intact. */ +bool powertrain_display_update(const char *payload, size_t payload_length); + +#endif diff --git a/MXChip/AZ3166/app/uprotocol-mqtt/telemetry.c b/MXChip/AZ3166/app/uprotocol-mqtt/telemetry.c new file mode 100644 index 00000000..3c654bdb --- /dev/null +++ b/MXChip/AZ3166/app/uprotocol-mqtt/telemetry.c @@ -0,0 +1,147 @@ +/*************************************************************************** + * Copyright (c) 2026 Eclipse ThreadX contributors + * + * This program and the accompanying materials are made available under the + * terms of the MIT License which is available at + * https://opensource.org/licenses/MIT. + * + * SPDX-License-Identifier: MIT + **************************************************************************/ + +#include "cloud_config.h" +#include "nanoprintf.h" +#include "sensor.h" +#include "telemetry.h" +#include + +// Refresh interval +static const int32_t telemetry_interval = 5; + +// Current data +static sensor_data current_sensor_data; + +// Telemetry output +static const int TELEMETRY_ROWS = 5; +static const int TELEMETRY_ROW_SIZE = 40; + +/* Function to compare two float arrays + * Returns true if arrays are equal within the given tolerance, otherwise false. + */ +static UINT compare_float_arrays(const float* arr1, const float* arr2, size_t size, float epsilon) { + if (arr1 == NULL || arr2 == NULL) { + return 0; + } + + for (size_t i = 0; i < size; ++i) { + if (fabsf((float)(arr1[i] - arr2[i])) > epsilon) { + return 0; // Found a difference outside the tolerance + } + } + return 1; // No significant differences found +} + +/** + * Check if the data set changed between readings. + * + * Proper epsilon values for physical sensor readings depend on the sensor's accuracy. + * We picked a margin of error that makes sense for the use case. + */ +static UINT data_changed(sensor_data const * const current_data, sensor_data const * const new_data){ + return !(current_data->pressure_hPa == new_data->pressure_hPa && + current_data->temperature_degC == new_data->temperature_degC && + current_data->humidity_perc == new_data->humidity_perc && + compare_float_arrays(current_data->acceleration_mg, new_data->acceleration_mg, 3, 0.1f) && + compare_float_arrays(current_data->magnetic_mG, new_data->magnetic_mG, 3, 1.0f)); // Could also be 5.0f +} + +static void get_sensor_data_buffer(sensor_data data, char* output){ + char buf[TELEMETRY_ROWS][TELEMETRY_ROW_SIZE]; + npf_snprintf(buf[0], TELEMETRY_ROW_SIZE, "Pressure: %.2f\r\n", (double)data.pressure_hPa); + npf_snprintf(buf[1], TELEMETRY_ROW_SIZE, "Temperature: %.2f\r\n", (double)data.temperature_degC); + npf_snprintf(buf[2], TELEMETRY_ROW_SIZE, "Humidity: %.2f\r\n", (double)data.humidity_perc); + npf_snprintf(buf[3], TELEMETRY_ROW_SIZE, "Acceleration: %.2f, %.2f, %.2f\r\n", + (double)data.acceleration_mg[0], + (double)data.acceleration_mg[1], + (double)data.acceleration_mg[2]); + npf_snprintf(buf[4], TELEMETRY_ROW_SIZE, "Magnetic: %.2f, %.2f, %.2f\r\n", + (double)data.magnetic_mG[0], + (double)data.magnetic_mG[1], + (double)data.magnetic_mG[2]); + + // Initialize the new array with an empty string. + output[0] = '\0'; + + // Concatenate the strings from the 2D array. + for (int i = 0; i < TELEMETRY_ROWS; i++) { + strcat(output, buf[i]); + } +} + +/** + * Only used if LOG_TELEMETRY is defined. + * + * Uncomment the definition in telemetry.h if needed. + */ +#ifdef LOG_TELEMETRY +static void print_sensor_data(sensor_data data){ + char data_string[TELEMETRY_BUFFER_SIZE]; + get_sensor_data_buffer(data, data_string); + printf("=====\r\n"); + printf("%s", data_string); + printf("=====\r\n\r\n"); +} +#endif + +/** + * Entry point for the telemetry thread. + */ +void telemetry_thread_entry(ULONG parameter) +{ + //UINT status; + sensor_data new_sensor_data; + + printf("Starting telemetry thread\r\n\r\n"); + + while(1){ + + // Serialize sensor reads with OLED transfers on the same I2C bus. + tx_mutex_get(&mqtt_i2c_mutex, TX_WAIT_FOREVER); + lps22hb_t lps22hb_data = lps22hb_data_read(); + new_sensor_data.temperature_degC = lps22hb_data.temperature_degC; + new_sensor_data.pressure_hPa = lps22hb_data.pressure_hPa; + hts221_data_t hts221_data = hts221_data_read(); + new_sensor_data.humidity_perc = hts221_data.humidity_perc; + lsm6dsl_data_t lsm6dsl_data = lsm6dsl_data_read(); + memcpy(new_sensor_data.acceleration_mg, + lsm6dsl_data.acceleration_mg, + sizeof(lsm6dsl_data.acceleration_mg)); + lis2mdl_data_t lis2mdl_data = lis2mdl_data_read(); + memcpy(new_sensor_data.magnetic_mG, + lis2mdl_data.magnetic_mG, + sizeof(lis2mdl_data.magnetic_mG)); + tx_mutex_put(&mqtt_i2c_mutex); + + if (data_changed(¤t_sensor_data, &new_sensor_data)){ + #ifdef LOG_TELEMETRY + printf("Telemetry changed.\r\n"); + print_sensor_data(new_sensor_data); + #endif + current_sensor_data = new_sensor_data; + tx_event_flags_set(&mqtt_app_flag, MQTT_MESSAGE_READY, TX_OR); + } + #ifdef LOG_TELEMETRY + else{ + printf("Telemetry did not change.\r\n"); + } + #endif + + tx_thread_sleep(TX_TIMER_TICKS_PER_SECOND * telemetry_interval); + } +} + +/** + * Returns current telemetry as a string. + */ +void get_current_telemetry_string(char* output){ + get_sensor_data_buffer(current_sensor_data, output); +} diff --git a/MXChip/AZ3166/app/uprotocol-mqtt/telemetry.h b/MXChip/AZ3166/app/uprotocol-mqtt/telemetry.h new file mode 100644 index 00000000..81b3cd61 --- /dev/null +++ b/MXChip/AZ3166/app/uprotocol-mqtt/telemetry.h @@ -0,0 +1,36 @@ +/*************************************************************************** + * Copyright (c) 2026 Eclipse ThreadX contributors + * + * This program and the accompanying materials are made available under the + * terms of the MIT License which is available at + * https://opensource.org/licenses/MIT. + * + * SPDX-License-Identifier: MIT + **************************************************************************/ +#include "sensor.h" +#include "tx_api.h" +#include +#include +#include + +#ifndef _TELEMETRY_H +#define _TELEMETRY_H + +#define TELEMETRY_BUFFER_SIZE 256 + +// Log telemetry if needed +//#define LOG_TELEMETRY + +// Sensor data +typedef struct{ + float pressure_hPa; + float temperature_degC; + float humidity_perc; + float acceleration_mg[3]; + float magnetic_mG[3]; +} sensor_data; + +void telemetry_thread_entry(ULONG parameter); +void get_current_telemetry_string(char* output); + +#endif // _TELEMETRY_H diff --git a/MXChip/AZ3166/app/uprotocol-mqtt/threadx_pnp_info.h b/MXChip/AZ3166/app/uprotocol-mqtt/threadx_pnp_info.h new file mode 100644 index 00000000..dc6acaba --- /dev/null +++ b/MXChip/AZ3166/app/uprotocol-mqtt/threadx_pnp_info.h @@ -0,0 +1,36 @@ +/*************************************************************************** + * Copyright (c) 2026 Eclipse ThreadX contributors + * + * This program and the accompanying materials are made available under the + * terms of the MIT License which is available at + * https://opensource.org/licenses/MIT. + * + * SPDX-License-Identifier: MIT + **************************************************************************/ + +#ifndef _THREADX_PNP_INFO_H +#define _THREADX_PNP_INFO_H + +#define DEVICE_INFO_COMPONENT_NAME "deviceInformation" + +// Device Info property names +#define DEVICE_INFO_MANUFACTURER_PROPERTY_NAME "manufacturer" +#define DEVICE_INFO_MODEL_PROPERTY_NAME "model" +#define DEVICE_INFO_SW_VERSION_PROPERTY_NAME "swVersion" +#define DEVICE_INFO_OS_NAME_PROPERTY_NAME "osName" +#define DEVICE_INFO_PROCESSOR_ARCHITECTURE_PROPERTY_NAME "processorArchitecture" +#define DEVICE_INFO_PROCESSOR_MANUFACTURER_PROPERTY_NAME "processorManufacturer" +#define DEVICE_INFO_TOTAL_STORAGE_PROPERTY_NAME "totalStorage" +#define DEVICE_INFO_TOTAL_MEMORY_PROPERTY_NAME "totalMemory" + +// Device Info property values +#define DEVICE_INFO_MANUFACTURER_PROPERTY_VALUE "MXCHIP" +#define DEVICE_INFO_MODEL_PROPERTY_VALUE "AZ3166" +#define DEVICE_INFO_SW_VERSION_PROPERTY_VALUE "6.1.4" +#define DEVICE_INFO_OS_NAME_PROPERTY_VALUE "Eclipse ThreadX" +#define DEVICE_INFO_PROCESSOR_ARCHITECTURE_PROPERTY_VALUE "Arm Cortex M4" +#define DEVICE_INFO_PROCESSOR_MANUFACTURER_PROPERTY_VALUE "STMicroelectronics" +#define DEVICE_INFO_TOTAL_STORAGE_PROPERTY_VALUE 1024 +#define DEVICE_INFO_TOTAL_MEMORY_PROPERTY_VALUE 128 + +#endif diff --git a/MXChip/AZ3166/app/uprotocol-mqtt/uprotocol_config.h b/MXChip/AZ3166/app/uprotocol-mqtt/uprotocol_config.h new file mode 100644 index 00000000..70f3f4b1 --- /dev/null +++ b/MXChip/AZ3166/app/uprotocol-mqtt/uprotocol_config.h @@ -0,0 +1,33 @@ +/*************************************************************************** + * Copyright (c) 2026 Eclipse ThreadX contributors + * + * This program and the accompanying materials are made available under the + * terms of the MIT License which is available at + * https://opensource.org/licenses/MIT. + * + * AI Disclosure: Portions of this file were generated with Codex (GPT-6). + * The AI-generated portions may be considered public domain (CC0-1.0) + * and not subject to the project's licence. Human review is required. + * + * SPDX-License-Identifier: MIT and CC0-1.0 + **************************************************************************/ + +#ifndef UPROTOCOL_CONFIG_H +#define UPROTOCOL_CONFIG_H + +/* Demo identities; coordinate these with the powertrain publisher. + * Telemetry entity/version/authority must match the local identity. */ +#ifndef UPROTOCOL_LOCAL_URI +#define UPROTOCOL_LOCAL_URI "up://az3166/AB/1/0" +#endif +#ifndef UPROTOCOL_TELEMETRY_URI +#define UPROTOCOL_TELEMETRY_URI "up://az3166/AB/1/8001" +#endif +#ifndef UPROTOCOL_POWERTRAIN_URI +#define UPROTOCOL_POWERTRAIN_URI "up://powertrain/BC/1/8001" +#endif +#define UPROTOCOL_TELEMETRY_TTL_MS 10000u +#define UPROTOCOL_RECEIVE_SIZE 512u +#define UPROTOCOL_MAXIMUM_PACKET_SIZE 2048u + +#endif diff --git a/MXChip/AZ3166/deps/lib/netxduo b/MXChip/AZ3166/deps/lib/netxduo index 6c8e9d1c..0edae5f8 160000 --- a/MXChip/AZ3166/deps/lib/netxduo +++ b/MXChip/AZ3166/deps/lib/netxduo @@ -1 +1 @@ -Subproject commit 6c8e9d1c95d71bd4b313e1cc37d8f8841543b248 +Subproject commit 0edae5f800ce6adbc6a1f0366a1621a8bc66a7a3 diff --git a/MXChip/AZ3166/lib/CMakeLists.txt b/MXChip/AZ3166/lib/CMakeLists.txt index ac81a798..3d5b9666 100644 --- a/MXChip/AZ3166/lib/CMakeLists.txt +++ b/MXChip/AZ3166/lib/CMakeLists.txt @@ -1,5 +1,6 @@ # Copyright (c) Microsoft # Copyright (c) 2024 Eclipse Foundation +# Copyright (c) 2026 Eclipse ThreadX contributors # # This program and the accompanying materials are made available # under the terms of the MIT license which is available at @@ -10,6 +11,7 @@ # Contributors: # Microsoft - Initial version # Frédéric Desbiens - 2024 version. +# Portions of this file were generated with AI assistance. # Define ThreadX user configuration set(TX_USER_FILE "${CMAKE_CURRENT_LIST_DIR}/threadx/tx_user.h" CACHE STRING "Enable TX user configuration") @@ -20,6 +22,9 @@ set(NXD_ENABLE_FILE_SERVERS OFF CACHE BOOL "Disable fileX dependency by netxduo" # Core libraries add_subdirectory(${SHARED_LIB_DIR}/threadx threadx) +if(APP_CONFIG STREQUAL "uprotocol-mqtt" AND NOT EXISTS "${SHARED_LIB_DIR}/netxduo/addons/uprotocol/nxd_uprotocol.h") + message(FATAL_ERROR "The uProtocol MQTT demo requires the pinned NetX Duo submodule. Run git submodule update --init MXChip/AZ3166/deps/lib/netxduo") +endif() add_subdirectory(${SHARED_LIB_DIR}/netxduo netxduo) add_subdirectory(${SHARED_LIB_DIR}/jsmn jsmn) add_subdirectory(${SHARED_LIB_DIR}/nanoprintf nanoprintf) diff --git a/MXChip/AZ3166/lib/netxduo/nx_user.h b/MXChip/AZ3166/lib/netxduo/nx_user.h index 48a54de1..3d112e5d 100644 --- a/MXChip/AZ3166/lib/netxduo/nx_user.h +++ b/MXChip/AZ3166/lib/netxduo/nx_user.h @@ -1,6 +1,7 @@ /* * Copyright (c) Microsoft * Copyright (c) 2024 Eclipse Foundation + * Copyright (c) 2026 Eclipse ThreadX contributors * * This program and the accompanying materials are made available * under the terms of the MIT license which is available at @@ -12,12 +13,13 @@ * Microsoft - Initial version * Frédéric Desbiens - 2024 version. */ +// Portions of this file were generated with AI assistance. #ifndef NX_USER_H #define NX_USER_H #define NX_SECURE_ENABLE -#define NX_ENABLE_EXTENDED_NOTIFY_SUPPORT +/* Keep NX_IP layout compatible with the prebuilt WICED NetX driver. */ #define NX_ENABLE_IP_PACKET_FILTER #define NX_DISABLE_IPV6 #define NX_DNS_CLIENT_USER_CREATE_PACKET_POOL diff --git a/MXChip/AZ3166/lib/stm32cubef4/CMakeLists.txt b/MXChip/AZ3166/lib/stm32cubef4/CMakeLists.txt index b0128dab..634f5eb0 100644 --- a/MXChip/AZ3166/lib/stm32cubef4/CMakeLists.txt +++ b/MXChip/AZ3166/lib/stm32cubef4/CMakeLists.txt @@ -1,8 +1,11 @@ +# Copyright (c) 2026 Eclipse ThreadX contributors +# SPDX-License-Identifier: MIT +# Portions of this file were generated with AI assistance. set(STM32_FAMILY "F4") set(STM32Cube_DIR "./") find_package(CMSIS REQUIRED) -find_package(STM32HAL REQUIRED COMPONENTS dma flash gpio tim timebase_tim uart usart i2c) +find_package(STM32HAL REQUIRED COMPONENTS dma flash gpio tim timebase_tim uart usart i2c rng) set(TARGET stm32cubef4) diff --git a/MXChip/AZ3166/lib/stm32cubef4/config/stm32f4xx_hal_conf.h b/MXChip/AZ3166/lib/stm32cubef4/config/stm32f4xx_hal_conf.h index 5125d8e4..7ed824e9 100644 --- a/MXChip/AZ3166/lib/stm32cubef4/config/stm32f4xx_hal_conf.h +++ b/MXChip/AZ3166/lib/stm32cubef4/config/stm32f4xx_hal_conf.h @@ -10,6 +10,7 @@ * *

© Copyright (c) 2017 STMicroelectronics. * All rights reserved.

+ * Copyright (c) 2026 Eclipse ThreadX contributors * * This software component is licensed by ST under BSD 3-Clause license, * the "License"; You may not use this file except in compliance with the @@ -17,7 +18,9 @@ * opensource.org/licenses/BSD-3-Clause * ****************************************************************************** - */ + */ +// Portions of this file were generated with AI assistance. + /* Define to prevent recursive inclusion -------------------------------------*/ #ifndef __STM32F4xx_HAL_CONF_H @@ -64,7 +67,7 @@ #define HAL_PWR_MODULE_ENABLED /* #define HAL_QSPI_MODULE_ENABLED */ #define HAL_RCC_MODULE_ENABLED -/* #define HAL_RNG_MODULE_ENABLED */ +#define HAL_RNG_MODULE_ENABLED /* #define HAL_RTC_MODULE_ENABLED */ /* #define HAL_SAI_MODULE_ENABLED */ /* #define HAL_SD_MODULE_ENABLED */ diff --git a/MXChip/AZ3166/scripts/publish_uprotocol_powertrain.py b/MXChip/AZ3166/scripts/publish_uprotocol_powertrain.py new file mode 100644 index 00000000..ecc9e704 --- /dev/null +++ b/MXChip/AZ3166/scripts/publish_uprotocol_powertrain.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 Eclipse ThreadX contributors +# This program and the accompanying materials are made available under +# the terms of the MIT License: https://opensource.org/licenses/MIT. +# AI Disclosure: This file was largely AI-generated by Codex (GPT-6). +# The AI-generated portions may be considered public domain (CC0-1.0). +# Human review and verification are required before submission. +# SPDX-License-Identifier: MIT and CC0-1.0 +"""Publish a uProtocol powertrain event using the existing mosquitto_pub tool.""" +import argparse +import os +import shlex +import subprocess +import time +import uuid +import json + + +def publish_command(host, port, mode, authority="powertrain", entity=0xBC, version=1, resource=0x8001): + """Encode the pinned MQTT mapping; JSON remains the application payload.""" + if not authority or any(c not in "abcdefghijklmnopqrstuvwxyz0123456789-._~" for c in authority): + raise ValueError("authority must be a lowercase URI name") + if not 0 <= entity <= 0xFFFFFFFF or (entity & 0xFFFF) == 0xFFFF or entity >> 16 == 0xFFFF: + raise ValueError("entity must be a concrete 32-bit uEntity ID") + if not 0 <= version < 255 or not 0x8000 <= resource < 0xFFFF: + raise ValueError("version/resource must identify a concrete publish resource") + milliseconds = time.time_ns() // 1_000_000 + identifier = bytearray(milliseconds.to_bytes(6, "big") + os.urandom(10)) + identifier[6] = (identifier[6] & 0x0F) | 0x70 + identifier[8] = (identifier[8] & 0x3F) | 0x80 + source = f"up://{authority}/{entity:X}/{version:X}/{resource:X}" + topic = f"{authority}/{entity & 0xFFFF:X}/{entity >> 16:X}/{version:X}/{resource:X}" + command = ["mosquitto_pub", "-V", "mqttv5", "-h", host, "-p", str(port), + "-q", "1", "-t", topic, "-m", json.dumps({"Mode": mode})] + for key, value in [("uP", "1"), ("1", str(uuid.UUID(bytes=bytes(identifier)))), + ("2", "up-pub.v1"), ("3", source), ("5", "CS1")]: + command.extend(["-D", "PUBLISH", "user-property", key, value]) + command.extend(["-D", "PUBLISH", "content-type", "3", + "-D", "PUBLISH", "message-expiry-interval", "10"]) + return command + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--host", required=True) + parser.add_argument("--port", type=int, default=1883) + parser.add_argument("--mode", required=True, help="Value displayed as the JSON Mode string") + parser.add_argument("--authority", default="powertrain") + parser.add_argument("--entity", type=lambda value: int(value, 16), default=0xBC) + parser.add_argument("--version", type=lambda value: int(value, 16), default=1) + parser.add_argument("--resource", type=lambda value: int(value, 16), default=0x8001) + parser.add_argument("--dry-run", action="store_true", help="Print the command without publishing") + args = parser.parse_args() + if not 1 <= args.port <= 65535: + parser.error("port must be between 1 and 65535") + try: + command = publish_command(args.host, args.port, args.mode, args.authority, + args.entity, args.version, args.resource) + except ValueError as error: + parser.error(str(error)) + if args.dry_run: + print(shlex.join(command)) + return 0 + return subprocess.run(command, check=False).returncode + + +if __name__ == "__main__": + raise SystemExit(main())