296 lines
8.0 KiB
C
296 lines
8.0 KiB
C
/**
|
|
* @file modbus_server.c
|
|
* @brief Modbus RTU server implementation for the irrigation system slave node.
|
|
*
|
|
* This file implements the Modbus server logic, including register callbacks,
|
|
* watchdog handling, and dynamic reconfiguration. It interfaces with other
|
|
* libraries like valve control, ADC sensors, and firmware updates.
|
|
*/
|
|
|
|
#include <zephyr/device.h>
|
|
#include <zephyr/drivers/misc/vnd7050aj/vnd7050aj.h>
|
|
#include <zephyr/drivers/uart.h>
|
|
#include <zephyr/kernel.h>
|
|
#include <zephyr/logging/log.h>
|
|
#include <zephyr/modbus/modbus.h>
|
|
#include <zephyr/settings/settings.h>
|
|
#include <zephyr/sys/reboot.h>
|
|
#include <zephyr/usb/usb_device.h>
|
|
#include <app_version.h>
|
|
#include <lib/fwu.h>
|
|
#include <lib/modbus_registers.h>
|
|
#include <lib/modbus_server.h>
|
|
#include <lib/valve.h>
|
|
|
|
LOG_MODULE_REGISTER(modbus_server, LOG_LEVEL_INF);
|
|
|
|
static int modbus_iface;
|
|
static struct modbus_iface_param server_param = {
|
|
.mode = MODBUS_MODE_RTU,
|
|
.server = {.user_cb = NULL, .unit_id = 1},
|
|
.serial = {.baud = 19200, .parity = UART_CFG_PARITY_NONE},
|
|
};
|
|
|
|
static uint16_t watchdog_timeout_s = 0;
|
|
static struct k_timer watchdog_timer;
|
|
|
|
/**
|
|
* @brief Timer handler for the Modbus watchdog.
|
|
*
|
|
* This function is called when the watchdog timer expires, indicating a loss
|
|
* of communication with the Modbus master. It triggers a fail-safe action,
|
|
* which is to close the valve.
|
|
*
|
|
* @param timer_id Pointer to the timer instance.
|
|
*/
|
|
static void watchdog_timer_handler(struct k_timer *timer_id)
|
|
{
|
|
LOG_WRN("Modbus watchdog expired! Closing valve as a fail-safe.");
|
|
valve_close();
|
|
}
|
|
|
|
/**
|
|
* @brief Resets the Modbus watchdog timer.
|
|
*
|
|
* This function should be called upon receiving any valid Modbus request
|
|
* to prevent the watchdog from expiring.
|
|
*/
|
|
static inline void reset_watchdog(void)
|
|
{
|
|
if (watchdog_timeout_s > 0) {
|
|
k_timer_start(&watchdog_timer, K_SECONDS(watchdog_timeout_s), K_NO_WAIT);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Callback for reading Modbus holding registers.
|
|
*
|
|
* @param addr Register address.
|
|
* @param reg Pointer to store the read value.
|
|
* @return 0 on success.
|
|
*/
|
|
static int holding_reg_rd(uint16_t addr, uint16_t *reg)
|
|
{
|
|
reset_watchdog();
|
|
switch (addr) {
|
|
case REG_HOLDING_MAX_OPENING_TIME_S:
|
|
*reg = valve_get_max_open_time();
|
|
break;
|
|
case REG_HOLDING_MAX_CLOSING_TIME_S:
|
|
*reg = valve_get_max_close_time();
|
|
break;
|
|
case REG_HOLDING_WATCHDOG_TIMEOUT_S:
|
|
*reg = watchdog_timeout_s;
|
|
break;
|
|
case REG_HOLDING_END_CURRENT_THRESHOLD_OPEN_MA:
|
|
*reg = valve_get_end_current_threshold_open();
|
|
break;
|
|
case REG_HOLDING_END_CURRENT_THRESHOLD_CLOSE_MA:
|
|
*reg = valve_get_end_current_threshold_close();
|
|
break;
|
|
case REG_HOLDING_OBSTACLE_THRESHOLD_OPEN_MA:
|
|
*reg = valve_get_obstacle_threshold_open();
|
|
break;
|
|
case REG_HOLDING_OBSTACLE_THRESHOLD_CLOSE_MA:
|
|
*reg = valve_get_obstacle_threshold_close();
|
|
break;
|
|
default:
|
|
*reg = 0;
|
|
break;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @brief Callback for writing Modbus holding registers.
|
|
*
|
|
* @param addr Register address.
|
|
* @param reg Value to write.
|
|
* @return 0 on success.
|
|
*/
|
|
static int holding_reg_wr(uint16_t addr, uint16_t reg)
|
|
{
|
|
reset_watchdog();
|
|
switch (addr) {
|
|
case REG_HOLDING_VALVE_COMMAND:
|
|
if (reg == 1) {
|
|
valve_open();
|
|
} else if (reg == 2) {
|
|
valve_close();
|
|
} else if (reg == 0) {
|
|
valve_stop();
|
|
}
|
|
break;
|
|
case REG_HOLDING_MAX_OPENING_TIME_S:
|
|
valve_set_max_open_time(reg);
|
|
break;
|
|
case REG_HOLDING_MAX_CLOSING_TIME_S:
|
|
valve_set_max_close_time(reg);
|
|
break;
|
|
case REG_HOLDING_END_CURRENT_THRESHOLD_OPEN_MA:
|
|
valve_set_end_current_threshold_open(reg);
|
|
break;
|
|
case REG_HOLDING_END_CURRENT_THRESHOLD_CLOSE_MA:
|
|
valve_set_end_current_threshold_close(reg);
|
|
break;
|
|
case REG_HOLDING_OBSTACLE_THRESHOLD_OPEN_MA:
|
|
valve_set_obstacle_threshold_open(reg);
|
|
break;
|
|
case REG_HOLDING_OBSTACLE_THRESHOLD_CLOSE_MA:
|
|
valve_set_obstacle_threshold_close(reg);
|
|
break;
|
|
case REG_HOLDING_WATCHDOG_TIMEOUT_S:
|
|
watchdog_timeout_s = reg;
|
|
if (watchdog_timeout_s > 0) {
|
|
LOG_INF("Watchdog enabled with %u s timeout.", watchdog_timeout_s);
|
|
reset_watchdog();
|
|
} else {
|
|
LOG_INF("Watchdog disabled.");
|
|
k_timer_stop(&watchdog_timer);
|
|
}
|
|
break;
|
|
case REG_HOLDING_DEVICE_RESET:
|
|
if (reg == 1) {
|
|
LOG_WRN("Modbus reset command received. Rebooting...");
|
|
sys_reboot(SYS_REBOOT_WARM);
|
|
}
|
|
break;
|
|
default:
|
|
fwu_handler(addr, reg);
|
|
break;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @brief Callback for reading Modbus input registers.
|
|
*
|
|
* @param addr Register address.
|
|
* @param reg Pointer to store the read value.
|
|
* @return 0 on success.
|
|
*/
|
|
static int input_reg_rd(uint16_t addr, uint16_t *reg)
|
|
{
|
|
reset_watchdog();
|
|
uint32_t uptime_s = k_uptime_get_32() / 1000;
|
|
switch (addr) {
|
|
case REG_INPUT_VALVE_STATE_MOVEMENT:
|
|
*reg = (valve_get_movement() << 8) | (valve_get_state() & 0xFF);
|
|
break;
|
|
case REG_INPUT_MOTOR_OPEN_CURRENT_MA:
|
|
*reg = (uint16_t)valve_get_opening_current();
|
|
break;
|
|
case REG_INPUT_MOTOR_CLOSE_CURRENT_MA:
|
|
*reg = (uint16_t)valve_get_closing_current();
|
|
break;
|
|
case REG_INPUT_UPTIME_SECONDS_LOW:
|
|
*reg = (uint16_t)(uptime_s & 0xFFFF);
|
|
break;
|
|
case REG_INPUT_UPTIME_SECONDS_HIGH:
|
|
*reg = (uint16_t)(uptime_s >> 16);
|
|
break;
|
|
case REG_INPUT_SUPPLY_VOLTAGE_MV:
|
|
*reg = (uint16_t)valve_get_vnd_voltage();
|
|
break;
|
|
case REG_INPUT_FWU_LAST_CHUNK_CRC:
|
|
*reg = fwu_get_last_chunk_crc();
|
|
break;
|
|
case REG_INPUT_FIRMWARE_VERSION_MAJOR_MINOR:
|
|
*reg = (APP_VERSION_MAJOR << 8) | APP_VERSION_MINOR;
|
|
break;
|
|
case REG_INPUT_FIRMWARE_VERSION_PATCH:
|
|
*reg = APP_PATCHLEVEL;
|
|
break;
|
|
default:
|
|
*reg = 0;
|
|
break;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static struct modbus_user_callbacks mbs_cbs = {
|
|
// Modbus server callback functions
|
|
.holding_reg_rd = holding_reg_rd,
|
|
.holding_reg_wr = holding_reg_wr,
|
|
.input_reg_rd = input_reg_rd,
|
|
};
|
|
|
|
#define MODBUS_NODE DT_COMPAT_GET_ANY_STATUS_OKAY(zephyr_modbus_serial)
|
|
|
|
int modbus_server_init(void)
|
|
{
|
|
k_timer_init(&watchdog_timer, watchdog_timer_handler, NULL);
|
|
|
|
// Load saved settings
|
|
uint32_t saved_baudrate = 19200;
|
|
uint8_t saved_unit_id = 1;
|
|
settings_load_one("modbus/baudrate", &saved_baudrate, sizeof(saved_baudrate));
|
|
settings_load_one("modbus/unit_id", &saved_unit_id, sizeof(saved_unit_id));
|
|
|
|
// Apply loaded settings
|
|
server_param.serial.baud = saved_baudrate;
|
|
server_param.server.unit_id = saved_unit_id;
|
|
|
|
const char iface_name[] = {DEVICE_DT_NAME(MODBUS_NODE)};
|
|
#if DT_NODE_HAS_COMPAT(DT_PARENT(MODBUS_NODE), zephyr_cdc_acm_uart)
|
|
const struct device *const dev = DEVICE_DT_GET(DT_PARENT(MODBUS_NODE));
|
|
uint32_t dtr = 0;
|
|
|
|
if (!device_is_ready(dev) || usb_enable(NULL)) {
|
|
return 0;
|
|
}
|
|
|
|
while (!dtr) {
|
|
uart_line_ctrl_get(dev, UART_LINE_CTRL_DTR, &dtr);
|
|
k_sleep(K_MSEC(100));
|
|
}
|
|
|
|
LOG_INF("Client connected to server on %s", dev->name);
|
|
#endif
|
|
modbus_iface = modbus_iface_get_by_name(iface_name);
|
|
if (modbus_iface < 0) {
|
|
return modbus_iface;
|
|
}
|
|
server_param.server.user_cb = &mbs_cbs;
|
|
|
|
LOG_INF("Starting Modbus server: baudrate=%u, unit_id=%u", saved_baudrate, saved_unit_id);
|
|
return modbus_init_server(modbus_iface, server_param);
|
|
}
|
|
|
|
int modbus_reconfigure(uint32_t baudrate, uint8_t unit_id)
|
|
{
|
|
// Update parameters
|
|
server_param.serial.baud = baudrate;
|
|
server_param.server.unit_id = unit_id;
|
|
|
|
// Try to reinitialize - this should work for most cases
|
|
int ret = modbus_init_server(modbus_iface, server_param);
|
|
|
|
if (ret == 0) {
|
|
settings_save_one("modbus/baudrate", &baudrate, sizeof(baudrate));
|
|
settings_save_one("modbus/unit_id", &unit_id, sizeof(unit_id));
|
|
LOG_INF("Modbus reconfigured: baudrate=%u, unit_id=%u", baudrate, unit_id);
|
|
} else {
|
|
LOG_ERR("Failed to reconfigure Modbus: %d", ret);
|
|
LOG_INF("Modbus reconfiguration requires restart to take effect");
|
|
|
|
// Save settings for next boot
|
|
settings_save_one("modbus/baudrate", &baudrate, sizeof(baudrate));
|
|
settings_save_one("modbus/unit_id", &unit_id, sizeof(unit_id));
|
|
|
|
LOG_INF("Settings saved. Type 'reset' to restart the device and apply the "
|
|
"change.");
|
|
return 0; // Return success since settings are saved
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
uint32_t modbus_get_baudrate(void)
|
|
{
|
|
return server_param.serial.baud;
|
|
}
|
|
uint8_t modbus_get_unit_id(void)
|
|
{
|
|
return server_param.server.unit_id;
|
|
} |