Initial commit
This commit is contained in:
commit
2212913cd0
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
## TODO
|
||||||
|
- NTP
|
||||||
|
- HTTP with data + timestamp
|
||||||
|
- Push to MQTT
|
||||||
|
- Log to 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
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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})
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
Loading…
Reference in New Issue