/** * @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 #include #include #include #include #include #include #include #include #include #include #include #include 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; 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_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 = adc_sensor_get_current_open_ma(); break; case REG_INPUT_MOTOR_CLOSE_CURRENT_MA: *reg = adc_sensor_get_current_close_ma(); 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 = adc_sensor_get_voltage_mv(); 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); // Initialize ADC sensor int ret = adc_sensor_init(); if (ret < 0) { LOG_ERR("Failed to initialize ADC sensor: %d", ret); return ret; } // 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; }