Feature/create object initial value backup (#1223)
* Added example Device object and property value backup using create-object list-of-initial-values and writable properties. Initiated using ReinitializeDevice with STARTBACKUP command. Added backup and restore required properties to the Device object. * Fixed fwrite parameters in bacfile_posix_write_stream_data to return the correct number of bytes written. * Added Device Management-Backup and Restore-B (DM-BR-B) conversion to Wireshark PCAP format command line utility in apps/dmbrcap which decodes and provides a view of the binary backup file.
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
#Makefile to build BACnet Application using GCC compiler
|
||||
|
||||
# Executable file name
|
||||
TARGET = dmbrcap
|
||||
# BACnet objects that are used with this app
|
||||
BACNET_OBJECT_DIR = $(BACNET_SRC_DIR)/bacnet/basic/object
|
||||
SRC = main.c \
|
||||
$(BACNET_OBJECT_DIR)/client/device-client.c \
|
||||
$(BACNET_OBJECT_DIR)/netport.c
|
||||
|
||||
# TARGET_EXT is defined in apps/Makefile as .exe or nothing
|
||||
TARGET_BIN = ${TARGET}$(TARGET_EXT)
|
||||
|
||||
OBJS += ${SRC:.c=.o}
|
||||
|
||||
all: ${BACNET_LIB_TARGET} Makefile ${TARGET_BIN}
|
||||
|
||||
${TARGET_BIN}: ${OBJS} Makefile ${BACNET_LIB_TARGET}
|
||||
${CC} ${PFLAGS} ${OBJS} ${LFLAGS} -o $@
|
||||
size $@
|
||||
cp $@ ../../bin
|
||||
|
||||
${BACNET_LIB_TARGET}:
|
||||
( cd ${BACNET_LIB_DIR} ; $(MAKE) clean ; $(MAKE) -s )
|
||||
|
||||
.c.o:
|
||||
${CC} -c ${CFLAGS} $*.c -o $@
|
||||
|
||||
.PHONY: depend
|
||||
depend:
|
||||
rm -f .depend
|
||||
${CC} -MM ${CFLAGS} *.c >> .depend
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -f core ${TARGET_BIN} ${OBJS} $(TARGET).map ${BACNET_LIB_TARGET}
|
||||
|
||||
.PHONY: include
|
||||
include: .depend
|
||||
@@ -0,0 +1,4 @@
|
||||
K.5.18 BIBB - Device Management-Backup and Restore-B (DM-BR-B)
|
||||
|
||||
This tool generates a Wireshark PCAP format file from a CreateObject services
|
||||
encoded backup file.
|
||||
@@ -0,0 +1,356 @@
|
||||
/**
|
||||
* @file
|
||||
* @brief Device Management-Backup and Restore tool that generates
|
||||
* a Wireshark PCAP format file from a CreateObject services encoded file.
|
||||
* @author Steve Karg <skarg@users.sourceforge.net>
|
||||
* @date February 2026
|
||||
* @copyright SPDX-License-Identifier: GPL-2.0-or-later WITH GCC-exception-2.0
|
||||
*/
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <time.h>
|
||||
/* BACnet Stack defines - first */
|
||||
#include "bacnet/bacdef.h"
|
||||
#include "bacnet/bacint.h"
|
||||
#include "bacnet/create_object.h"
|
||||
#include "bacnet/datetime.h"
|
||||
#include "bacnet/npdu.h"
|
||||
#include "bacnet/basic/sys/mstimer.h"
|
||||
#include "bacnet/basic/sys/filename.h"
|
||||
|
||||
/* define our Data Link Type for libPCAP */
|
||||
#define DLT_CAPTURE_TYPE (1)
|
||||
|
||||
static uint8_t MTU_Buffer[1501];
|
||||
static char Capture_Filename[64] = "dmbr_20260209012345.cap";
|
||||
static FILE *Capture_File_Handle = NULL; /* stream pointer */
|
||||
static FILE *Backup_File_Handle = NULL; /* stream pointer */
|
||||
static long Backup_File_Start_Position;
|
||||
static long Backup_File_Packet_Counter;
|
||||
|
||||
/**
|
||||
* @brief Write data to the capture file.
|
||||
* @param ptr Pointer to the data to be written
|
||||
* @param size Size of each element to write
|
||||
* @param nitems Number of items to write
|
||||
* @return Number of items successfully written to the capture file
|
||||
*/
|
||||
static size_t data_write(const void *ptr, size_t size, size_t nitems)
|
||||
{
|
||||
size_t written = 0;
|
||||
|
||||
if (Capture_File_Handle) {
|
||||
written = fwrite(ptr, size, nitems, Capture_File_Handle);
|
||||
}
|
||||
|
||||
return written;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Write header data to the capture file.
|
||||
* @param ptr Pointer to the header data to be written
|
||||
* @param size Size of each element to write
|
||||
* @param nitems Number of items to write
|
||||
* @return Number of items successfully written to the capture file
|
||||
*/
|
||||
static size_t data_write_header(const void *ptr, size_t size, size_t nitems)
|
||||
{
|
||||
size_t written = 0;
|
||||
|
||||
if (Capture_File_Handle) {
|
||||
written = fwrite(ptr, size, nitems, Capture_File_Handle);
|
||||
}
|
||||
|
||||
return written;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Create a new capture filename based on the current date and time.
|
||||
*
|
||||
* Closes any existing capture file, generates a new filename using the
|
||||
* current local date and time, and opens a new capture file for writing.
|
||||
*/
|
||||
static void filename_create_new(void)
|
||||
{
|
||||
BACNET_DATE bdate;
|
||||
BACNET_TIME btime;
|
||||
char *filename = &Capture_Filename[0];
|
||||
size_t filename_size = sizeof(Capture_Filename);
|
||||
|
||||
if (Capture_File_Handle) {
|
||||
fclose(Capture_File_Handle);
|
||||
}
|
||||
Capture_File_Handle = NULL;
|
||||
datetime_local(&bdate, &btime, NULL, NULL);
|
||||
snprintf(
|
||||
filename, filename_size, "dmbr_%04d%02d%02d%02d%02d%02d.cap",
|
||||
(int)bdate.year, (int)bdate.month, (int)bdate.day, (int)btime.hour,
|
||||
(int)btime.min, (int)btime.sec);
|
||||
Capture_File_Handle = fopen(filename, "wb");
|
||||
if (Capture_File_Handle) {
|
||||
fprintf(stdout, "dmbrcap: saving capture to %s\n", filename);
|
||||
} else {
|
||||
fprintf(
|
||||
stderr, "dmbrcap: failed to open %s: %s\n", filename,
|
||||
strerror(errno));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Write the global header to the capture file in libpcap format.
|
||||
*
|
||||
* Writes the standard libpcap global header including magic number,
|
||||
* version information, timezone, timestamp accuracy, snapshot length,
|
||||
* and data link type.
|
||||
*/
|
||||
static void write_global_header(void)
|
||||
{
|
||||
uint32_t magic_number = 0xa1b2c3d4; /* magic number */
|
||||
uint16_t version_major = 2; /* major version number */
|
||||
uint16_t version_minor = 4; /* minor version number */
|
||||
int32_t thiszone = 0; /* GMT to local correction */
|
||||
uint32_t sigfigs = 0; /* accuracy of timestamps */
|
||||
uint32_t snaplen = 65535; /* max length of captured packet, in octets */
|
||||
uint32_t network = DLT_CAPTURE_TYPE; /* data link type */
|
||||
|
||||
/* create a new file. */
|
||||
(void)data_write_header(&magic_number, sizeof(magic_number), 1);
|
||||
(void)data_write_header(&version_major, sizeof(version_major), 1);
|
||||
(void)data_write_header(&version_minor, sizeof(version_minor), 1);
|
||||
(void)data_write_header(&thiszone, sizeof(thiszone), 1);
|
||||
(void)data_write_header(&sigfigs, sizeof(sigfigs), 1);
|
||||
(void)data_write_header(&snaplen, sizeof(snaplen), 1);
|
||||
(void)data_write_header(&network, sizeof(network), 1);
|
||||
fflush(Capture_File_Handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Write a packet record to the capture file in libpcap format.
|
||||
*
|
||||
* Writes a packet record with timestamp and length information followed by
|
||||
* the packet data to the capture file in libpcap format.
|
||||
* @param packet Pointer to the packet data to write
|
||||
* @param packet_len Length of the packet data in bytes
|
||||
*/
|
||||
static void write_received_packet(uint8_t *packet, size_t packet_len)
|
||||
{
|
||||
uint32_t ts_msec = 0; /* timestamp milliseconds */
|
||||
uint32_t ts_sec = 0; /* timestamp seconds */
|
||||
uint32_t ts_usec = 0; /* timestamp microseconds */
|
||||
uint32_t incl_len = 0; /* number of octets of packet saved in file */
|
||||
uint32_t orig_len = 0; /* actual length of packet */
|
||||
|
||||
ts_msec = mstimer_now();
|
||||
ts_sec = ts_msec / 1000;
|
||||
ts_usec = (ts_msec % 1000) * 1000;
|
||||
(void)data_write(&ts_sec, sizeof(ts_sec), 1);
|
||||
(void)data_write(&ts_usec, sizeof(ts_usec), 1);
|
||||
incl_len = packet_len;
|
||||
(void)data_write(&incl_len, sizeof(incl_len), 1);
|
||||
orig_len = packet_len;
|
||||
(void)data_write(&orig_len, sizeof(orig_len), 1);
|
||||
(void)data_write(packet, packet_len, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Open a backup file for reading.
|
||||
*
|
||||
* Opens an existing backup file for reading and initializes the file
|
||||
* position tracking for packet extraction.
|
||||
* @param filename Path to the backup file to open
|
||||
* @return true if the file was successfully opened, false otherwise
|
||||
*/
|
||||
static bool open_backup_file(const char *filename)
|
||||
{
|
||||
/* open existing file. */
|
||||
Backup_File_Handle = fopen(filename, "rb");
|
||||
if (Backup_File_Handle) {
|
||||
fprintf(stdout, "dmbrcap: reading backup from %s\n", filename);
|
||||
Backup_File_Start_Position = 0;
|
||||
return true;
|
||||
}
|
||||
fprintf(
|
||||
stderr, "dmbrcap: failed to open %s: %s\n", filename, strerror(errno));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Extract and convert the next packet from the backup file.
|
||||
*
|
||||
* Reads the next CreateObject service request from the backup file,
|
||||
* encapsulates it in BACnet NPDU and APDU headers with Ethernet framing,
|
||||
* and writes the complete packet to the capture file. Advances the file
|
||||
* position for the next packet extraction.
|
||||
* @return Length of the packet written, or 0 if no more packets are available
|
||||
*/
|
||||
static size_t backup_file_packet(void)
|
||||
{
|
||||
BACNET_NPDU_DATA npdu_data = { 0 };
|
||||
size_t len = 0, packet_len = 0, apdu_len = 0;
|
||||
int decoded_len = 0;
|
||||
uint8_t apdu[1500] = { 0 };
|
||||
|
||||
/* Ethernet SNAP Encoding*/
|
||||
/* dest MAC */
|
||||
MTU_Buffer[0] = 0xFF;
|
||||
MTU_Buffer[1] = 0xFF;
|
||||
MTU_Buffer[2] = 0xFF;
|
||||
MTU_Buffer[3] = 0xFF;
|
||||
MTU_Buffer[4] = 0xFF;
|
||||
MTU_Buffer[5] = 0xFF;
|
||||
/* source MAC */
|
||||
MTU_Buffer[6] = 0xFF;
|
||||
MTU_Buffer[7] = 0xFF;
|
||||
MTU_Buffer[8] = 0xFF;
|
||||
MTU_Buffer[9] = 0xFF;
|
||||
MTU_Buffer[10] = 0xFF;
|
||||
MTU_Buffer[11] = 0xFF;
|
||||
/* length - 12, 13 */
|
||||
/* Logical-Link Control SNAP */
|
||||
/* DSAP for SNAP */
|
||||
MTU_Buffer[14] = 0x82;
|
||||
/* SSAP for SNAP */
|
||||
MTU_Buffer[15] = 0x82;
|
||||
/* Control Field for SNAP */
|
||||
MTU_Buffer[16] = 0x03;
|
||||
/* BACnet NPDU */
|
||||
len = npdu_encode_pdu(&MTU_Buffer[17], NULL, NULL, &npdu_data);
|
||||
packet_len = 17 + len;
|
||||
/* BACnet APDU header */
|
||||
MTU_Buffer[packet_len++] = PDU_TYPE_CONFIRMED_SERVICE_REQUEST;
|
||||
MTU_Buffer[packet_len++] = encode_max_segs_max_apdu(0, MAX_APDU);
|
||||
MTU_Buffer[packet_len++] = Backup_File_Packet_Counter % 256;
|
||||
MTU_Buffer[packet_len++] = SERVICE_CONFIRMED_CREATE_OBJECT;
|
||||
/* BACnet APDU service data */
|
||||
if (Backup_File_Handle) {
|
||||
(void)fseek(Backup_File_Handle, Backup_File_Start_Position, SEEK_SET);
|
||||
apdu_len =
|
||||
fread(apdu, sizeof(uint8_t), sizeof(apdu), Backup_File_Handle);
|
||||
if (apdu_len > 0) {
|
||||
decoded_len =
|
||||
create_object_decode_service_request(apdu, apdu_len, NULL);
|
||||
if (decoded_len > 0) {
|
||||
apdu_len = decoded_len;
|
||||
Backup_File_Start_Position += decoded_len;
|
||||
if ((packet_len + apdu_len) > sizeof(MTU_Buffer)) {
|
||||
apdu_len = sizeof(MTU_Buffer) - packet_len;
|
||||
}
|
||||
memcpy(&MTU_Buffer[packet_len], apdu, apdu_len);
|
||||
packet_len += apdu_len;
|
||||
/* Ethernet length is data only - not address or length bytes */
|
||||
encode_unsigned16(&MTU_Buffer[12], packet_len - 14);
|
||||
write_received_packet(MTU_Buffer, packet_len);
|
||||
} else {
|
||||
packet_len = 0;
|
||||
}
|
||||
} else {
|
||||
packet_len = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return packet_len;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Clean up and close all open file handles.
|
||||
*
|
||||
* Flushes and closes the capture file and backup file handles, preparing
|
||||
* the program for exit. This function is registered as an exit handler.
|
||||
*/
|
||||
static void cleanup(void)
|
||||
{
|
||||
if (Capture_File_Handle) {
|
||||
fflush(Capture_File_Handle);
|
||||
fclose(Capture_File_Handle);
|
||||
}
|
||||
Capture_File_Handle = NULL;
|
||||
if (Backup_File_Handle) {
|
||||
fclose(Backup_File_Handle);
|
||||
}
|
||||
Backup_File_Handle = NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Print the command-line usage information.
|
||||
* @param filename The name of the program (typically argv[0])
|
||||
*/
|
||||
static void print_usage(const char *filename)
|
||||
{
|
||||
printf("Usage: %s <filename>", filename);
|
||||
printf(" [--version][--help]\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Print detailed help information for the program.
|
||||
* @param filename The name of the program (typically argv[0])
|
||||
*/
|
||||
static void print_help(const char *filename)
|
||||
{
|
||||
printf(
|
||||
"%s <filename>\n"
|
||||
"convert a backup file into a capture file.\n",
|
||||
filename);
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Main entry point for the dmbrcap utility.
|
||||
*
|
||||
* Processes command-line arguments, opens the backup file, initializes the
|
||||
* timer system, creates a new capture file with libpcap headers, reads all
|
||||
* packets from the backup file and converts them to libpcap format, then
|
||||
* closes all files and exits.
|
||||
* @param argc Number of command-line arguments
|
||||
* @param argv Array of command-line argument strings
|
||||
* @return 0 on success, 1 if backup file cannot be opened or is not provided
|
||||
*/
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
const char *filename = NULL;
|
||||
int argi = 0;
|
||||
|
||||
/* decode any command line parameters */
|
||||
filename = filename_remove_path(argv[0]);
|
||||
for (argi = 1; argi < argc; argi++) {
|
||||
if (strcmp(argv[argi], "--help") == 0) {
|
||||
print_usage(filename);
|
||||
print_help(filename);
|
||||
return 0;
|
||||
}
|
||||
if (strcmp(argv[argi], "--version") == 0) {
|
||||
printf("dmbrcap 1.0.0\n");
|
||||
printf("Copyright (C) 2026 by Steve Karg\n"
|
||||
"This is free software; see the source for copying "
|
||||
"conditions.\n"
|
||||
"There is NO warranty; not even for MERCHANTABILITY or\n"
|
||||
"FITNESS FOR A PARTICULAR PURPOSE.\n");
|
||||
return 0;
|
||||
}
|
||||
if (!open_backup_file(argv[argi])) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
if (!Backup_File_Handle) {
|
||||
print_usage(filename);
|
||||
return 1;
|
||||
}
|
||||
atexit(cleanup);
|
||||
mstimer_init();
|
||||
filename_create_new();
|
||||
write_global_header();
|
||||
while (backup_file_packet() > 0) {
|
||||
Backup_File_Packet_Counter++;
|
||||
}
|
||||
if (Backup_File_Packet_Counter) {
|
||||
fprintf(
|
||||
stdout, "dmbrcap: wrote %u packets\n",
|
||||
(unsigned)Backup_File_Packet_Counter);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user