/*
modbus.c - a lightweight ModBus implementation, interface wrapper
Part of grblHAL
Copyright (c) 2023-2026 Terje Io
grblHAL is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
grblHAL is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with grblHAL. If not, see .
*/
#include "modbus.h"
#include
#include "nuts_bolts.h"
#define N_MODBUS_API 2
static uint_fast16_t n_api = 0, tcp_api = N_MODBUS_API, rtu_api = N_MODBUS_API;
static modbus_api_t modbus[N_MODBUS_API] = {0};
FLASHMEM modbus_cap_t modbus_isup (void)
{
uint_fast16_t idx = n_api;
modbus_cap_t cap = {};
if(idx) do {
idx--;
if(modbus[idx].is_up()) switch(modbus[idx].interface) {
case Modbus_InterfaceRTU:
cap.rtu = On;
break;
case Modbus_InterfaceASCII:
cap.ascii = On;
break;
case Modbus_InterfaceTCP:
cap.tcp = On;
break;
}
} while(idx);
return cap;
}
bool modbus_isbusy (void)
{
bool busy = false;
uint_fast16_t idx = n_api;
if(idx) do {
idx--;
if(modbus[idx].is_busy) switch(modbus[idx].interface) {
case Modbus_InterfaceRTU:
busy = modbus[idx].is_busy();
break;
case Modbus_InterfaceASCII:
busy = modbus[idx].is_busy();
break;
case Modbus_InterfaceTCP:
busy = modbus[idx].is_busy();
break;
}
} while(idx && !busy);
return busy;
}
FLASHMEM bool modbus_enabled (void)
{
return n_api > 0;
}
FLASHMEM void modbus_flush_queue (void)
{
uint_fast16_t idx = n_api;
if(idx) do {
modbus[--idx].flush_queue();
} while(idx);
}
FLASHMEM void modbus_set_silence (const modbus_silence_timeout_t *timeout)
{
if(rtu_api != N_MODBUS_API)
modbus[rtu_api].set_silence(timeout);
}
bool modbus_send (modbus_message_t *msg, const modbus_callbacks_t *callbacks, bool block)
{
bool ok = false;
if(tcp_api != N_MODBUS_API)
ok = modbus[tcp_api].send(msg, callbacks, block);
return ok || (rtu_api != N_MODBUS_API && modbus[rtu_api].send(msg, callbacks, block));
}
// Dummy exception handler
FLASHMEM void modbus_null_exception_handler (uint8_t code, void *context)
{
// NOOP
}
FLASHMEM uint16_t modbus_read_u16 (uint8_t *p)
{
return (*p << 8) | *(p + 1);
}
FLASHMEM void modbus_write_u16 (uint8_t *p, uint16_t value)
{
*p = (uint8_t)(value >> 8);
*(p + 1) = (uint8_t)(value & 0x00FF);
}
FLASHMEM bool modbus_register_api (const modbus_api_t *api)
{
bool ok;
if((ok = n_api < N_MODBUS_API)) {
memcpy(&modbus[n_api], api, sizeof(modbus_api_t));
if(api->interface == Modbus_InterfaceTCP)
tcp_api = n_api;
else if(api->interface == Modbus_InterfaceRTU)
rtu_api = n_api;
n_api++;
}
return ok;
}
// Experimental high level API
static void rx_packet (modbus_message_t *msg);
static void rx_exception (uint8_t code, void *context);
static void rx_timeout (uint8_t code, void *context);
PROGMEM static const modbus_function_properties_t cmds[] = {
{ 0, false, false },
{ ModBus_ReadCoils, false, false },
{ ModBus_ReadDiscreteInputs, false, false },
{ ModBus_ReadHoldingRegisters, false, false },
{ ModBus_ReadInputRegisters, false, false },
{ ModBus_WriteCoil, true, true },
{ ModBus_WriteRegister, true, true },
{ ModBus_ReadExceptionStatus, false, true },
{ 0, true, false }, // ModBus_Diagnostics
{ 0, false, false },
{ 0, false, false },
{ 0, false, false },
{ 0, false, false },
{ 0, false, false },
{ 0, false, false },
{ ModBus_WriteCoils, true, false },
{ ModBus_WriteRegisters, true, false }
};
PROGMEM static const uint8_t max_function = (sizeof(cmds) / sizeof(modbus_function_properties_t)) - 1;
PROGMEM static const modbus_callbacks_t callbacks = {
.retries = 5,
.retry_delay = 100,
.on_rx_packet = rx_packet,
.on_rx_exception = rx_exception,
.on_rx_timeout = rx_timeout
};
static uint8_t tx_id;
static modbus_callback_ptr xcallback;
static modbus_response_t response;
FLASHMEM static void rx_exception (uint8_t code, void *context)
{
response.exception = code;
if(xcallback)
xcallback(&response);
}
FLASHMEM static void rx_timeout (uint8_t code, void *context)
{
response.exception = ModBus_Timeout;
if(xcallback)
xcallback(&response);
}
FLASHMEM static void rx_packet (modbus_message_t *msg)
{
if(!(msg->adu[0] & 0x80)) {
uint_fast8_t idx;
response.function = msg->adu[1];
if(response.function <= max_function && cmds[response.function].function != 0)
switch(response.function) {
case ModBus_ReadExceptionStatus:
response.num_values = 1;
response.values[0] = msg->adu[2];
break;
case ModBus_Diagnostics:
response.num_values = 0; // TBA
break;
case ModBus_ReadCoils:
case ModBus_ReadDiscreteInputs:
response.num_values = min(msg->adu[2], MODBUS_MAX_REGISTERS); // byte count
for(idx = 0; idx < response.num_values; idx++)
response.values[idx] = msg->adu[3 + idx];
break;
default:;
response.num_values = cmds[response.function].single_register || cmds[response.function].is_write ? 2 : msg->adu[2] / 2;
response.num_values = min(response.num_values, MODBUS_MAX_REGISTERS);
uint_fast8_t pos = cmds[response.function].single_register || cmds[response.function].is_write ? 2 : 3;
for(idx = 0; idx < response.num_values; idx++)
response.values[idx] = modbus_read_u16(&msg->adu[pos + (idx << 1)]);
break;
} else
response.exception = 255;
if(xcallback)
xcallback(&response);
}
}
FLASHMEM const modbus_function_properties_t *modbus_get_function_properties (modbus_function_t function)
{
const modbus_function_properties_t *details = NULL;
if(function <= max_function && cmds[function].function)
details = &cmds[function];
return details;
}
FLASHMEM status_code_t modbus_message (uint8_t server, modbus_function_t function, uint16_t address, uint16_t *values, uint8_t registers, modbus_callback_ptr callback)
{
if(function > max_function || !cmds[function].function)
return Status_InvalidStatement;
uint_fast8_t idx;
status_code_t status;
modbus_message_t cmd = {
.context = (void *)((uint32_t)tx_id++),
.crc_check = true,
.adu[0] = (uint8_t)server,
.adu[1] = (uint8_t)function,
.adu[2] = (uint8_t)(address >> 8),
.adu[3] = (uint8_t)(address & 0xFF)
};
xcallback = callback;
memset(&response, 0, sizeof(modbus_response_t));
if(function == ModBus_ReadExceptionStatus) {
cmd.tx_length = 4;
cmd.rx_length = 5;
} else {
cmd.tx_length = 6 + 2 * registers;
cmd.rx_length = cmd.tx_length - 1;
if(cmds[function].is_write) {
if(cmds[function].single_register) {
cmd.tx_length = cmd.rx_length = 8;
for(idx = 0; idx < registers; idx++) {
cmd.adu[(idx << 1) + 4] = (uint8_t)(values[idx] >> 8);
cmd.adu[(idx << 1) + 5] = (uint8_t)(values[idx] & 0xFF);
}
} else {
cmd.tx_length += 3; cmd.rx_length = 8;
cmd.adu[4] = (uint8_t)(registers >> 8);
cmd.adu[5] = (uint8_t)(registers & 0xFF);
cmd.adu[6] = (uint8_t)(registers << 1);
for(idx = 0; idx < registers; idx++) {
cmd.adu[(idx << 1) + 7] = (uint8_t)(values[idx] >> 8);
cmd.adu[(idx << 1) + 8] = (uint8_t)(values[idx] & 0xFF);
}
}
} else { // read
cmd.adu[4] = (uint8_t)(registers >> 8);
cmd.adu[5] = (uint8_t)(registers & 0xFF);
if(function == ModBus_ReadCoils || function == ModBus_ReadDiscreteInputs)
cmd.rx_length = 5 + ((registers + 7) / 8); // bit-packed, ceil(n/8) bytes
else
cmd.rx_length = 5 + (registers << 1); // 2 bytes per register
}
}
status = modbus_send(&cmd, &callbacks, true) ? Status_OK : Status_AccessDenied;
return status;
}
/**/