Initial commit

This commit is contained in:
osiu97 2026-01-05 11:17:19 +01:00
commit 2212913cd0
12 changed files with 1205 additions and 0 deletions

48
.gitignore vendored Normal file
View File

@ -0,0 +1,48 @@
# Build output (in-repo)
build/
# Other common build dirs
cmake-build-*/
out/
dist/
# CMake/Ninja generated files (in case you build in-tree elsewhere)
CMakeCache.txt
CMakeFiles/
cmake_install.cmake
install_manifest.txt
compile_commands.json
*.ninja
.ninja_deps
.ninja_log
# Pico SDK / embedded build artifacts
*.elf
*.uf2
*.bin
*.hex
*.map
*.dis
*.lst
*.sym
# Object/dependency archives
*.o
*.a
*.d
*.su
# Logs and crash dumps
*.log
core
core.*
# IDE/editor
.vscode/
.idea/
*.code-workspace
*.swp
*.swo
*~
.DS_Store
Thumbs.db

68
CMakeLists.txt Normal file
View File

@ -0,0 +1,68 @@
# Generated Cmake Pico project file
cmake_minimum_required(VERSION 3.13)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# Initialise pico_sdk from installed location
# (note this can come from environment, CMake cache etc)
# == DO NOT EDIT THE FOLLOWING LINES for the Raspberry Pi Pico VS Code Extension to work ==
if(WIN32)
set(USERHOME $ENV{USERPROFILE})
else()
set(USERHOME $ENV{HOME})
endif()
set(sdkVersion 2.2.0)
set(toolchainVersion 14_2_Rel1)
set(picotoolVersion 2.2.0-a4)
set(picoVscode ${USERHOME}/.pico-sdk/cmake/pico-vscode.cmake)
if (EXISTS ${picoVscode})
include(${picoVscode})
endif()
# ====================================================================================
set(PICO_BOARD pico_w CACHE STRING "Board type")
# Pull in Raspberry Pi Pico SDK (must be before project)
include(pico_sdk_import.cmake)
project(pico-usb-host-modbus-hopi C CXX ASM)
# Initialise the Raspberry Pi Pico SDK
pico_sdk_init()
# Add executable. Default name is the project name, version 0.1
add_executable(pico-usb-host-modbus-hopi
pico-usb-host-modbus-hopi.c
modbus_rtu.c
)
pico_set_program_name(pico-usb-host-modbus-hopi "pico-usb-host-modbus-hopi")
pico_set_program_version(pico-usb-host-modbus-hopi "0.1")
# Modify the below lines to enable/disable output over UART/USB
pico_enable_stdio_uart(pico-usb-host-modbus-hopi 1)
pico_enable_stdio_usb(pico-usb-host-modbus-hopi 0)
# Add the standard library to the build
target_link_libraries(pico-usb-host-modbus-hopi
pico_stdlib
pico_stdio_uart
hardware_uart
tinyusb_host
tinyusb_board
pico_cyw43_arch_lwip_threadsafe_background
pico_lwip_mqtt)
# Add the standard include files to the build
target_include_directories(pico-usb-host-modbus-hopi PRIVATE
${CMAKE_CURRENT_LIST_DIR}
)
pico_add_extra_outputs(pico-usb-host-modbus-hopi)

5
README.md Normal file
View File

@ -0,0 +1,5 @@
## TODO
- NTP
- HTTP with data + timestamp
- Push to MQTT
- Log to file?

64
lwiopts.h Normal file
View File

@ -0,0 +1,64 @@
#ifndef LWIPOPTS_H
#define LWIPOPTS_H
#include <stdint.h>
// Minimal lwIP configuration for Pico W (CYW43) with DHCP.
// This is intentionally small: we only need basic IPv4 + DHCP to get an address.
// No RTOS
#define NO_SYS 1
// Memory options
#define MEM_LIBC_MALLOC 0
#define MEM_ALIGNMENT 4
#define MEM_SIZE (16 * 1024)
// Pbuf options
#define PBUF_POOL_SIZE 24
#define PBUF_POOL_BUFSIZE 1700
// Link layer
#define LWIP_ARP 1
#define LWIP_ETHERNET 1
// IP options
#define LWIP_IPV4 1
#define LWIP_IPV6 0
#define IP_REASSEMBLY 0
#define IP_FRAG 0
// DHCP (client)
#define LWIP_DHCP 1
// Increase timeout slots (DHCP + DNS + TCP + MQTT can consume several; retries can amplify this).
#define MEMP_NUM_SYS_TIMEOUT 24
// Optional helpers (kept off unless needed)
#define LWIP_DNS 1
#define LWIP_UDP 1
#define LWIP_TCP 1
// MQTT needs an output ring buffer large enough for topic+payload.
// Default (256) is too small for our JSON payload and results in ERR_MEM (-1).
#define MQTT_OUTPUT_RINGBUF_SIZE 1024
// Sockets / netconn APIs not used
#define LWIP_SOCKET 0
#define LWIP_NETCONN 0
// Checksums
#define CHECKSUM_GEN_IP 1
#define CHECKSUM_GEN_UDP 1
#define CHECKSUM_GEN_TCP 1
#define CHECKSUM_CHECK_IP 1
#define CHECKSUM_CHECK_UDP 1
#define CHECKSUM_CHECK_TCP 1
// Stats disabled
#define LWIP_STATS 0
// Debug disabled
#define LWIP_DEBUG 0
#endif // LWIPOPTS_H

5
lwipopts.h Normal file
View File

@ -0,0 +1,5 @@
#pragma once
// lwIP expects this exact filename: lwipopts.h
// Keep the actual configuration in lwiopts.h (existing file in this repo).
#include "lwiopts.h"

144
modbus_rtu.c Normal file
View File

@ -0,0 +1,144 @@
#include "modbus_rtu.h"
#include <string.h>
// Standard Modbus RTU CRC16 (poly 0xA001)
uint16_t modbus_rtu_crc16(const uint8_t *data, size_t len) {
uint16_t crc = 0xFFFF;
for (size_t i = 0; i < len; i++) {
crc ^= data[i];
for (int j = 0; j < 8; j++) {
if (crc & 1) {
crc = (uint16_t)((crc >> 1) ^ 0xA001);
} else {
crc >>= 1;
}
}
}
return crc;
}
uint16_t modbus_rtu_get_u16_be(const uint8_t *p) {
return (uint16_t)(((uint16_t)p[0] << 8) | (uint16_t)p[1]);
}
void modbus_rtu_master_init(modbus_rtu_master_t *m, modbus_rtu_master_config_t cfg) {
memset(m, 0, sizeof(*m));
m->cfg = cfg;
}
size_t modbus_rtu_build_read_holding(uint8_t *out, size_t out_cap,
uint8_t slave_id, uint16_t start_addr, uint16_t quantity,
bool disable_crc) {
// Request: [id][0x03][start hi][start lo][qty hi][qty lo][crc lo][crc hi]
const size_t base_len = 6;
const size_t crc_len = disable_crc ? 0 : 2;
if (out_cap < base_len + crc_len) return 0;
out[0] = slave_id;
out[1] = 0x03;
out[2] = (uint8_t)(start_addr >> 8);
out[3] = (uint8_t)(start_addr & 0xFF);
out[4] = (uint8_t)(quantity >> 8);
out[5] = (uint8_t)(quantity & 0xFF);
if (!disable_crc) {
uint16_t crc = modbus_rtu_crc16(out, base_len);
out[6] = (uint8_t)(crc & 0xFF);
out[7] = (uint8_t)(crc >> 8);
}
return base_len + crc_len;
}
void modbus_rtu_feed(modbus_rtu_master_t *m, const uint8_t *data, size_t len) {
// Simple append with truncation on overflow.
size_t space = sizeof(m->rx_buf) - m->rx_len;
if (len > space) len = space;
if (len == 0) return;
memcpy(&m->rx_buf[m->rx_len], data, len);
m->rx_len += len;
}
static void drop_prefix(modbus_rtu_master_t *m, size_t n) {
if (n == 0 || n > m->rx_len) return;
memmove(m->rx_buf, m->rx_buf + n, m->rx_len - n);
m->rx_len -= n;
}
bool modbus_rtu_try_parse_read_holding_response(modbus_rtu_master_t *m,
uint8_t expected_slave_id,
uint16_t expected_quantity,
size_t *out_data_offset,
uint16_t *out_word_count) {
// Response: [id][0x03][byte_count][data...][crc lo][crc hi]
// Some devices always include CRC but may compute it incorrectly.
// If m->cfg.disable_crc==true, we do NOT validate CRC, but we still accept frames that include CRC bytes.
// We also accept CRC-less frames only when m->cfg.disable_crc==true.
const size_t min_header = 3;
// Try to resync if buffer has junk: look for [id][0x03].
while (m->rx_len >= 2) {
if (m->rx_buf[0] == expected_slave_id && m->rx_buf[1] == 0x03) break;
drop_prefix(m, 1);
}
if (m->rx_len < min_header) return false;
uint8_t byte_count = m->rx_buf[2];
if ((byte_count % 2) != 0) {
// invalid, resync
drop_prefix(m, 1);
return false;
}
const uint16_t word_count = (uint16_t)(byte_count / 2);
if (word_count != expected_quantity) {
// Could be a different request/response; resync minimally.
// (You can relax this if you later add more Modbus queries.)
drop_prefix(m, 1);
return false;
}
const size_t data_len = (size_t)3 + (size_t)byte_count;
if (m->rx_len < data_len) return false;
// Prefer CRC-framed response when present.
const bool has_crc = (m->rx_len >= (data_len + 2));
const size_t total_len = has_crc ? (data_len + 2) : data_len;
if (has_crc && !m->cfg.disable_crc) {
uint16_t got_crc = (uint16_t)((uint16_t)m->rx_buf[total_len - 2] | ((uint16_t)m->rx_buf[total_len - 1] << 8));
uint16_t calc_crc = modbus_rtu_crc16(m->rx_buf, total_len - 2);
if (got_crc != calc_crc) {
drop_prefix(m, 1);
return false;
}
}
if (!has_crc && !m->cfg.disable_crc) {
// In strict mode we require CRC bytes to be present.
return false;
}
*out_data_offset = 3;
*out_word_count = word_count;
// Leave data in buffer for caller to consume; caller should drop_prefix(total_len) afterwards.
return true;
}
float modbus_float_dcba(uint16_t w1, uint16_t w2) {
// Matches your ESPHome lambdas:
// b0=low(w2), b1=high(w2), b2=low(w1), b3=high(w1)
// u = b0<<24 | b1<<16 | b2<<8 | b3
uint8_t b0 = (uint8_t)(w2 & 0xFF);
uint8_t b1 = (uint8_t)((w2 >> 8) & 0xFF);
uint8_t b2 = (uint8_t)(w1 & 0xFF);
uint8_t b3 = (uint8_t)((w1 >> 8) & 0xFF);
uint32_t u = ((uint32_t)b0 << 24) | ((uint32_t)b1 << 16) | ((uint32_t)b2 << 8) | (uint32_t)b3;
float f;
memcpy(&f, &u, sizeof(f));
return f;
}

50
modbus_rtu.h Normal file
View File

@ -0,0 +1,50 @@
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
// Simple Modbus RTU master focused on function 0x03 (Read Holding Registers).
// Transport is byte-stream oriented (USB-serial in this project).
typedef struct {
uint8_t slave_id;
bool disable_crc;
} modbus_rtu_master_config_t;
typedef struct {
modbus_rtu_master_config_t cfg;
// RX accumulation
uint8_t rx_buf[256];
size_t rx_len;
// Pending request tracking
bool awaiting_response;
uint32_t request_started_ms;
uint16_t expected_byte_count;
} modbus_rtu_master_t;
void modbus_rtu_master_init(modbus_rtu_master_t *m, modbus_rtu_master_config_t cfg);
// Builds a Read Holding Registers (0x03) request.
// Returns frame length.
size_t modbus_rtu_build_read_holding(uint8_t *out, size_t out_cap,
uint8_t slave_id, uint16_t start_addr, uint16_t quantity,
bool disable_crc);
// Feed received bytes into the Modbus parser.
void modbus_rtu_feed(modbus_rtu_master_t *m, const uint8_t *data, size_t len);
// Returns true when a full response for function 0x03 is present in rx_buf.
// If true, `*out_data_offset` points to first data byte, `*out_word_count` is number of 16-bit registers.
bool modbus_rtu_try_parse_read_holding_response(modbus_rtu_master_t *m,
uint8_t expected_slave_id,
uint16_t expected_quantity,
size_t *out_data_offset,
uint16_t *out_word_count);
uint16_t modbus_rtu_get_u16_be(const uint8_t *p);
uint16_t modbus_rtu_crc16(const uint8_t *data, size_t len);
float modbus_float_dcba(uint16_t w1, uint16_t w2);

41
mqtt_config.h Normal file
View File

@ -0,0 +1,41 @@
#pragma once
// MQTT settings (no TLS)
//
// If you use a hostname, DNS must work on your network.
// For simplest setup, use an IPv4 string like "192.168.1.10".
#ifndef MQTT_BROKER_HOST
#define MQTT_BROKER_HOST "172.29.0.179"
#endif
#ifndef MQTT_BROKER_PORT
#define MQTT_BROKER_PORT 8883
#endif
#ifndef MQTT_CLIENT_ID
#define MQTT_CLIENT_ID "pico-usb-host-modbus"
#endif
// Publish topic for readings
#ifndef MQTT_TOPIC_READINGS
#define MQTT_TOPIC_READINGS "pico-usb-host-modbus/modbus/readings"
#endif
// Optional auth (set to NULL if unused)
#ifndef MQTT_USERNAME
#define MQTT_USERNAME "ha"
#endif
#ifndef MQTT_PASSWORD
#define MQTT_PASSWORD "rNgMmtrYtLjU4rdzbibo"
#endif
// MQTT publish options
#ifndef MQTT_QOS
#define MQTT_QOS 0
#endif
#ifndef MQTT_RETAIN
#define MQTT_RETAIN 0
#endif

575
pico-usb-host-modbus-hopi.c Normal file
View File

@ -0,0 +1,575 @@
#include <stdio.h>
#include <string.h>
#include "pico/error.h"
#include "pico/cyw43_arch.h"
#include "pico/stdio_uart.h"
#include "pico/stdlib.h"
#include "tusb.h"
#include "lwip/apps/mqtt.h"
#include "lwip/dns.h"
#include "lwip/ip4_addr.h"
#include "lwip/netif.h"
#include "lwip/timeouts.h"
#include "modbus_rtu.h"
#include "wifi_config.h"
#include "mqtt_config.h"
// ---- User settings (mirror your ESPHome config) ----
#define MODBUS_SLAVE_ID 1
#define MODBUS_BAUDRATE 9600
// Device expects a correct CRC in the query, but may respond with an incorrect CRC.
// So: TX includes CRC, RX ignores CRC validation.
#define MODBUS_DISABLE_CRC 1
#define POLL_INTERVAL_MS 5000
#define RESPONSE_TIMEOUT_MS 800
// We read holding registers 0..19 (20 regs)
#define READ_START_ADDR 0
#define READ_QUANTITY 20
// We'll use UART0 for logs because RP2040 USB is used for host.
static void log_uart_init(void) {
// Note: these are GPIO numbers (GP16/GP17), not physical header pin numbers.
stdio_uart_init_full(uart0, 115200, 16, 17);
sleep_ms(50);
}
// ---- Debug helpers ----
#ifndef MODBUS_DEBUG
#define MODBUS_DEBUG 1
#endif
static void hexdump_line(const uint8_t *buf, size_t len, size_t max_len) {
const size_t n = (len < max_len) ? len : max_len;
for (size_t i = 0; i < n; i++) {
printf("%02X", buf[i]);
if (i + 1 != n) printf(" ");
}
if (len > n) printf(" ...");
}
static uint32_t ms_now(void) {
return to_ms_since_boot(get_absolute_time());
}
// ---- MQTT publisher (lwIP) ----
static mqtt_client_t *g_mqtt = NULL;
static volatile bool g_mqtt_connected = false;
static ip_addr_t g_mqtt_broker_ip;
static volatile bool g_mqtt_dns_done = false;
static volatile bool g_mqtt_dns_ok = false;
static const char *lwip_err_name(err_t err) {
switch (err) {
case ERR_OK: return "ERR_OK";
case ERR_MEM: return "ERR_MEM";
case ERR_BUF: return "ERR_BUF";
case ERR_TIMEOUT: return "ERR_TIMEOUT";
case ERR_RTE: return "ERR_RTE";
case ERR_INPROGRESS: return "ERR_INPROGRESS";
case ERR_VAL: return "ERR_VAL";
case ERR_WOULDBLOCK: return "ERR_WOULDBLOCK";
case ERR_USE: return "ERR_USE";
case ERR_ALREADY: return "ERR_ALREADY";
case ERR_ISCONN: return "ERR_ISCONN";
case ERR_CONN: return "ERR_CONN";
case ERR_IF: return "ERR_IF";
case ERR_ABRT: return "ERR_ABRT";
case ERR_RST: return "ERR_RST";
case ERR_CLSD: return "ERR_CLSD";
case ERR_ARG: return "ERR_ARG";
default: return "ERR_?";
}
}
static const char *mqtt_status_name(mqtt_connection_status_t st) {
switch (st) {
case MQTT_CONNECT_ACCEPTED: return "ACCEPTED";
case MQTT_CONNECT_REFUSED_PROTOCOL_VERSION: return "REFUSED_PROTOCOL";
case MQTT_CONNECT_REFUSED_IDENTIFIER: return "REFUSED_ID";
case MQTT_CONNECT_REFUSED_SERVER: return "REFUSED_SERVER";
case MQTT_CONNECT_REFUSED_USERNAME_PASS: return "REFUSED_USERPASS";
case MQTT_CONNECT_REFUSED_NOT_AUTHORIZED_: return "REFUSED_NOAUTH";
case MQTT_CONNECT_DISCONNECTED: return "DISCONNECTED";
case MQTT_CONNECT_TIMEOUT: return "TIMEOUT";
default: return "UNKNOWN";
}
}
static void mqtt_request_cb(void *arg, err_t err) {
(void)arg;
if (err != ERR_OK) {
printf("MQTT: publish ack err=%d (%s)\n", (int)err, lwip_err_name(err));
}
}
static void mqtt_connection_cb(mqtt_client_t *client, void *arg, mqtt_connection_status_t status) {
(void)client;
(void)arg;
g_mqtt_connected = (status == MQTT_CONNECT_ACCEPTED);
printf("MQTT: connection status=%d (%s)\n", (int)status, mqtt_status_name(status));
}
static void mqtt_dns_found_cb(const char *name, const ip_addr_t *ipaddr, void *arg) {
(void)name;
(void)arg;
if (ipaddr) {
g_mqtt_broker_ip = *ipaddr;
g_mqtt_dns_ok = true;
} else {
g_mqtt_dns_ok = false;
}
g_mqtt_dns_done = true;
}
static bool mqtt_resolve_broker(uint32_t timeout_ms) {
const char *host = MQTT_BROKER_HOST;
if (ipaddr_aton(host, &g_mqtt_broker_ip)) {
return true;
}
g_mqtt_dns_done = false;
g_mqtt_dns_ok = false;
ip_addr_set_any(IPADDR_TYPE_ANY, &g_mqtt_broker_ip);
cyw43_arch_lwip_begin();
ip_addr_t cached;
err_t derr = dns_gethostbyname(host, &cached, mqtt_dns_found_cb, NULL);
if (derr == ERR_OK) {
g_mqtt_broker_ip = cached;
g_mqtt_dns_ok = true;
g_mqtt_dns_done = true;
} else if (derr != ERR_INPROGRESS) {
g_mqtt_dns_ok = false;
g_mqtt_dns_done = true;
}
cyw43_arch_lwip_end();
const uint32_t start = ms_now();
while (!g_mqtt_dns_done && (ms_now() - start) < timeout_ms) {
cyw43_arch_lwip_begin();
sys_check_timeouts();
cyw43_arch_lwip_end();
sleep_ms(10);
}
return g_mqtt_dns_done && g_mqtt_dns_ok;
}
static void mqtt_init_and_connect(void) {
if (!g_mqtt) {
g_mqtt = mqtt_client_new();
if (!g_mqtt) {
printf("MQTT: mqtt_client_new failed\n");
return;
}
}
if (!mqtt_resolve_broker(3000)) {
printf("MQTT: DNS failed for '%s'\n", MQTT_BROKER_HOST);
return;
}
const struct mqtt_connect_client_info_t ci = {
.client_id = MQTT_CLIENT_ID,
.client_user = MQTT_USERNAME,
.client_pass = MQTT_PASSWORD,
.keep_alive = 60,
.will_topic = NULL,
.will_msg = NULL,
.will_msg_len = 0,
.will_qos = 0,
.will_retain = 0,
};
cyw43_arch_lwip_begin();
err_t err = mqtt_client_connect(g_mqtt, &g_mqtt_broker_ip, (u16_t)MQTT_BROKER_PORT,
mqtt_connection_cb, NULL, &ci);
cyw43_arch_lwip_end();
if (err != ERR_OK) {
printf("MQTT: connect err=%d\n", (int)err);
}
}
static void mqtt_publish_readings(float active_power_w,
float rms_current_a,
float voltage_v,
float frequency_hz,
float power_factor,
float annual_kwh,
float active_kwh,
float reactive_kwh,
float load_time_h,
uint16_t hours_day,
uint16_t device_addr) {
if (!g_mqtt_connected) {
// Best-effort reconnect.
mqtt_init_and_connect();
if (!g_mqtt_connected) return;
}
static char payload[320];
const uint32_t uptime_ms = ms_now();
const int n = snprintf(payload, sizeof(payload),
"{\"uptime_ms\":%lu,\"active_power_w\":%.2f,\"rms_current_a\":%.3f,\"voltage_v\":%.1f,"
"\"frequency_hz\":%.2f,\"power_factor\":%.3f,\"annual_kwh\":%.3f,\"active_kwh\":%.3f,"
"\"reactive_kwh\":%.3f,\"load_time_h\":%.2f,\"hours_day\":%u,\"device_addr\":%u}",
(unsigned long)uptime_ms,
active_power_w,
rms_current_a,
voltage_v,
frequency_hz,
power_factor,
annual_kwh,
active_kwh,
reactive_kwh,
load_time_h,
(unsigned)hours_day,
(unsigned)device_addr);
if (n <= 0 || (size_t)n >= sizeof(payload)) return;
cyw43_arch_lwip_begin();
err_t err = mqtt_publish(g_mqtt, MQTT_TOPIC_READINGS,
payload, (u16_t)n,
(u8_t)MQTT_QOS, (u8_t)MQTT_RETAIN,
mqtt_request_cb, NULL);
cyw43_arch_lwip_end();
if (err != ERR_OK) {
printf("MQTT: publish call err=%d (%s) payload_len=%d topic='%s'\n",
(int)err, lwip_err_name(err), n, MQTT_TOPIC_READINGS);
}
}
static modbus_rtu_master_t g_mb;
static uint8_t g_cdc_idx = 0xFF;
static uint32_t g_usb_rx_bytes_total = 0;
static uint32_t g_usb_rx_last_print_ms = 0;
typedef enum {
APP_WAIT_USB = 0,
APP_IDLE,
APP_WAIT_RESPONSE,
} app_state_t;
static app_state_t g_state = APP_WAIT_USB;
static uint32_t g_next_poll_ms = 0;
static uint32_t wifi_backoff_ms(uint32_t attempt_1_based) {
// attempt=1 -> base, attempt=2 -> 2x, attempt=3 -> 4x, ... clamped.
uint32_t delay = WIFI_RETRY_BASE_DELAY_MS;
if (attempt_1_based > 1) {
const uint32_t shift = attempt_1_based - 1;
if (shift >= 31) {
delay = WIFI_RETRY_MAX_DELAY_MS;
} else {
uint32_t mult = 1u << shift;
if (mult == 0) mult = 0xFFFFFFFFu;
delay = WIFI_RETRY_BASE_DELAY_MS * mult;
}
}
if (delay > WIFI_RETRY_MAX_DELAY_MS) delay = WIFI_RETRY_MAX_DELAY_MS;
// Add small jitter (0..255ms) to avoid sync collisions.
delay += (time_us_32() & 0xFFu);
return delay;
}
static bool wifi_wait_for_dhcp_ip(char *ip_buf, size_t ip_buf_len) {
absolute_time_t deadline = make_timeout_time_ms(WIFI_DHCP_TIMEOUT_MS);
while (absolute_time_diff_us(get_absolute_time(), deadline) > 0) {
bool has_ip = false;
if (ip_buf && ip_buf_len) ip_buf[0] = '\0';
cyw43_arch_lwip_begin();
struct netif *netif = &cyw43_state.netif[CYW43_ITF_STA];
const ip4_addr_t *ip = netif_ip4_addr(netif);
if (netif_is_up(netif) && ip && !ip4_addr_isany(ip)) {
if (ip_buf && ip_buf_len) {
snprintf(ip_buf, ip_buf_len, "%s", ip4addr_ntoa(ip));
}
has_ip = true;
}
cyw43_arch_lwip_end();
if (has_ip) return true;
sleep_ms(200);
}
return false;
}
static bool wifi_init_and_connect(void) {
int init_err = cyw43_arch_init_with_country(WIFI_COUNTRY);
if (init_err != 0) {
printf("WiFi init failed: %d\n", init_err);
return false;
}
cyw43_arch_enable_sta_mode();
for (uint32_t attempt = 1; attempt <= WIFI_CONNECT_RETRIES; attempt++) {
printf("WiFi: connecting to SSID '%s' (attempt %lu/%u)...\n",
WIFI_SSID, (unsigned long)attempt, (unsigned)WIFI_CONNECT_RETRIES);
int err = cyw43_arch_wifi_connect_timeout_ms(
WIFI_SSID,
WIFI_PASSWORD,
WIFI_AUTH,
WIFI_CONNECT_TIMEOUT_MS);
if (err != 0) {
if (err == PICO_ERROR_BADAUTH) {
printf("WiFi connect failed (bad auth): %d\n", err);
return false;
}
printf("WiFi connect failed: %d\n", err);
const uint32_t delay = wifi_backoff_ms(attempt);
printf("WiFi: retrying in %lu ms\n", (unsigned long)delay);
sleep_ms(delay);
continue;
}
char ip_buf[16] = {0};
if (!wifi_wait_for_dhcp_ip(ip_buf, sizeof(ip_buf))) {
printf("WiFi DHCP timeout (attempt %lu/%u)\n",
(unsigned long)attempt, (unsigned)WIFI_CONNECT_RETRIES);
const uint32_t delay = wifi_backoff_ms(attempt);
printf("WiFi: retrying in %lu ms\n", (unsigned long)delay);
sleep_ms(delay);
continue;
}
printf("WiFi up. IP: %s\n", ip_buf);
// Connect MQTT once at boot (best-effort).
mqtt_init_and_connect();
return true;
}
printf("WiFi: giving up after %u attempts\n", (unsigned)WIFI_CONNECT_RETRIES);
return false;
}
static void start_poll(void) {
uint8_t frame[16];
// Always include CRC on TX (your device requires it).
size_t n = modbus_rtu_build_read_holding(frame, sizeof(frame), MODBUS_SLAVE_ID, READ_START_ADDR, READ_QUANTITY, false);
if (n == 0) return;
#if MODBUS_DEBUG
printf("Modbus TX (%u bytes): ", (unsigned)n);
hexdump_line(frame, n, 32);
printf("\n");
#endif
// Clear any stale RX bytes
g_mb.rx_len = 0;
tuh_cdc_write(g_cdc_idx, frame, (uint32_t)n);
tuh_cdc_write_flush(g_cdc_idx);
g_mb.awaiting_response = true;
g_mb.request_started_ms = ms_now();
g_state = APP_WAIT_RESPONSE;
}
static void print_decoded(const uint16_t regs[READ_QUANTITY]) {
float active_power_w = modbus_float_dcba(regs[0], regs[1]);
float rms_current_a = modbus_float_dcba(regs[2], regs[3]);
float voltage_v = modbus_float_dcba(regs[4], regs[5]);
float frequency_hz = modbus_float_dcba(regs[6], regs[7]);
float power_factor = modbus_float_dcba(regs[8], regs[9]);
float annual_kwh = modbus_float_dcba(regs[10], regs[11]);
float active_kwh = modbus_float_dcba(regs[12], regs[13]);
float reactive_kwh = modbus_float_dcba(regs[14], regs[15]);
float load_time_h = modbus_float_dcba(regs[16], regs[17]) / 60.0f;
uint16_t hours_day = regs[18];
uint16_t device_addr = regs[19];
printf("Active Power: %.2f W\n", active_power_w);
printf("RMS Current: %.3f A\n", rms_current_a);
printf("Voltage: %.1f V\n", voltage_v);
printf("Frequency: %.2f Hz\n", frequency_hz);
printf("Power Factor: %.3f\n", power_factor);
printf("Annual Power Consumption: %.3f kWh\n", annual_kwh);
printf("Active Consumption: %.3f kWh\n", active_kwh);
printf("Reactive Consumption: %.3f kWh\n", reactive_kwh);
printf("Load Time: %.2f h\n", load_time_h);
printf("Work Hours Per Day: %u h\n", hours_day);
printf("Device Address: %u\n", device_addr);
printf("----\n");
// Publish once per refresh.
mqtt_publish_readings(active_power_w,
rms_current_a,
voltage_v,
frequency_hz,
power_factor,
annual_kwh,
active_kwh,
reactive_kwh,
load_time_h,
hours_day,
device_addr);
}
static void app_task(void) {
const uint32_t now = ms_now();
if (g_state == APP_IDLE) {
if ((int32_t)(now - g_next_poll_ms) >= 0) {
start_poll();
g_next_poll_ms = now + POLL_INTERVAL_MS;
}
return;
}
if (g_state == APP_WAIT_RESPONSE) {
// Non-blocking receive: accumulate whatever is available.
while (tuh_cdc_read_available(g_cdc_idx)) {
uint8_t tmp[64];
uint32_t n = tuh_cdc_read(g_cdc_idx, tmp, sizeof(tmp));
if (n == 0) break;
#if MODBUS_DEBUG
g_usb_rx_bytes_total += n;
// Print at most every 100ms to avoid flooding UART.
if ((now - g_usb_rx_last_print_ms) > 100) {
printf("USB RX chunk (%lu bytes), total=%lu: ", (unsigned long)n, (unsigned long)g_usb_rx_bytes_total);
hexdump_line(tmp, (size_t)n, 32);
printf("\n");
printf("Modbus RX buf len=%u: ", (unsigned)g_mb.rx_len);
hexdump_line(g_mb.rx_buf, g_mb.rx_len, 32);
printf("\n");
g_usb_rx_last_print_ms = now;
}
#endif
modbus_rtu_feed(&g_mb, tmp, (size_t)n);
}
size_t data_off = 0;
uint16_t word_count = 0;
if (modbus_rtu_try_parse_read_holding_response(&g_mb, MODBUS_SLAVE_ID, READ_QUANTITY, &data_off, &word_count)) {
#if MODBUS_DEBUG
printf("Modbus: parsed response (word_count=%u, data_off=%u, rx_len=%u)\n",
(unsigned)word_count, (unsigned)data_off, (unsigned)g_mb.rx_len);
#endif
uint16_t regs[READ_QUANTITY];
memset(regs, 0, sizeof(regs));
// Data is big-endian 16-bit registers.
const uint8_t *p = &g_mb.rx_buf[data_off];
for (uint16_t i = 0; i < word_count && i < READ_QUANTITY; i++) {
regs[i] = modbus_rtu_get_u16_be(&p[i * 2]);
}
print_decoded(regs);
// Drop the parsed frame from RX buffer
const size_t crc_len = MODBUS_DISABLE_CRC ? 0 : 2;
const size_t total_len = 3 + (size_t)(READ_QUANTITY * 2) + crc_len;
// (Reuse internal helper behavior by just resetting for single outstanding request)
g_mb.rx_len = 0;
g_state = APP_IDLE;
return;
}
if ((now - g_mb.request_started_ms) > RESPONSE_TIMEOUT_MS) {
#if MODBUS_DEBUG
const uint32_t age = now - g_mb.request_started_ms;
printf("Modbus timeout (no valid response) age=%lu ms, usb_total=%lu, rx_len=%u\n",
(unsigned long)age, (unsigned long)g_usb_rx_bytes_total, (unsigned)g_mb.rx_len);
if (g_mb.rx_len) {
printf("Modbus RX buf: ");
hexdump_line(g_mb.rx_buf, g_mb.rx_len, 64);
printf("\n");
}
printf("----\n");
#else
printf("Modbus timeout (no valid response)\n----\n");
#endif
g_mb.rx_len = 0;
g_state = APP_IDLE;
return;
}
}
}
// TinyUSB Host CDC callbacks
void tuh_cdc_mount_cb(uint8_t idx) {
g_cdc_idx = idx;
// Configure UART settings on the adapter (supported by ACM + CH34x; some other chips may vary).
cdc_line_coding_t lc = {
.bit_rate = MODBUS_BAUDRATE,
.stop_bits = CDC_LINE_CODING_STOP_BITS_1,
.parity = CDC_LINE_CODING_PARITY_NONE,
.data_bits = 8,
};
tuh_cdc_set_line_coding(idx, &lc, NULL, 0);
tuh_cdc_connect(idx, NULL, 0); // assert DTR/RTS
#if MODBUS_DEBUG
printf("USB-serial mounted (cdc idx=%u) line=%lu %u%c%u\n",
idx,
(unsigned long)lc.bit_rate,
(unsigned)lc.data_bits,
(lc.parity == CDC_LINE_CODING_PARITY_NONE) ? 'N' :
(lc.parity == CDC_LINE_CODING_PARITY_ODD) ? 'O' :
(lc.parity == CDC_LINE_CODING_PARITY_EVEN) ? 'E' : '?',
(lc.stop_bits == CDC_LINE_CODING_STOP_BITS_1) ? 1 : 2);
#else
printf("USB-serial mounted (cdc idx=%u)\n", idx);
#endif
g_state = APP_IDLE;
g_next_poll_ms = ms_now();
}
void tuh_cdc_umount_cb(uint8_t idx) {
if (g_cdc_idx == idx) g_cdc_idx = 0xFF;
printf("USB-serial unmounted (cdc idx=%u)\n", idx);
g_state = APP_WAIT_USB;
}
void tuh_cdc_rx_cb(uint8_t idx) {
// We also poll in app_task(); keep this empty to avoid duplicating logic.
(void)idx;
}
int main(void) {
log_uart_init();
printf("pico_usb_modbus_host starting...\n");
// Optional: Wi-Fi STA + DHCP (Pico W)
// If this fails, we keep running the USB host application.
(void)wifi_init_and_connect();
modbus_rtu_master_config_t cfg = {
.slave_id = MODBUS_SLAVE_ID,
.disable_crc = (MODBUS_DISABLE_CRC != 0),
};
modbus_rtu_master_init(&g_mb, cfg);
// Initialize TinyUSB host stack (RP2040 USB controller as host)
tuh_init(0);
while (true) {
tuh_task();
if (g_state != APP_WAIT_USB && g_cdc_idx != 0xFF && tuh_cdc_mounted(g_cdc_idx)) {
app_task();
}
// Keep loop responsive.
sleep_ms(1);
}
}

121
pico_sdk_import.cmake Normal file
View File

@ -0,0 +1,121 @@
# This is a copy of <PICO_SDK_PATH>/external/pico_sdk_import.cmake
# This can be dropped into an external project to help locate this SDK
# It should be include()ed prior to project()
# Copyright 2020 (c) 2020 Raspberry Pi (Trading) Ltd.
#
# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
# following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH))
set(PICO_SDK_PATH $ENV{PICO_SDK_PATH})
message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')")
endif ()
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT))
set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT})
message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')")
endif ()
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH))
set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH})
message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')")
endif ()
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_TAG} AND (NOT PICO_SDK_FETCH_FROM_GIT_TAG))
set(PICO_SDK_FETCH_FROM_GIT_TAG $ENV{PICO_SDK_FETCH_FROM_GIT_TAG})
message("Using PICO_SDK_FETCH_FROM_GIT_TAG from environment ('${PICO_SDK_FETCH_FROM_GIT_TAG}')")
endif ()
if (PICO_SDK_FETCH_FROM_GIT AND NOT PICO_SDK_FETCH_FROM_GIT_TAG)
set(PICO_SDK_FETCH_FROM_GIT_TAG "master")
message("Using master as default value for PICO_SDK_FETCH_FROM_GIT_TAG")
endif()
set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK")
set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable")
set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK")
set(PICO_SDK_FETCH_FROM_GIT_TAG "${PICO_SDK_FETCH_FROM_GIT_TAG}" CACHE FILEPATH "release tag for SDK")
if (NOT PICO_SDK_PATH)
if (PICO_SDK_FETCH_FROM_GIT)
include(FetchContent)
set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR})
if (PICO_SDK_FETCH_FROM_GIT_PATH)
get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}")
endif ()
FetchContent_Declare(
pico_sdk
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
)
if (NOT pico_sdk)
message("Downloading Raspberry Pi Pico SDK")
# GIT_SUBMODULES_RECURSE was added in 3.17
if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0")
FetchContent_Populate(
pico_sdk
QUIET
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
GIT_SUBMODULES_RECURSE FALSE
SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src
BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build
SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild
)
else ()
FetchContent_Populate(
pico_sdk
QUIET
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src
BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build
SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild
)
endif ()
set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR})
endif ()
set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE})
else ()
message(FATAL_ERROR
"SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git."
)
endif ()
endif ()
get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}")
if (NOT EXISTS ${PICO_SDK_PATH})
message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found")
endif ()
set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake)
if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE})
message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK")
endif ()
set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE)
include(${PICO_SDK_INIT_CMAKE_FILE})

45
tusb_config.h Normal file
View File

@ -0,0 +1,45 @@
#pragma once
// TinyUSB configuration for RP2040 in HOST mode.
// This targets USB-serial adapters, including WCH CH34x (CH340/CH341).
#include "tusb_option.h"
#ifdef __cplusplus
extern "C" {
#endif
//------------- COMMON -------------
#define CFG_TUSB_MCU OPT_MCU_RP2040
#define CFG_TUSB_OS OPT_OS_PICO
// Use RP2040 USB controller as host (FS)
#define CFG_TUSB_RHPORT0_MODE (OPT_MODE_HOST | OPT_MODE_FULL_SPEED)
//------------- HOST -------------
#define CFG_TUH_ENABLED 1
// Small but sufficient for CDC-serial
#define CFG_TUH_ENUMERATION_BUFSIZE 256
// We only need CDC in this project
#define CFG_TUH_HID 0
#define CFG_TUH_MSC 0
#define CFG_TUH_VENDOR 0
// Enable 1 CDC interface
#define CFG_TUH_CDC 1
// CDC buffers (host side)
#define CFG_TUH_CDC_RX_BUFSIZE 512
#define CFG_TUH_CDC_TX_BUFSIZE 512
// USB-UART chip support under TinyUSB host CDC
#define CFG_TUH_CDC_CH34X 1
#define CFG_TUH_CDC_CP210X 0
#define CFG_TUH_CDC_FTDI 0
#define CFG_TUH_CDC_PL2303 0
#ifdef __cplusplus
}
#endif

39
wifi_config.h Normal file
View File

@ -0,0 +1,39 @@
#pragma once
// Wi-Fi client configuration (Pico W / CYW43)
//
// NOTE: These are example credentials.
// Change them to match your Wi-Fi network.
#define WIFI_SSID "729_IoT"
#define WIFI_PASSWORD "DiU0w1V3z4tNscfa"
// Authentication mode for cyw43_arch_wifi_connect_timeout_ms()
// Common values:
// - CYW43_AUTH_OPEN
// - CYW43_AUTH_WPA2_AES_PSK
#define WIFI_AUTH CYW43_AUTH_WPA2_AES_PSK
// Optional regulatory domain; set this if your AP uses channels 12/13 (common in EU)
// Examples: CYW43_COUNTRY_POLAND, CYW43_COUNTRY_GERMANY, CYW43_COUNTRY_USA
#ifndef WIFI_COUNTRY
#define WIFI_COUNTRY CYW43_COUNTRY_USA
#endif
// Timeouts
#define WIFI_CONNECT_TIMEOUT_MS 30000
#define WIFI_DHCP_TIMEOUT_MS 30000
// Retry strategy
#ifndef WIFI_CONNECT_RETRIES
#define WIFI_CONNECT_RETRIES 5
#endif
// Base delay after a failed attempt; actual delay is exponential backoff
#ifndef WIFI_RETRY_BASE_DELAY_MS
#define WIFI_RETRY_BASE_DELAY_MS 500
#endif
#ifndef WIFI_RETRY_MAX_DELAY_MS
#define WIFI_RETRY_MAX_DELAY_MS 10000
#endif