333 lines
9.1 KiB
C++
333 lines
9.1 KiB
C++
/**
|
|
* @file
|
|
* @brief BACnet router entry point bridging BACnet/IP and BACnet MS/TP
|
|
* @author Kato Gangstad
|
|
*/
|
|
|
|
#include <Arduino.h>
|
|
#include <cstring>
|
|
#include <M5StamPLC.h>
|
|
#include <WiFi.h>
|
|
|
|
extern "C" {
|
|
#include "bacnet_app.h"
|
|
#include "bip.h"
|
|
#include "dlenv.h"
|
|
#include "bacnet/bacdcode.h"
|
|
#include "bacnet/npdu.h"
|
|
#include "bacnet/apdu.h"
|
|
#include "bacnet/datalink/dlmstp.h"
|
|
#include "bacnet/basic/npdu/h_npdu.h"
|
|
#include "bacnet/basic/tsm/tsm.h"
|
|
}
|
|
|
|
#ifndef WIFI_SSID
|
|
#define WIFI_SSID ""
|
|
#endif
|
|
|
|
#ifndef WIFI_PASS
|
|
#define WIFI_PASS ""
|
|
#endif
|
|
|
|
#ifndef BACNET_IP_PORT
|
|
#define BACNET_IP_PORT 47808
|
|
#endif
|
|
|
|
#ifndef ROUTER_BIP_NET
|
|
#define ROUTER_BIP_NET 1
|
|
#endif
|
|
|
|
#ifndef ROUTER_MSTP_NET
|
|
#define ROUTER_MSTP_NET 200
|
|
#endif
|
|
|
|
#ifndef ROUTER_MSTP_MAC
|
|
#define ROUTER_MSTP_MAC 2
|
|
#endif
|
|
|
|
#ifndef PLC_INPUT_ACTIVE_LOW
|
|
#define PLC_INPUT_ACTIVE_LOW 0
|
|
#endif
|
|
|
|
#ifndef GATEWAY_MAX
|
|
#define GATEWAY_MAX(a, b) (((a) > (b)) ? (a) : (b))
|
|
#endif
|
|
|
|
static uint8_t BipRxBuffer[BIP_MPDU_MAX];
|
|
static uint8_t MstpRxBuffer[DLMSTP_MPDU_MAX];
|
|
static uint8_t TxBuffer[GATEWAY_MAX(BIP_MPDU_MAX, DLMSTP_MPDU_MAX)];
|
|
|
|
/**
|
|
* @brief Connect the BACnet/IP side of the router to WiFi
|
|
*/
|
|
static void wifi_connect(void)
|
|
{
|
|
if (strlen(WIFI_SSID) == 0) {
|
|
Serial.println("[WIFI] SSID not configured; skipping WiFi connect");
|
|
return;
|
|
}
|
|
|
|
Serial.printf("[WIFI] Connecting to SSID: %s\n", WIFI_SSID);
|
|
WiFi.mode(WIFI_STA);
|
|
WiFi.begin(WIFI_SSID, WIFI_PASS);
|
|
|
|
unsigned long start = millis();
|
|
while ((WiFi.status() != WL_CONNECTED) && ((millis() - start) < 15000UL)) {
|
|
delay(200);
|
|
Serial.print('.');
|
|
}
|
|
Serial.println();
|
|
|
|
if (WiFi.status() == WL_CONNECTED) {
|
|
Serial.printf(
|
|
"[WIFI] Connected. IP: %s\n", WiFi.localIP().toString().c_str());
|
|
} else {
|
|
Serial.printf(
|
|
"[WIFI] Connect timeout. status=%d\n", (int)WiFi.status());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Send a BACnet router network message on the MS/TP segment
|
|
* @param type network layer message type
|
|
* @param dnet destination network number, or negative when omitted
|
|
* @return number of bytes sent, or a negative value on error
|
|
*/
|
|
static int
|
|
send_router_message_on_mstp(BACNET_NETWORK_MESSAGE_TYPE type, int dnet)
|
|
{
|
|
BACNET_NPDU_DATA npdu_data = { 0 };
|
|
BACNET_ADDRESS dest = { 0 };
|
|
int pdu_len = 0;
|
|
|
|
dest.mac_len = 0;
|
|
dest.net = BACNET_BROADCAST_NETWORK;
|
|
dest.len = 0;
|
|
|
|
npdu_encode_npdu_network(&npdu_data, type, false, MESSAGE_PRIORITY_NORMAL);
|
|
pdu_len = npdu_encode_pdu(TxBuffer, &dest, NULL, &npdu_data);
|
|
if (dnet >= 0) {
|
|
pdu_len += encode_unsigned16(&TxBuffer[pdu_len], (uint16_t)dnet);
|
|
}
|
|
|
|
return dlmstp_send_pdu(&dest, &npdu_data, TxBuffer, (unsigned)pdu_len);
|
|
}
|
|
|
|
/**
|
|
* @brief Send a BACnet router network message on the BACnet/IP segment
|
|
* @param type network layer message type
|
|
* @param dnet destination network number, or negative when omitted
|
|
* @return number of bytes sent, or a negative value on error
|
|
*/
|
|
static int
|
|
send_router_message_on_bip(BACNET_NETWORK_MESSAGE_TYPE type, int dnet)
|
|
{
|
|
BACNET_NPDU_DATA npdu_data = { 0 };
|
|
BACNET_ADDRESS dest = { 0 };
|
|
int pdu_len = 0;
|
|
|
|
bip_get_broadcast_address(&dest);
|
|
|
|
npdu_encode_npdu_network(&npdu_data, type, false, MESSAGE_PRIORITY_NORMAL);
|
|
pdu_len = npdu_encode_pdu(TxBuffer, &dest, NULL, &npdu_data);
|
|
if (dnet >= 0) {
|
|
pdu_len += encode_unsigned16(&TxBuffer[pdu_len], (uint16_t)dnet);
|
|
}
|
|
|
|
return bip_send_pdu(&dest, &npdu_data, TxBuffer, (unsigned)pdu_len);
|
|
}
|
|
|
|
/**
|
|
* @brief Check whether a received NPDU should be processed by the local device
|
|
* @param dest decoded destination address
|
|
* @return true if the packet targets the local device or broadcast
|
|
*/
|
|
static bool should_process_locally(const BACNET_ADDRESS *dest)
|
|
{
|
|
return (dest->net == 0U) || (dest->net == BACNET_BROADCAST_NETWORK);
|
|
}
|
|
|
|
/**
|
|
* @brief Route an NPDU between BACnet/IP and MS/TP segments
|
|
* @param from_bip true when the frame arrived from BACnet/IP
|
|
* @param src decoded source address
|
|
* @param pdu NPDU buffer to route
|
|
* @param pdu_len NPDU length in bytes
|
|
*/
|
|
static void
|
|
forward_pdu(bool from_bip, BACNET_ADDRESS *src, uint8_t *pdu, uint16_t pdu_len)
|
|
{
|
|
BACNET_ADDRESS dest = { 0 };
|
|
BACNET_ADDRESS routed_src = { 0 };
|
|
BACNET_NPDU_DATA npdu_data = { 0 };
|
|
int apdu_offset = bacnet_npdu_decode(pdu, pdu_len, &dest, src, &npdu_data);
|
|
if (apdu_offset <= 0) {
|
|
return;
|
|
}
|
|
|
|
const uint16_t src_net = from_bip ? ROUTER_BIP_NET : ROUTER_MSTP_NET;
|
|
const uint16_t dst_net = from_bip ? ROUTER_MSTP_NET : ROUTER_BIP_NET;
|
|
|
|
if (npdu_data.network_layer_message) {
|
|
if (npdu_data.network_message_type ==
|
|
NETWORK_MESSAGE_WHO_IS_ROUTER_TO_NETWORK) {
|
|
uint16_t requested = 0;
|
|
bool has_dnet = (pdu_len - apdu_offset) >= 2;
|
|
if (has_dnet) {
|
|
(void)decode_unsigned16(&pdu[apdu_offset], &requested);
|
|
}
|
|
if (!has_dnet || (requested == dst_net)) {
|
|
if (from_bip) {
|
|
send_router_message_on_bip(
|
|
NETWORK_MESSAGE_I_AM_ROUTER_TO_NETWORK, dst_net);
|
|
} else {
|
|
send_router_message_on_mstp(
|
|
NETWORK_MESSAGE_I_AM_ROUTER_TO_NETWORK, dst_net);
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if ((dest.net != 0U) && (dest.net != BACNET_BROADCAST_NETWORK) &&
|
|
(dest.net != dst_net)) {
|
|
return;
|
|
}
|
|
|
|
if (npdu_data.hop_count > 0) {
|
|
npdu_data.hop_count--;
|
|
if (npdu_data.hop_count == 0) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (src->net != 0U) {
|
|
routed_src = *src;
|
|
} else {
|
|
memset(&routed_src, 0, sizeof(routed_src));
|
|
routed_src.net = src_net;
|
|
routed_src.len = src->mac_len;
|
|
for (uint8_t i = 0; i < src->mac_len && i < MAX_MAC_LEN; i++) {
|
|
routed_src.adr[i] = src->mac[i];
|
|
}
|
|
}
|
|
|
|
BACNET_ADDRESS out_dest = { 0 };
|
|
if (dest.net == BACNET_BROADCAST_NETWORK ||
|
|
((dest.net == dst_net) && (dest.len == 0))) {
|
|
out_dest.mac_len = 0;
|
|
out_dest.net = BACNET_BROADCAST_NETWORK;
|
|
out_dest.len = 0;
|
|
} else if ((dest.net == dst_net) && (dest.len > 0)) {
|
|
out_dest.mac_len = dest.len;
|
|
for (uint8_t i = 0; i < dest.len && i < MAX_MAC_LEN; i++) {
|
|
out_dest.mac[i] = dest.adr[i];
|
|
}
|
|
out_dest.net = 0;
|
|
out_dest.len = 0;
|
|
} else {
|
|
out_dest = dest;
|
|
}
|
|
|
|
int out_len = npdu_encode_pdu(TxBuffer, &out_dest, &routed_src, &npdu_data);
|
|
if (out_len <= 0) {
|
|
return;
|
|
}
|
|
|
|
const uint16_t apdu_len = (uint16_t)(pdu_len - apdu_offset);
|
|
if ((out_len + (int)apdu_len) > (int)sizeof(TxBuffer)) {
|
|
return;
|
|
}
|
|
memcpy(&TxBuffer[out_len], &pdu[apdu_offset], apdu_len);
|
|
out_len += apdu_len;
|
|
|
|
if (from_bip) {
|
|
(void)dlmstp_send_pdu(
|
|
&out_dest, &npdu_data, TxBuffer, (unsigned)out_len);
|
|
} else {
|
|
(void)bip_send_pdu(&out_dest, &npdu_data, TxBuffer, (unsigned)out_len);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Initialize the BACnet router and local device services
|
|
*/
|
|
void setup(void)
|
|
{
|
|
Serial.begin(115200);
|
|
delay(100);
|
|
Serial.println("[BOOT] Mode: BACnet Gateway (Router + Local Device)");
|
|
Serial.printf(
|
|
"[GATEWAY] BIP NET=%u MSTP NET=%u MSTP MAC=%u\n",
|
|
(unsigned)ROUTER_BIP_NET, (unsigned)ROUTER_MSTP_NET,
|
|
(unsigned)ROUTER_MSTP_MAC);
|
|
|
|
M5StamPLC.begin();
|
|
wifi_connect();
|
|
|
|
/* Local device + objects run on B/IP side (same logic as existing M5 app).
|
|
*/
|
|
bacnet_app_init();
|
|
|
|
if (!m5_dlenv_init((uint8_t)ROUTER_MSTP_MAC)) {
|
|
Serial.println("[GATEWAY] ERROR: MSTP init failed");
|
|
for (;;) {
|
|
delay(1000);
|
|
}
|
|
}
|
|
|
|
(void)send_router_message_on_bip(
|
|
NETWORK_MESSAGE_I_AM_ROUTER_TO_NETWORK, ROUTER_MSTP_NET);
|
|
(void)send_router_message_on_mstp(
|
|
NETWORK_MESSAGE_I_AM_ROUTER_TO_NETWORK, ROUTER_BIP_NET);
|
|
|
|
Serial.println("[GATEWAY] Ready");
|
|
}
|
|
|
|
/**
|
|
* @brief Run the BACnet router main loop
|
|
*/
|
|
void loop(void)
|
|
{
|
|
BACNET_ADDRESS src = { 0 };
|
|
BACNET_ADDRESS dest = { 0 };
|
|
BACNET_NPDU_DATA npdu_data = { 0 };
|
|
uint16_t pdu_len = 0;
|
|
|
|
M5StamPLC.update();
|
|
|
|
for (uint8_t i = 0; i < bacnet_app_input_count(); i++) {
|
|
bool state = M5StamPLC.readPlcInput(i);
|
|
#if PLC_INPUT_ACTIVE_LOW
|
|
state = !state;
|
|
#endif
|
|
bacnet_app_input_set(i, state);
|
|
}
|
|
|
|
for (uint8_t i = 0; i < bacnet_app_relay_count(); i++) {
|
|
M5StamPLC.writePlcRelay(i, bacnet_app_relay_get(i));
|
|
}
|
|
|
|
bacnet_app_temperature_set(M5StamPLC.getTemp());
|
|
bacnet_app_free_heap_kb_set((float)ESP.getFreeHeap() / 1024.0f);
|
|
|
|
pdu_len = bip_receive(&src, BipRxBuffer, sizeof(BipRxBuffer), 0);
|
|
if (pdu_len > 0) {
|
|
int apdu_offset =
|
|
bacnet_npdu_decode(BipRxBuffer, pdu_len, &dest, &src, &npdu_data);
|
|
if ((apdu_offset > 0) && should_process_locally(&dest)) {
|
|
npdu_handler(&src, BipRxBuffer, pdu_len);
|
|
}
|
|
forward_pdu(true, &src, BipRxBuffer, pdu_len);
|
|
}
|
|
|
|
pdu_len = dlmstp_receive(&src, MstpRxBuffer, sizeof(MstpRxBuffer), 0);
|
|
if (pdu_len > 0) {
|
|
forward_pdu(false, &src, MstpRxBuffer, pdu_len);
|
|
}
|
|
|
|
tsm_timer_milliseconds(1);
|
|
|
|
delay(1);
|
|
}
|