diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 15c8022..fd8dd41 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -134,8 +134,26 @@ jobs: - uses: actions/download-artifact@v4 with: path: artifacts + # Every platform artifact carries the same docs (README.md, LICENSE, ...). + # Uploading two assets with one name makes the release action fail part-way + # and leaves a draft release behind (a real tagged release hit this). Flatten the + # artifacts into one folder with a single copy of each file name, and stop + # if two files share a name but differ. + - name: Collect release assets (one copy of each file name) + shell: bash + run: | + mkdir -p release + find artifacts -type f | sort | while read -r f; do + name=$(basename "$f") + if [ -e "release/$name" ]; then + cmp -s "$f" "release/$name" || { echo "FAIL: two different files are both named $name ($f)"; exit 1; } + else + cp "$f" "release/$name" + fi + done + ls -l release - uses: softprops/action-gh-release@v2 with: name: ${{ github.ref_name }} generate_release_notes: true - files: artifacts/**/* + files: release/* diff --git a/AGENTS.md b/AGENTS.md index 5d12e47..4f2c126 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -74,7 +74,7 @@ Interactive keys while running: `h` help, `q` quit, up/down nudge Analog Input 1 ## Conventions -- Device is named "Rainbow"; objects use the series' colour names; vendor id 389. +- Device is named "Chipkin Example B-RTR"; objects use the series' colour names; vendor id 389. - Implement **only** the services and objects the B-RTR profile requires - but expose **every required property** of each object for Protocol_Revision 24. - Outputs are **commandable**: store the 16-slot `Priority_Array` + diff --git a/CHANGELOG.md b/CHANGELOG.md index e784569..b28b4cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.2] - unreleased + +### Changed + +- **Device renamed from the series' colour placeholder "Rainbow" to "Chipkin + Example B-RTR"** so devices from different examples in the series are + distinguishable from each other on the same BACnet network - every example + previously announced the identical Object_Name "Rainbow", which made two + examples on one subnet indistinguishable by name. Sub-object names (Analog + Input 1 "Bronze", etc.) are unchanged - only the Device object's name + changed. `docs/colour-table.md` (series root) updated to match. APP_VERSION + bumped 1.0.1 -> 1.0.2. + ## [Unreleased] ### Changed — documentation restructure, single-command build @@ -66,6 +79,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 release refreshes them under the documented SOURCE build, matching the series convention. +## [1.0.1] - unreleased + +### Fixed + +- **`Application_Software_Version` (12) and `Firmware_Revision` (44) were + hardcoded and never wired up**, despite a comment flagging the gap + ("Change both to your product's REAL firmware / application software + versions"). Fixed: `Application_Software_Version` now reads `APP_VERSION` + directly (one source of truth, can't drift from `--version`'s own banner). + `Firmware_Revision` is now built at runtime from the CAS BACnet Stack's own + `BACnetStack_GetAPIMajorVersion()`/`GetAPIMinorVersion()`/ + `GetAPIPatchVersion()`/`GetAPIBuildVersion()` (the same 4 calls + `common/CASExampleHelper.cpp`'s `PrintVersion()` already uses for the + startup banner), populated once right after `LoadBACnetFunctions()` + succeeds. Code-read verification: `Application_Software_Version` now + returns `"1.0.1"`, `Firmware_Revision` now returns the pinned stack's real + `major.minor.patch.build` (e.g. `"6.0.21.0"`) instead of the stale + hardcoded `"1.0.0"` for both. + ## [1.0.0] - unreleased ### Added diff --git a/README.md b/README.md index 69010e7..181b736 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ discoverable via **Who-Is / I-Am**. It is the series' canonical example for ## The device this example creates ``` -Device 389018 "Rainbow" (Vendor 389 - Chipkin Automation Systems) +Device 389018 "Chipkin Example B-RTR" (Vendor 389 - Chipkin Automation Systems) | +-- Analog Input 1 "Bronze" Present_Value 21.5 (REAL, degrees Celsius; read-only) +-- Binary Input 1 "Emerald" Present_Value inactive (0 = inactive / 1 = active; read-only) @@ -93,7 +93,7 @@ alarming, scheduling, or trending - not required by B-RTR. | Object type | Instance | Name | Access | |-------------|:--------:|------|--------| -| Device | 389018 | Rainbow | - | +| Device | 389018 | Chipkin Example B-RTR | - | | Analog Input | 1 | Bronze | read-only | | Binary Input | 1 | Emerald | read-only | | Multi-State Input | 1 | Hot Pink | read-only | @@ -197,7 +197,7 @@ FYI: Listening for BACnet/IP on UDP port 47809 (Network Port 2). TX 21 bytes to 192.168.3.255:47808 (broadcast) (Network Port 1) TX 21 bytes to 192.168.3.255:47809 (broadcast) (Network Port 2) FYI: I-Am-Router-To-Network broadcast on Network Port 1 (Vermilion): sent; on Network Port 2 (Vermilion 2): sent -FYI: Device 389018 ("Rainbow") ready. Vendor ID 389. Routing network 1 (Vermilion, UDP 47808) <-> network 2 (Vermilion 2, UDP 47809). Press 'h' for help. +FYI: Device 389018 ("Chipkin Example B-RTR") ready. Vendor ID 389. Routing network 1 (Vermilion, UDP 47808) <-> network 2 (Vermilion 2, UDP 47809). Press 'h' for help. ``` The device listens on UDP **47808** and **47809** by default. Allow both ports @@ -238,7 +238,7 @@ You need a BACnet client, such as the 1. **Discover on each network independently** - ReadProperty of Device `Object_Name` succeeds directly against `127.0.0.1:47808` (network 1) AND - `127.0.0.1:47809` (network 2), both returning `"Rainbow"` (DS-RP-B works on + `127.0.0.1:47809` (network 2), both returning `"Chipkin Example B-RTR"` (DS-RP-B works on both ports). 2. **Network_Number / Network_Number_Quality** - ReadProperty of Network Port 1's `Network_Number` = `1`, `Network_Number_Quality` = `configured`; Network Port diff --git a/common/CASBACnetStackExampleConstants.h b/common/CASBACnetStackExampleConstants.h index cc22c9c..6e72fb9 100644 --- a/common/CASBACnetStackExampleConstants.h +++ b/common/CASBACnetStackExampleConstants.h @@ -38,6 +38,8 @@ static const uint32_t PROPERTY_IDENTIFIER_APPLICATION_SOFTWARE_VERSION = 12; static const uint32_t PROPERTY_IDENTIFIER_APDU_LENGTH = 399; static const uint32_t PROPERTY_IDENTIFIER_DESCRIPTION = 28; static const uint32_t PROPERTY_IDENTIFIER_FIRMWARE_REVISION = 44; +static const uint32_t PROPERTY_IDENTIFIER_LOCAL_DATE = 56; +static const uint32_t PROPERTY_IDENTIFIER_LOCAL_TIME = 57; static const uint32_t PROPERTY_IDENTIFIER_LOCATION = 58; static const uint32_t PROPERTY_IDENTIFIER_MODEL_NAME = 70; static const uint32_t PROPERTY_IDENTIFIER_IP_ADDRESS = 400; @@ -45,6 +47,7 @@ static const uint32_t PROPERTY_IDENTIFIER_IP_DEFAULT_GATEWAY = 401; static const uint32_t PROPERTY_IDENTIFIER_BACNET_IP_MODE = 408; static const uint32_t PROPERTY_IDENTIFIER_IP_SUBNET_MASK = 411; static const uint32_t PROPERTY_IDENTIFIER_BACNET_IP_UDP_PORT = 412; +static const uint32_t PROPERTY_IDENTIFIER_LINK_SPEED = 420; static const uint32_t PROPERTY_IDENTIFIER_MAC_ADDRESS = 423; static const uint32_t PROPERTY_IDENTIFIER_NUMBER_OF_STATES = 74; static const uint32_t PROPERTY_IDENTIFIER_OBJECT_IDENTIFIER = 75; diff --git a/common/CASExampleHelper.cpp b/common/CASExampleHelper.cpp index f0baad3..e1da0c3 100644 --- a/common/CASExampleHelper.cpp +++ b/common/CASExampleHelper.cpp @@ -156,9 +156,10 @@ uint16_t HelperReceiveMessage(uint8_t* message, const uint16_t maxMessageLength, return 0; // no datagram waiting on any bound port } - printf("RX %u bytes from %u.%u.%u.%u:%u (Network Port %u)\n", (unsigned)bytesRead, + printf("RX %u bytes from %u.%u.%u.%u:%u (Network Port %u)", (unsigned)bytesRead, fromIp[0], fromIp[1], fromIp[2], fromIp[3], (unsigned)fromPort, (unsigned)servicedInstance); + CASExampleHelper::LogBacnetFrame(message, bytesRead); // Fill the 6-byte BACnet/IP connection string: IP[4] + port[2] (big-endian). sourceConnectionString[0] = fromIp[0]; @@ -207,9 +208,10 @@ uint16_t HelperSendMessage(const uint8_t* message, const uint16_t messageLength, const uint16_t port = (uint16_t)((connectionString[4] << 8) | connectionString[5]); const uint16_t sent = binding->udp.Send(ip, port, message, messageLength); - printf("TX %u bytes to %u.%u.%u.%u:%u%s (Network Port %u)\n", (unsigned)sent, + printf("TX %u bytes to %u.%u.%u.%u:%u%s (Network Port %u)", (unsigned)sent, ip[0], ip[1], ip[2], ip[3], (unsigned)port, broadcast ? " (broadcast)" : "", (unsigned)networkPortInstance); + CASExampleHelper::LogBacnetFrame(message, messageLength); return sent; } @@ -224,6 +226,83 @@ CASBACnetTime HelperGetSystemTime() { return (CASBACnetTime)time(0); } +// --------------------------------------------------------------------------- +// Find the primary network interface's negotiated link speed, in bits per +// second (what the Network Port object's Link_Speed property reports - a +// REAL, 0.0 meaning "indeterminable" per Clause 12.56.15). Same interface +// selection as GetPrimaryIPv4 (first non-loopback, "up" IPv4 interface) - +// deliberately not shared code with it: Windows needs a second API call +// (GetIfEntry) keyed off the adapter's index, and POSIX needs the interface +// NAME rather than its address, so the loop bodies diverge enough that +// factoring out "the one interface" isn't worth it for two ~30-line +// functions called once each, at start-up. Returns false (leaves +// *bitsPerSecond untouched) if no interface was found or its speed could not +// be read - the caller then reports 0.0 ("indeterminable"), which is honest, +// not this function silently inventing a number. +bool GetPrimaryLinkSpeedBitsPerSecond(double* bitsPerSecond) { +#if defined(_WIN32) + IP_ADAPTER_INFO adapters[32]; + ULONG len = sizeof(adapters); + if (GetAdaptersInfo(adapters, &len) != ERROR_SUCCESS) { + return false; + } + for (const IP_ADAPTER_INFO* a = adapters; a != NULL; a = a->Next) { + struct in_addr ipAddr; + if (inet_pton(AF_INET, a->IpAddressList.IpAddress.String, &ipAddr) != 1) { + continue; + } + const uint32_t ipN = ipAddr.s_addr; + if (ipN == 0 || (ipN & 0xFF) == 127) { + continue; // no address, or loopback - same exclusions as GetPrimaryIPv4 + } + MIB_IFROW ifRow; + memset(&ifRow, 0, sizeof(ifRow)); + ifRow.dwIndex = a->Index; + if (GetIfEntry(&ifRow) != NO_ERROR) { + return false; + } + *bitsPerSecond = (double)ifRow.dwSpeed; // already bits/sec on this API + return true; + } + return false; +#else + struct ifaddrs* ifap = NULL; + if (getifaddrs(&ifap) != 0) { + return false; + } + bool found = false; + for (const struct ifaddrs* ifa = ifap; ifa != NULL && !found; ifa = ifa->ifa_next) { + if (ifa->ifa_addr == NULL || ifa->ifa_addr->sa_family != AF_INET) { + continue; + } + if ((ifa->ifa_flags & IFF_LOOPBACK) || !(ifa->ifa_flags & IFF_UP)) { + continue; + } + // /sys/class/net//speed - the kernel's own view of the negotiated + // link speed in Mbit/s (an ethtool query would need root on some distros + // and a dependency this file doesn't otherwise have; + // the sysfs file is the same number, readable by anyone, no extra headers). + // Reads "-1" (or fails to open) for a link that's down or doesn't expose + // a speed (e.g. some virtual/tunnel interfaces) - either way, no value. + char path[64]; + snprintf(path, sizeof(path), "/sys/class/net/%s/speed", ifa->ifa_name); + FILE* f = fopen(path, "r"); + if (f == NULL) { + continue; + } + long mbps = -1; + const int scanned = fscanf(f, "%ld", &mbps); + fclose(f); + if (scanned == 1 && mbps > 0) { + *bitsPerSecond = (double)mbps * 1000000.0; + found = true; + } + } + freeifaddrs(ifap); + return found; +#endif +} + // --------------------------------------------------------------------------- // Find the primary IPv4 interface's address and subnet mask (each 4 octets in // network/big-endian order, i.e. ip[0] is the first dotted octet). These feed @@ -359,6 +438,1331 @@ uint64_t MonotonicMilliseconds() { } // namespace +namespace { + +// ANSI/ASHRAE 135 Clause 21 confirmed-service-choice names, index = the wire +// value. Includes the two removed-but-still-numbered choices (13, 24, 25) so +// an old/malformed frame using them still gets a name instead of falling +// through to the numeric fallback. +const char* ConfirmedServiceName(uint8_t choice) { + static const char* const names[] = { + "AcknowledgeAlarm", "ConfirmedCOVNotification", "ConfirmedEventNotification", + "GetAlarmSummary", "GetEnrollmentSummary", "SubscribeCOV", "AtomicReadFile", + "AtomicWriteFile", "AddListElement", "RemoveListElement", "CreateObject", + "DeleteObject", "ReadProperty", "ReadPropertyConditional", "ReadPropertyMultiple", + "WriteProperty", "WritePropertyMultiple", "DeviceCommunicationControl", + "ConfirmedPrivateTransfer", "ConfirmedTextMessage", "ReinitializeDevice", + "VTOpen", "VTClose", "VTData", "Authenticate", "RequestKey", "ReadRange", + "LifeSafetyOperation", "SubscribeCOVProperty", "GetEventInformation", + "SubscribeCOVPropertyMultiple", "ConfirmedCOVNotificationMultiple" + }; + if (choice < sizeof(names) / sizeof(names[0])) { + return names[choice]; + } + return NULL; +} + +// Clause 21 unconfirmed-service-choice names. +const char* UnconfirmedServiceName(uint8_t choice) { + static const char* const names[] = { + "I-Am", "I-Have", "UnconfirmedCOVNotification", "UnconfirmedEventNotification", + "UnconfirmedPrivateTransfer", "UnconfirmedTextMessage", "TimeSynchronization", + "Who-Has", "Who-Is", "UTCTimeSynchronization", "WriteGroup", + "UnconfirmedCOVNotificationMultiple" + }; + if (choice < sizeof(names) / sizeof(names[0])) { + return names[choice]; + } + return NULL; +} + +const char* RejectReasonName(uint8_t reason) { + static const char* const names[] = { + "other", "buffer-overflow", "inconsistent-parameters", + "invalid-parameter-data-type", "invalid-tag", "missing-required-parameter", + "parameter-out-of-range", "too-many-arguments", "undefined-enumeration", + "unrecognized-service" + }; + if (reason < sizeof(names) / sizeof(names[0])) { + return names[reason]; + } + return NULL; +} + +const char* AbortReasonName(uint8_t reason) { + static const char* const names[] = { + "other", "buffer-overflow", "invalid-apdu-in-this-state", + "preempted-by-higher-priority-task", "segmentation-not-supported", + "security-error", "insufficient-security", "window-size-out-of-range", + "application-exceeded-reply-time", "out-of-resources", "tsm-timeout", + "apdu-too-long" + }; + if (reason < sizeof(names) / sizeof(names[0])) { + return names[reason]; + } + return NULL; +} + +// Only the handful actually seen in this series' examples (routing discovery +// and network-number discovery); everything else falls back to "type=". +const char* NetworkMessageName(uint8_t type) { + switch (type) { + case 0: return "Who-Is-Router-To-Network"; + case 1: return "I-Am-Router-To-Network"; + case 2: return "I-Could-Be-Router-To-Network"; + case 3: return "Reject-Message-To-Network"; + case 4: return "Router-Busy-To-Network"; + case 5: return "Router-Available-To-Network"; + case 6: return "Initialize-Routing-Table"; + case 7: return "Initialize-Routing-Table-Ack"; + case 18: return "What-Is-Network-Number"; + case 19: return "Network-Number-Is"; + default: return NULL; + } +} + +// --- Object type / property identifier name tables -------------------------- +// Generated from the pinned stack's own BACnetObjectType.h / +// BACnetPropertyIdentifier.h (the authoritative source for names AND values - +// see common/CHANGELOG.md 2.7.0 for the exact generation script; do not +// hand-edit individual rows, regenerate both tables together if the stack +// pin changes). Sorted ascending by id - LookUpIdName binary-searches this, +// so a hand-edit that breaks the ordering breaks every lookup after it +// silently (no runtime check). +struct IdName { + uint32_t id; + const char* name; +}; + +const IdName OBJECT_TYPE_NAMES[] = { + { 0, "Analog_Input" }, + { 1, "Analog_Output" }, + { 2, "Analog_Value" }, + { 3, "Binary_Input" }, + { 4, "Binary_Output" }, + { 5, "Binary_Value" }, + { 6, "Calendar" }, + { 7, "Command" }, + { 8, "Device" }, + { 9, "Event_Enrollment" }, + { 10, "File" }, + { 11, "Group" }, + { 12, "Loop" }, + { 13, "Multi_State_Input" }, + { 14, "Multi_State_Output" }, + { 15, "Notification_Class" }, + { 16, "Program" }, + { 17, "Schedule" }, + { 18, "Averaging" }, + { 19, "Multi_State_Value" }, + { 20, "Trend_Log" }, + { 21, "Life_Safety_Point" }, + { 22, "Life_Safety_Zone" }, + { 23, "Accumulator" }, + { 24, "Pulse_Converter" }, + { 25, "Event_Log" }, + { 26, "Global_Group" }, + { 27, "Trend_Log_Multiple" }, + { 28, "Load_Control" }, + { 29, "Structured_View" }, + { 30, "Access_Door" }, + { 31, "Timer" }, + { 32, "Access_Credential" }, + { 33, "Access_Point" }, + { 34, "Access_Rights" }, + { 35, "Access_User" }, + { 36, "Access_Zone" }, + { 37, "Credential_Data_Input" }, + { 38, "Network_Security" }, + { 39, "Bitstring_Value" }, + { 40, "Characterstring_Value" }, + { 41, "Datepattern_Value" }, + { 42, "Date_Value" }, + { 43, "Datetimepattern_Value" }, + { 44, "Datetime_Value" }, + { 45, "Integer_Value" }, + { 46, "Large_Analog_Value" }, + { 47, "Octetstring_Value" }, + { 48, "Positive_Integer_Value" }, + { 49, "Timepattern_Value" }, + { 50, "Time_Value" }, + { 51, "Notification_Forwarder" }, + { 52, "Alert_Enrollment" }, + { 53, "Channel" }, + { 54, "Lighting_Output" }, + { 55, "Binary_Lighting_Output" }, + { 56, "Network_Port" }, + { 57, "Elevator_Group" }, + { 58, "Escalator" }, + { 59, "Lift" }, + { 60, "Staging" }, + { 61, "Audit_Log" }, + { 62, "Audit_Reporter" }, + { 63, "Color" }, + { 64, "Color_Temperature" }, +}; + +const IdName PROPERTY_NAMES[] = { + { 0, "Acked_Transitions" }, + { 1, "Ack_Required" }, + { 2, "Action" }, + { 3, "Action_Text" }, + { 4, "Active_Text" }, + { 5, "Active_Vt_Sessions" }, + { 6, "Alarm_Value" }, + { 7, "Alarm_Values" }, + { 8, "All" }, + { 9, "All_Writes_Successful" }, + { 10, "Apdu_Segment_Timeout" }, + { 11, "Apdu_Timeout" }, + { 12, "Application_Software_Version" }, + { 13, "Archive" }, + { 14, "Bias" }, + { 15, "Change_Of_State_Count" }, + { 16, "Change_Of_State_Time" }, + { 17, "Notification_Class" }, + { 19, "Controlled_Variable_Reference" }, + { 20, "Controlled_Variable_Units" }, + { 21, "Controlled_Variable_Value" }, + { 22, "Cov_Increment" }, + { 23, "Date_List" }, + { 24, "Daylight_Savings_Status" }, + { 25, "Deadband" }, + { 26, "Derivative_Constant" }, + { 27, "Derivative_Constant_Units" }, + { 28, "Description" }, + { 29, "Description_Of_Halt" }, + { 30, "Device_Address_Binding" }, + { 31, "Device_Type" }, + { 32, "Effective_Period" }, + { 33, "Elapsed_Active_Time" }, + { 34, "Error_Limit" }, + { 35, "Event_Enable" }, + { 36, "Event_State" }, + { 37, "Event_Type" }, + { 38, "Exception_Schedule" }, + { 39, "Fault_Values" }, + { 40, "Feedback_Value" }, + { 41, "File_Access_Method" }, + { 42, "File_Size" }, + { 43, "File_Type" }, + { 44, "Firmware_Revision" }, + { 45, "High_Limit" }, + { 46, "Inactive_Text" }, + { 47, "In_Process" }, + { 48, "Instance_Of" }, + { 49, "Integral_Constant" }, + { 50, "Integral_Constant_Units" }, + { 51, "Issue_Confirmed_Notifications" }, + { 52, "Limit_Enable" }, + { 53, "List_Of_Group_Members" }, + { 54, "List_Of_Object_Property_References" }, + { 56, "Local_Date" }, + { 57, "Local_Time" }, + { 58, "Location" }, + { 59, "Low_Limit" }, + { 60, "Manipulated_Variable_Reference" }, + { 61, "Maximum_Output" }, + { 62, "Max_Apdu_Length_Accepted" }, + { 63, "Max_Info_Frames" }, + { 64, "Max_Manager" }, + { 65, "Max_Pres_Value" }, + { 66, "Minimum_Off_Time" }, + { 67, "Minimum_On_Time" }, + { 68, "Minimum_Output" }, + { 69, "Min_Pres_Value" }, + { 70, "Model_Name" }, + { 71, "Modification_Date" }, + { 72, "Notify_Type" }, + { 73, "Number_Of_Apdu_Retries" }, + { 74, "Number_Of_States" }, + { 75, "Object_Identifier" }, + { 76, "Object_List" }, + { 77, "Object_Name" }, + { 78, "Object_Property_Reference" }, + { 79, "Object_Type" }, + { 80, "Optional" }, + { 81, "Out_Of_Service" }, + { 82, "Output_Units" }, + { 83, "Event_Parameters" }, + { 84, "Polarity" }, + { 85, "Present_Value" }, + { 86, "Priority" }, + { 87, "Priority_Array" }, + { 88, "Priority_For_Writing" }, + { 89, "Process_Identifier" }, + { 90, "Program_Change" }, + { 91, "Program_Location" }, + { 92, "Program_State" }, + { 93, "Proportional_Constant" }, + { 94, "Proportional_Constant_Units" }, + { 96, "Protocol_Object_Types_Supported" }, + { 97, "Protocol_Services_Supported" }, + { 98, "Protocol_Version" }, + { 99, "Read_Only" }, + { 100, "Reason_For_Halt" }, + { 102, "Recipient_List" }, + { 103, "Reliability" }, + { 104, "Relinquish_Default" }, + { 105, "Required" }, + { 106, "Resolution" }, + { 107, "Segmentation_Supported" }, + { 108, "Setpoint" }, + { 109, "Setpoint_Reference" }, + { 110, "State_Text" }, + { 111, "Status_Flags" }, + { 112, "System_Status" }, + { 113, "Time_Delay" }, + { 114, "Time_Of_Active_Time_Reset" }, + { 115, "Time_Of_State_Count_Reset" }, + { 116, "Time_Synchronization_Recipients" }, + { 117, "Units" }, + { 118, "Update_Interval" }, + { 119, "Utc_Offset" }, + { 120, "Vendor_Identifier" }, + { 121, "Vendor_Name" }, + { 122, "Vt_Classes_Supported" }, + { 123, "Weekly_Schedule" }, + { 124, "Attempted_Samples" }, + { 125, "Average_Value" }, + { 126, "Buffer_Size" }, + { 127, "Client_Cov_Increment" }, + { 128, "Cov_Resubscription_Interval" }, + { 130, "Event_Time_Stamps" }, + { 131, "Log_Buffer" }, + { 132, "Log_Device_Object_Property" }, + { 133, "Enable" }, + { 134, "Log_Interval" }, + { 135, "Maximum_Value" }, + { 136, "Minimum_Value" }, + { 137, "Notification_Threshold" }, + { 139, "Protocol_Revision" }, + { 140, "Records_Since_Notification" }, + { 141, "Record_Count" }, + { 142, "Start_Time" }, + { 143, "Stop_Time" }, + { 144, "Stop_When_Full" }, + { 145, "Total_Record_Count" }, + { 146, "Valid_Samples" }, + { 147, "Window_Interval" }, + { 148, "Window_Samples" }, + { 149, "Maximum_Value_Timestamp" }, + { 150, "Minimum_Value_Timestamp" }, + { 151, "Variance_Value" }, + { 152, "Active_Cov_Subscriptions" }, + { 153, "Backup_Failure_Timeout" }, + { 154, "Configuration_Files" }, + { 155, "Database_Revision" }, + { 156, "Direct_Reading" }, + { 157, "Last_Restore_Time" }, + { 158, "Maintenance_Required" }, + { 159, "Member_Of" }, + { 160, "Mode" }, + { 161, "Operation_Expected" }, + { 162, "Setting" }, + { 163, "Silenced" }, + { 164, "Tracking_Value" }, + { 165, "Zone_Members" }, + { 166, "Life_Safety_Alarm_Values" }, + { 167, "Max_Segments_Accepted" }, + { 168, "Profile_Name" }, + { 169, "Auto_Subordinate_Discovery" }, + { 170, "Manual_Subordinate_Address_Binding" }, + { 171, "Subordinate_Address_Binding" }, + { 172, "Subordinate_Proxy_Enable" }, + { 173, "Last_Notify_Record" }, + { 174, "Schedule_Default" }, + { 175, "Accepted_Modes" }, + { 176, "Adjust_Value" }, + { 177, "Count" }, + { 178, "Count_Before_Change" }, + { 179, "Count_Change_Time" }, + { 180, "Cov_Period" }, + { 181, "Input_Reference" }, + { 182, "Limit_Monitoring_Interval" }, + { 183, "Logging_Object" }, + { 184, "Logging_Record" }, + { 185, "Prescale" }, + { 186, "Pulse_Rate" }, + { 187, "Scale" }, + { 188, "Scale_Factor" }, + { 189, "Update_Time" }, + { 190, "Value_Before_Change" }, + { 191, "Value_Set" }, + { 192, "Value_Change_Time" }, + { 193, "Align_Intervals" }, + { 195, "Interval_Offset" }, + { 196, "Last_Restart_Reason" }, + { 197, "Logging_Type" }, + { 202, "Restart_Notification_Recipients" }, + { 203, "Time_Of_Device_Restart" }, + { 204, "Time_Synchronization_Interval" }, + { 205, "Trigger" }, + { 206, "Utc_Time_Synchronization_Recipients" }, + { 207, "Node_Subtype" }, + { 208, "Node_Type" }, + { 209, "Structured_Object_List" }, + { 210, "Subordinate_Annotations" }, + { 211, "Subordinate_List" }, + { 212, "Actual_Shed_Level" }, + { 213, "Duty_Window" }, + { 214, "Expected_Shed_Level" }, + { 215, "Full_Duty_Baseline" }, + { 218, "Requested_Shed_Level" }, + { 219, "Shed_Duration" }, + { 220, "Shed_Level_Descriptions" }, + { 221, "Shed_Levels" }, + { 222, "State_Description" }, + { 226, "Door_Alarm_State" }, + { 227, "Door_Extended_Pulse_Time" }, + { 228, "Door_Members" }, + { 229, "Door_Open_Too_Long_Time" }, + { 230, "Door_Pulse_Time" }, + { 231, "Door_Status" }, + { 232, "Door_Unlock_Delay_Time" }, + { 233, "Lock_Status" }, + { 234, "Masked_Alarm_Values" }, + { 235, "Secured_Status" }, + { 244, "Absentee_Limit" }, + { 245, "Access_Alarm_Events" }, + { 246, "Access_Doors" }, + { 247, "Access_Event" }, + { 248, "Access_Event_Authentication_Factor" }, + { 249, "Access_Event_Credential" }, + { 250, "Access_Event_Time" }, + { 251, "Access_Transaction_Events" }, + { 252, "Accompaniment" }, + { 253, "Accompaniment_Time" }, + { 254, "Activation_Time" }, + { 255, "Active_Authentication_Policy" }, + { 256, "Assigned_Access_Rights" }, + { 257, "Authentication_Factors" }, + { 258, "Authentication_Policy_List" }, + { 259, "Authentication_Policy_Names" }, + { 260, "Authentication_Status" }, + { 261, "Authorization_Mode" }, + { 262, "Belongs_To" }, + { 263, "Credential_Disable" }, + { 264, "Credential_Status" }, + { 265, "Credentials" }, + { 266, "Credentials_In_Zone" }, + { 267, "Days_Remaining" }, + { 268, "Entry_Points" }, + { 269, "Exit_Points" }, + { 270, "Expiration_Time" }, + { 271, "Extended_Time_Enable" }, + { 272, "Failed_Attempt_Events" }, + { 273, "Failed_Attempts" }, + { 274, "Failed_Attempts_Time" }, + { 275, "Last_Access_Event" }, + { 276, "Last_Access_Point" }, + { 277, "Last_Credential_Added" }, + { 278, "Last_Credential_Added_Time" }, + { 279, "Last_Credential_Removed" }, + { 280, "Last_Credential_Removed_Time" }, + { 281, "Last_Use_Time" }, + { 282, "Lockout" }, + { 283, "Lockout_Relinquish_Time" }, + { 285, "Max_Failed_Attempts" }, + { 286, "Members" }, + { 287, "Muster_Point" }, + { 288, "Negative_Access_Rules" }, + { 289, "Number_Of_Authentication_Policies" }, + { 290, "Occupancy_Count" }, + { 291, "Occupancy_Count_Adjust" }, + { 292, "Occupancy_Count_Enable" }, + { 294, "Occupancy_Lower_Limit" }, + { 295, "Occupancy_Lower_Limit_Enforced" }, + { 296, "Occupancy_State" }, + { 297, "Occupancy_Upper_Limit" }, + { 298, "Occupancy_Upper_Limit_Enforced" }, + { 300, "Passback_Mode" }, + { 301, "Passback_Timeout" }, + { 302, "Positive_Access_Rules" }, + { 303, "Reason_For_Disable" }, + { 304, "Supported_Formats" }, + { 305, "Supported_Format_Classes" }, + { 306, "Threat_Authority" }, + { 307, "Threat_Level" }, + { 308, "Trace_Flag" }, + { 309, "Transaction_Notification_Class" }, + { 310, "User_External_Identifier" }, + { 311, "User_Information_Reference" }, + { 317, "User_Name" }, + { 318, "User_Type" }, + { 319, "Uses_Remaining" }, + { 320, "Zone_From" }, + { 321, "Zone_To" }, + { 322, "Access_Event_Tag" }, + { 323, "Global_Identifier" }, + { 326, "Verification_Time" }, + { 327, "Base_Device_Security_Policy" }, + { 328, "Distribution_Key_Revision" }, + { 329, "Do_Not_Hide" }, + { 330, "Key_Sets" }, + { 331, "Last_Key_Server" }, + { 332, "Network_Access_Security_Policies" }, + { 333, "Packet_Reorder_Time" }, + { 334, "Security_Pdu_Timeout" }, + { 335, "Security_Time_Window" }, + { 336, "Supported_Security_Algorithms" }, + { 338, "Backup_And_Restore_State" }, + { 339, "Backup_Preparation_Time" }, + { 340, "Restore_Completion_Time" }, + { 341, "Restore_Preparation_Time" }, + { 342, "Bit_Mask" }, + { 343, "Bit_Text" }, + { 344, "Is_Utc" }, + { 345, "Group_Members" }, + { 346, "Group_Member_Names" }, + { 347, "Member_Status_Flags" }, + { 348, "Requested_Update_Interval" }, + { 349, "Covu_Period" }, + { 350, "Covu_Recipients" }, + { 351, "Event_Message_Texts" }, + { 352, "Event_Message_Texts_Config" }, + { 353, "Event_Detection_Enable" }, + { 354, "Event_Algorithm_Inhibit" }, + { 355, "Event_Algorithm_Inhibit_Ref" }, + { 356, "Time_Delay_Normal" }, + { 357, "Reliability_Evaluation_Inhibit" }, + { 358, "Fault_Parameters" }, + { 359, "Fault_Type" }, + { 360, "Local_Forwarding_Only" }, + { 361, "Process_Identifier_Filter" }, + { 362, "Subscribed_Recipients" }, + { 363, "Port_Filter" }, + { 364, "Authorization_Exemptions" }, + { 365, "Allow_Group_Delay_Inhibit" }, + { 366, "Channel_Number" }, + { 367, "Control_Groups" }, + { 368, "Execution_Delay" }, + { 369, "Last_Priority" }, + { 370, "Write_Status" }, + { 371, "Property_List" }, + { 372, "Serial_Number" }, + { 373, "Blink_Warn_Enable" }, + { 374, "Default_Fade_Time" }, + { 375, "Default_Ramp_Rate" }, + { 376, "Default_Step_Increment" }, + { 377, "Egress_Time" }, + { 378, "In_Progress" }, + { 379, "Instantaneous_Power" }, + { 380, "Lighting_Command" }, + { 381, "Lighting_Command_Default_Priority" }, + { 382, "Max_Actual_Value" }, + { 383, "Min_Actual_Value" }, + { 384, "Power" }, + { 385, "Transition" }, + { 386, "Egress_Active" }, + { 387, "Interface_Value" }, + { 388, "Fault_High_Limit" }, + { 389, "Fault_Low_Limit" }, + { 390, "Low_Diff_Limit" }, + { 391, "Strike_Count" }, + { 392, "Time_Of_Strike_Count_Reset" }, + { 393, "Default_Timeout" }, + { 394, "Initial_Timeout" }, + { 395, "Last_State_Change" }, + { 396, "State_Change_Values" }, + { 397, "Timer_Running" }, + { 398, "Timer_State" }, + { 399, "Apdu_Length" }, + { 400, "Ip_Address" }, + { 401, "Ip_Default_Gateway" }, + { 402, "Ip_Dhcp_Enable" }, + { 403, "Ip_Dhcp_Lease_Time" }, + { 404, "Ip_Dhcp_Lease_Time_Remaining" }, + { 405, "Ip_Dhcp_Server" }, + { 406, "Ip_Dns_Server" }, + { 407, "Bacnet_Ip_Global_Address" }, + { 408, "Bacnet_Ip_Mode" }, + { 409, "Bacnet_Ip_Multicast_Address" }, + { 410, "Bacnet_Ip_Nat_Traversal" }, + { 411, "Ip_Subnet_Mask" }, + { 412, "Bacnet_Ip_Udp_Port" }, + { 413, "Bbmd_Accept_Fd_Registrations" }, + { 414, "Bbmd_Broadcast_Distribution_Table" }, + { 415, "Bbmd_Foreign_Device_Table" }, + { 416, "Changes_Pending" }, + { 417, "Command" }, + { 418, "Fd_Bbmd_Address" }, + { 419, "Fd_Subscription_Lifetime" }, + { 420, "Link_Speed" }, + { 421, "Link_Speeds" }, + { 422, "Link_Speed_Autonegotiate" }, + { 423, "Mac_Address" }, + { 424, "Network_Interface_Name" }, + { 425, "Network_Number" }, + { 426, "Network_Number_Quality" }, + { 427, "Network_Type" }, + { 428, "Routing_Table" }, + { 429, "Virtual_Mac_Address_Table" }, + { 430, "Command_Time_Array" }, + { 431, "Current_Command_Priority" }, + { 432, "Last_Command_Time" }, + { 433, "Value_Source" }, + { 434, "Value_Source_Array" }, + { 435, "Bacnet_Ipv6_Mode" }, + { 436, "Ipv6_Address" }, + { 437, "Ipv6_Prefix_Length" }, + { 438, "Bacnet_Ipv6_Udp_Port" }, + { 439, "Ipv6_Default_Gateway" }, + { 440, "Bacnet_Ipv6_Multicast_Address" }, + { 441, "Ipv6_Dns_Server" }, + { 442, "Ipv6_Auto_Addressing_Enable" }, + { 443, "Ipv6_Dhcp_Lease_Time" }, + { 444, "Ipv6_Dhcp_Lease_Time_Remaining" }, + { 445, "Ipv6_Dhcp_Server" }, + { 446, "Ipv6_Zone_Index" }, + { 447, "Assigned_Landing_Calls" }, + { 448, "Car_Assigned_Direction" }, + { 449, "Car_Door_Command" }, + { 450, "Car_Door_Status" }, + { 451, "Car_Door_Text" }, + { 452, "Car_Door_Zone" }, + { 453, "Car_Drive_Status" }, + { 454, "Car_Load" }, + { 455, "Car_Load_Units" }, + { 456, "Car_Mode" }, + { 457, "Car_Moving_Direction" }, + { 458, "Car_Position" }, + { 459, "Elevator_Group" }, + { 460, "Energy_Meter" }, + { 461, "Energy_Meter_Ref" }, + { 462, "Escalator_Mode" }, + { 463, "Fault_Signals" }, + { 464, "Floor_Text" }, + { 465, "Group_Id" }, + { 467, "Group_Mode" }, + { 468, "Higher_Deck" }, + { 469, "Installation_Id" }, + { 470, "Landing_Calls" }, + { 471, "Landing_Call_Control" }, + { 472, "Landing_Door_Status" }, + { 473, "Lower_Deck" }, + { 474, "Machine_Room_Id" }, + { 475, "Making_Car_Call" }, + { 476, "Next_Stopping_Floor" }, + { 477, "Operation_Direction" }, + { 478, "Passenger_Alarm" }, + { 479, "Power_Mode" }, + { 480, "Registered_Car_Call" }, + { 481, "Active_Cov_Multiple_Subscriptions" }, + { 482, "Protocol_Level" }, + { 483, "Reference_Port" }, + { 484, "Deployed_Profile_Location" }, + { 485, "Profile_Location" }, + { 486, "Tags" }, + { 487, "Subordinate_Node_Types" }, + { 488, "Subordinate_Tags" }, + { 489, "Subordinate_Relationships" }, + { 490, "Default_Subordinate_Relationship" }, + { 491, "Represents" }, + { 492, "Default_Present_Value" }, + { 493, "Present_Stage" }, + { 494, "Stages" }, + { 495, "Stage_Names" }, + { 496, "Target_References" }, + { 497, "Audit_Source_Reporter" }, + { 498, "Audit_Level" }, + { 499, "Audit_Notification_Recipient" }, + { 500, "Audit_Priority_Filter" }, + { 501, "Auditable_Operations" }, + { 502, "Delete_On_Forward" }, + { 503, "Maximum_Send_Delay" }, + { 504, "Monitored_Objects" }, + { 505, "Send_Now" }, + { 506, "Floor_Number" }, + { 507, "Device_Uuid" }, + { 508, "Additional_Reference_Ports" }, + { 509, "Certificate_Signing_Request_File" }, + { 510, "Command_Validation_Result" }, + { 511, "Issuer_Certificate_Files" }, + { 4194304, "Max_Bvlc_Length_Accepted" }, + { 4194305, "Max_Npdu_Length_Accepted" }, + { 4194306, "Operational_Certificate_File" }, + { 4194307, "Current_Health" }, + { 4194308, "Sc_Connect_Wait_Timeout" }, + { 4194309, "Sc_Direct_Connect_Accept_Enable" }, + { 4194310, "Sc_Direct_Connect_Accept_Uris" }, + { 4194311, "Sc_Direct_Connect_Binding" }, + { 4194312, "Sc_Direct_Connect_Connection_Status" }, + { 4194313, "Sc_Direct_Connect_Initiate_Enable" }, + { 4194314, "Sc_Disconnect_Wait_Timeout" }, + { 4194315, "Sc_Failed_Connection_Requests" }, + { 4194316, "Sc_Failover_Hub_Connection_Status" }, + { 4194317, "Sc_Failover_Hub_Uri" }, + { 4194318, "Sc_Hub_Connector_State" }, + { 4194319, "Sc_Hub_Function_Accept_Uris" }, + { 4194320, "Sc_Hub_Function_Binding" }, + { 4194321, "Sc_Hub_Function_Connection_Status" }, + { 4194322, "Sc_Hub_Function_Enable" }, + { 4194323, "Sc_Heartbeat_Timeout" }, + { 4194324, "Sc_Primary_Hub_Connection_Status" }, + { 4194325, "Sc_Primary_Hub_Uri" }, + { 4194326, "Sc_Maximum_Reconnect_Time" }, + { 4194327, "Sc_Minimum_Reconnect_Time" }, + { 4194328, "Color_Override" }, + { 4194329, "Color_Reference" }, + { 4194330, "Default_Color" }, + { 4194331, "Default_Color_Temperature" }, + { 4194332, "Override_Color_Reference" }, + { 4194333, "Write_Every_Scheduled_Action" }, + { 4194334, "Color_Command" }, + { 4194335, "High_End_Trim" }, + { 4194336, "Low_End_Trim" }, + { 4194337, "Trim_Fade_Time" }, + { 4194338, "Device_Address_Proxy_Enable" }, + { 4194339, "Device_Address_Proxy_Table" }, + { 4194340, "Device_Address_Proxy_Timeout" }, + { 4194341, "Default_On_Value" }, + { 4194342, "Last_On_Value" }, + { 4194343, "Authorization_Cache" }, + { 4194344, "Authorization_Groups" }, + { 4194345, "Authorization_Policy" }, + { 4194346, "Authorization_Scope" }, + { 4194347, "Authorization_Server" }, + { 4194348, "Authorization_Status" }, + { 4194349, "Max_Proxied_I_Ams_Per_Second" }, +}; + +const char* LookUpIdName(const IdName* table, size_t count, uint32_t id) { + size_t lo = 0; + size_t hi = count; + while (lo < hi) { + const size_t mid = lo + (hi - lo) / 2; + if (table[mid].id == id) { + return table[mid].name; + } + if (table[mid].id < id) { + lo = mid + 1; + } else { + hi = mid; + } + } + return NULL; +} + +const char* ObjectTypeName(uint16_t objectType) { + return LookUpIdName(OBJECT_TYPE_NAMES, sizeof(OBJECT_TYPE_NAMES) / sizeof(OBJECT_TYPE_NAMES[0]), objectType); +} + +const char* PropertyName(uint32_t propertyId) { + return LookUpIdName(PROPERTY_NAMES, sizeof(PROPERTY_NAMES) / sizeof(PROPERTY_NAMES[0]), propertyId); +} + +// --- Generic BACnet tag-header decode (Clause 20.2.1) ------------------------ +struct TagHeader { + uint8_t tagNumber; + bool contextSpecific; + bool isOpeningTag; // constructed-tag opening ("(" - LVT==6) + bool isClosingTag; // constructed-tag closing (")" - LVT==7) + uint32_t length; // data length for a primitive tag; 0 for opening/closing + uint32_t headerLength; // bytes the tag header itself occupied +}; + +// Decodes the tag header starting at frame[offset]. Returns false (leaves +// *out untouched) on truncation or an encoding this decoder doesn't handle - +// every caller treats that as "stop decoding, fall back to what's already +// known" rather than guessing at the rest of the frame. +bool DecodeTagHeader(const uint8_t* frame, uint32_t frameLength, uint32_t offset, TagHeader* out) { + if (offset >= frameLength) { + return false; + } + uint32_t pos = offset; + const uint8_t b0 = frame[pos]; + uint8_t tagNumber = (b0 >> 4) & 0x0F; + const bool contextSpecific = (b0 & 0x08) != 0; + const uint8_t lvt = b0 & 0x07; + pos += 1; + if (tagNumber == 0x0F) { // extended tag number + if (pos >= frameLength) { + return false; + } + tagNumber = frame[pos]; + pos += 1; + } + bool isOpening = false; + bool isClosing = false; + uint32_t length = 0; + if (contextSpecific && lvt == 6) { + isOpening = true; + } else if (contextSpecific && lvt == 7) { + isClosing = true; + } else if (lvt <= 4) { + length = lvt; + } else if (lvt == 5) { // extended length + if (pos >= frameLength) { + return false; + } + const uint8_t lenByte = frame[pos]; + pos += 1; + if (lenByte < 254) { + length = lenByte; + } else if (lenByte == 254) { + if (pos + 2 > frameLength) { + return false; + } + length = ((uint32_t)frame[pos] << 8) | frame[pos + 1]; + pos += 2; + } else { + if (pos + 4 > frameLength) { + return false; + } + length = ((uint32_t)frame[pos] << 24) | ((uint32_t)frame[pos + 1] << 16) | + ((uint32_t)frame[pos + 2] << 8) | frame[pos + 3]; + pos += 4; + } + } else { + return false; // LVT 6/7 without the context-specific bit isn't valid here + } + out->tagNumber = tagNumber; + out->contextSpecific = contextSpecific; + out->isOpeningTag = isOpening; + out->isClosingTag = isClosing; + out->length = length; + out->headerLength = pos - offset; + return true; +} + +// --- One decode pass shared by the one-line summary and the XML dump -------- +// Everything SummarizeBacnetFrame() and the XML dump print comes out of one +// DecodedFrame, built by one walk of the bytes - the two views can never +// disagree with each other about what a frame contains. +struct DecodedFrame { + bool bacnetIp; // false: not even a recognizable BVLC header + uint8_t bvlcFunction; + bool hasNpdu; // false for a BVLC control message (no NPDU/APDU) + uint8_t npduVersion; + uint8_t npduControl; + bool hasDest; + uint16_t dnet; + const uint8_t* dadr; // points into the original frame; NULL/0-length = broadcast on dnet + uint8_t dadrLength; + bool hasSrc; + uint16_t snet; + const uint8_t* sadr; + uint8_t sadrLength; + bool isNetworkMessage; + uint8_t networkMessageType; + bool hasApdu; + uint8_t pduType; // 0-7 per Clause 20.1 + bool segmented; + bool hasInvokeId; + uint8_t invokeId; + bool hasServiceChoice; + uint8_t serviceChoice; // confirmed-service-choice for 0/2/3/5; unconfirmed for 1 + bool hasReason; + uint8_t reason; // reject reason (pduType 6) or abort reason (pduType 7) + bool hasObjectProperty; + uint16_t objectType; + uint32_t objectInstance; + uint32_t propertyId; + bool hasArrayIndex; + uint32_t arrayIndex; + + DecodedFrame() + : bacnetIp(false), bvlcFunction(0), hasNpdu(false), npduVersion(0), npduControl(0), + hasDest(false), dnet(0), dadr(NULL), dadrLength(0), hasSrc(false), snet(0), sadr(NULL), + sadrLength(0), isNetworkMessage(false), networkMessageType(0), hasApdu(false), pduType(0xFF), + segmented(false), hasInvokeId(false), invokeId(0), hasServiceChoice(false), serviceChoice(0), + hasReason(false), reason(0), hasObjectProperty(false), objectType(0), objectInstance(0), + propertyId(0), hasArrayIndex(false), arrayIndex(0) {} +}; + +// Shared layout of ReadProperty-Request, ReadProperty-ACK and +// WriteProperty-Request: context tag0 = ObjectIdentifier (4-byte primitive), +// context tag1 = PropertyIdentifier (1-4 byte unsigned), optional context +// tag2 = PropertyArrayIndex (1-4 byte unsigned). Populates the +// ObjectProperty* fields on success; leaves hasObjectProperty false if the +// layout doesn't match, so callers just omit that detail rather than +// printing something wrong. +void DecodeObjectAndProperty(const uint8_t* frame, uint32_t frameLength, uint32_t offset, + DecodedFrame* out) { + TagHeader objTag; + if (!DecodeTagHeader(frame, frameLength, offset, &objTag) || !objTag.contextSpecific || + objTag.tagNumber != 0 || objTag.length != 4 || + offset + objTag.headerLength + 4 > frameLength) { + return; + } + const uint32_t objValueOffset = offset + objTag.headerLength; + const uint32_t objValue = ((uint32_t)frame[objValueOffset] << 24) | + ((uint32_t)frame[objValueOffset + 1] << 16) | + ((uint32_t)frame[objValueOffset + 2] << 8) | + frame[objValueOffset + 3]; + uint32_t pos = objValueOffset + 4; + + TagHeader propTag; + if (!DecodeTagHeader(frame, frameLength, pos, &propTag) || !propTag.contextSpecific || + propTag.tagNumber != 1 || propTag.length == 0 || propTag.length > 4 || + pos + propTag.headerLength + propTag.length > frameLength) { + return; + } + pos += propTag.headerLength; + uint32_t propValue = 0; + for (uint32_t i = 0; i < propTag.length; ++i) { + propValue = (propValue << 8) | frame[pos + i]; + } + pos += propTag.length; + + out->hasObjectProperty = true; + out->objectType = (uint16_t)((objValue >> 22) & 0x3FF); + out->objectInstance = objValue & 0x3FFFFF; + out->propertyId = propValue; + + TagHeader idxTag; + if (DecodeTagHeader(frame, frameLength, pos, &idxTag) && idxTag.contextSpecific && + idxTag.tagNumber == 2 && idxTag.length > 0 && idxTag.length <= 4 && + pos + idxTag.headerLength + idxTag.length <= frameLength) { + uint32_t v = 0; + for (uint32_t i = 0; i < idxTag.length; ++i) { + v = (v << 8) | frame[pos + idxTag.headerLength + i]; + } + out->hasArrayIndex = true; + out->arrayIndex = v; + } +} + +// Walks BVLC -> NPDU -> APDU exactly as far as needed to fill in 'out'; +// never reads past frameLength. See DecodedFrame's comment. +void DecodeBacnetFrame(const uint8_t* frame, uint32_t frameLength, DecodedFrame* out) { + *out = DecodedFrame(); + if (frame == NULL || frameLength < 4 || frame[0] != 0x81) { + return; + } + out->bacnetIp = true; + out->bvlcFunction = frame[1]; + uint32_t offset; + switch (out->bvlcFunction) { + case 0x0A: // Original-Unicast-NPDU + case 0x0B: // Original-Broadcast-NPDU + offset = 4; + break; + case 0x04: // Forwarded-NPDU: BVLC header + 6-byte originating address + offset = 10; + break; + default: + return; // a BVLC control message carries no NPDU/APDU + } + if (offset + 2u > frameLength) { + return; + } + out->hasNpdu = true; + out->npduVersion = frame[offset]; + out->npduControl = frame[offset + 1]; + offset += 2; + + out->hasDest = (out->npduControl & 0x20) != 0; + out->hasSrc = (out->npduControl & 0x08) != 0; + out->isNetworkMessage = (out->npduControl & 0x80) != 0; + + if (out->hasDest) { + if (offset + 3u > frameLength) { + return; + } + out->dnet = (uint16_t)((frame[offset] << 8) | frame[offset + 1]); + const uint8_t dlen = frame[offset + 2]; + offset += 3; + if (offset + dlen > frameLength) { + return; + } + out->dadrLength = dlen; + out->dadr = (dlen > 0) ? &frame[offset] : NULL; + offset += dlen; + } + if (out->hasSrc) { + if (offset + 3u > frameLength) { + return; + } + out->snet = (uint16_t)((frame[offset] << 8) | frame[offset + 1]); + const uint8_t slen = frame[offset + 2]; + offset += 3; + if (offset + slen > frameLength) { + return; + } + out->sadrLength = slen; + out->sadr = (slen > 0) ? &frame[offset] : NULL; + offset += slen; + } + if (out->hasDest) { + if (offset >= frameLength) { + return; + } + offset += 1; // hop count, present whenever DNET is (Clause 6.2.2) + } + if (offset >= frameLength) { + return; + } + + if (out->isNetworkMessage) { + out->networkMessageType = frame[offset]; + return; + } + + out->hasApdu = true; + out->pduType = (frame[offset] >> 4) & 0x0F; + switch (out->pduType) { + case 0: { // Confirmed-Request + out->segmented = (frame[offset] & 0x08) != 0; + if (offset + 2u >= frameLength) { + return; + } + out->hasInvokeId = true; + out->invokeId = frame[offset + 2]; + const uint32_t serviceOffset = offset + (out->segmented ? 5u : 3u); + if (serviceOffset >= frameLength) { + return; + } + out->hasServiceChoice = true; + out->serviceChoice = frame[serviceOffset]; + if (out->serviceChoice == 12 || out->serviceChoice == 15) { // ReadProperty, WriteProperty + DecodeObjectAndProperty(frame, frameLength, serviceOffset + 1, out); + } + return; + } + case 1: { // Unconfirmed-Request + if (offset + 1u >= frameLength) { + return; + } + out->hasServiceChoice = true; + out->serviceChoice = frame[offset + 1]; + return; + } + case 2: { // SimpleACK + if (offset + 2u >= frameLength) { + return; + } + out->hasInvokeId = true; + out->invokeId = frame[offset + 1]; + out->hasServiceChoice = true; + out->serviceChoice = frame[offset + 2]; + return; + } + case 3: { // ComplexACK + out->segmented = (frame[offset] & 0x08) != 0; + if (offset + 1u >= frameLength) { + return; + } + out->hasInvokeId = true; + out->invokeId = frame[offset + 1]; + const uint32_t serviceOffset = offset + (out->segmented ? 4u : 2u); + if (serviceOffset >= frameLength) { + return; + } + out->hasServiceChoice = true; + out->serviceChoice = frame[serviceOffset]; + if (out->serviceChoice == 12) { // ReadProperty-ACK + DecodeObjectAndProperty(frame, frameLength, serviceOffset + 1, out); + } + return; + } + case 4: // SegmentACK - nothing further to decode + return; + case 5: { // Error + if (offset + 2u >= frameLength) { + return; + } + out->hasInvokeId = true; + out->invokeId = frame[offset + 1]; + out->hasServiceChoice = true; + out->serviceChoice = frame[offset + 2]; + return; + } + case 6: { // Reject + if (offset + 2u >= frameLength) { + return; + } + out->hasInvokeId = true; + out->invokeId = frame[offset + 1]; + out->hasReason = true; + out->reason = frame[offset + 2]; + return; + } + case 7: { // Abort + if (offset + 2u >= frameLength) { + return; + } + out->hasInvokeId = true; + out->invokeId = frame[offset + 1]; + out->hasReason = true; + out->reason = frame[offset + 2]; + return; + } + default: + return; + } +} + +void AppendHex(char* buf, size_t bufLen, const uint8_t* data, uint8_t length) { + size_t used = strlen(buf); + for (uint8_t i = 0; i < length && used + 2 < bufLen; ++i) { + used += (size_t)snprintf(buf + used, bufLen - used, "%02X", data[i]); + } +} + +// Appends " DNET=[ DADR=]" to 'buf' (nothing if the frame carries no +// destination specifier) - the shared tail for the one-line summary. +void AppendRouteSuffix(char* buf, size_t bufLen, const DecodedFrame& d) { + if (!d.hasDest) { + return; + } + size_t used = strlen(buf); + snprintf(buf + used, bufLen - used, " DNET=%u", (unsigned)d.dnet); + if (d.dadrLength > 0 && d.dadr != NULL) { + used = strlen(buf); + snprintf(buf + used, bufLen - used, " DADR="); + AppendHex(buf, bufLen, d.dadr, d.dadrLength); + } +} + +} // namespace + +void CASExampleHelper::SummarizeBacnetFrame(const uint8_t* frame, uint16_t frameLength, + char* outSummary, size_t outSummaryLength) { + if (outSummary == NULL || outSummaryLength == 0) { + return; + } + outSummary[0] = '\0'; + + DecodedFrame d; + DecodeBacnetFrame(frame, frameLength, &d); + if (!d.bacnetIp) { + snprintf(outSummary, outSummaryLength, "?"); + return; + } + if (!d.hasNpdu) { + snprintf(outSummary, outSummaryLength, "BVLC function=0x%02X", (unsigned)d.bvlcFunction); + return; + } + if (d.isNetworkMessage) { + const char* name = NetworkMessageName(d.networkMessageType); + if (name != NULL) snprintf(outSummary, outSummaryLength, "Network-Layer-Message: %s", name); + else snprintf(outSummary, outSummaryLength, "Network-Layer-Message: type=%u", (unsigned)d.networkMessageType); + AppendRouteSuffix(outSummary, outSummaryLength, d); + return; + } + if (!d.hasApdu) { + snprintf(outSummary, outSummaryLength, "?"); + return; + } + + char body[128]; + body[0] = '\0'; + switch (d.pduType) { + case 0: { + const char* name = d.hasServiceChoice ? ConfirmedServiceName(d.serviceChoice) : NULL; + if (!d.hasServiceChoice) snprintf(body, sizeof(body), "ConfirmedRequest"); + else if (name != NULL) snprintf(body, sizeof(body), "ConfirmedRequest: %s", name); + else snprintf(body, sizeof(body), "ConfirmedRequest: service=%u", (unsigned)d.serviceChoice); + break; + } + case 1: { + const char* name = d.hasServiceChoice ? UnconfirmedServiceName(d.serviceChoice) : NULL; + if (!d.hasServiceChoice) snprintf(body, sizeof(body), "Unconfirmed"); + else if (name != NULL) snprintf(body, sizeof(body), "Unconfirmed: %s", name); + else snprintf(body, sizeof(body), "Unconfirmed: service=%u", (unsigned)d.serviceChoice); + break; + } + case 2: { + const char* name = d.hasServiceChoice ? ConfirmedServiceName(d.serviceChoice) : NULL; + if (!d.hasServiceChoice) snprintf(body, sizeof(body), "SimpleACK"); + else if (name != NULL) snprintf(body, sizeof(body), "SimpleACK: %s", name); + else snprintf(body, sizeof(body), "SimpleACK: service=%u", (unsigned)d.serviceChoice); + break; + } + case 3: { + const char* name = d.hasServiceChoice ? ConfirmedServiceName(d.serviceChoice) : NULL; + if (!d.hasServiceChoice) snprintf(body, sizeof(body), "ComplexACK"); + else if (name != NULL) snprintf(body, sizeof(body), "ComplexACK: %s", name); + else snprintf(body, sizeof(body), "ComplexACK: service=%u", (unsigned)d.serviceChoice); + break; + } + case 4: + snprintf(body, sizeof(body), "SegmentACK"); + break; + case 5: { + const char* name = d.hasServiceChoice ? ConfirmedServiceName(d.serviceChoice) : NULL; + if (!d.hasServiceChoice) snprintf(body, sizeof(body), "Error"); + else if (name != NULL) snprintf(body, sizeof(body), "Error: %s", name); + else snprintf(body, sizeof(body), "Error: service=%u", (unsigned)d.serviceChoice); + break; + } + case 6: { + const char* name = d.hasReason ? RejectReasonName(d.reason) : NULL; + if (!d.hasReason) snprintf(body, sizeof(body), "Reject"); + else if (name != NULL) snprintf(body, sizeof(body), "Reject: %s", name); + else snprintf(body, sizeof(body), "Reject: reason=%u", (unsigned)d.reason); + break; + } + case 7: { + const char* name = d.hasReason ? AbortReasonName(d.reason) : NULL; + if (!d.hasReason) snprintf(body, sizeof(body), "Abort"); + else if (name != NULL) snprintf(body, sizeof(body), "Abort: %s", name); + else snprintf(body, sizeof(body), "Abort: reason=%u", (unsigned)d.reason); + break; + } + default: + snprintf(body, sizeof(body), "?"); + break; + } + + if (d.hasObjectProperty) { + const char* typeName = ObjectTypeName(d.objectType); + const char* propName = PropertyName(d.propertyId); + const size_t used = strlen(body); + if (d.hasArrayIndex) { + if (typeName != NULL && propName != NULL) { + snprintf(body + used, sizeof(body) - used, " %s %u.%s[%u]", + typeName, (unsigned)d.objectInstance, propName, (unsigned)d.arrayIndex); + } else { + snprintf(body + used, sizeof(body) - used, " object-type=%u %u.property=%u[%u]", + (unsigned)d.objectType, (unsigned)d.objectInstance, + (unsigned)d.propertyId, (unsigned)d.arrayIndex); + } + } else { + if (typeName != NULL && propName != NULL) { + snprintf(body + used, sizeof(body) - used, " %s %u.%s", + typeName, (unsigned)d.objectInstance, propName); + } else { + snprintf(body + used, sizeof(body) - used, " object-type=%u %u.property=%u", + (unsigned)d.objectType, (unsigned)d.objectInstance, (unsigned)d.propertyId); + } + } + } + + snprintf(outSummary, outSummaryLength, "%s", body); + AppendRouteSuffix(outSummary, outSummaryLength, d); +} + +// --- XML frame dump (off by default; ParseXmlLogArg turns it on) ----------- +namespace { +bool g_xmlFrameLoggingEnabled = false; +} + +void CASExampleHelper::ParseXmlLogArg(int argc, char** argv) { + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--xmlLog") == 0 || strcmp(argv[i], "--xml") == 0) { + g_xmlFrameLoggingEnabled = true; + return; + } + } +} + +bool CASExampleHelper::IsXmlFrameLoggingEnabled() { + return g_xmlFrameLoggingEnabled; +} + +namespace { + +// Prints one indented XML block describing 'frame' to stdout (no leading/ +// trailing blank line management beyond what's shown - the caller has +// already printed the "RX/TX bytes ..." prefix with no newline). Very +// verbose by design - this is the deep-debug path, off by default. Always +// includes the full raw hex regardless of how much of the structured decode +// above succeeded, so nothing is ever silently lost (segmented PDUs, a +// service other than ReadProperty/WriteProperty, or the actual property +// VALUE bytes - none of which this decoder parses - are still fully present +// in RawHex for manual inspection). +void PrintFrameAsXml(const uint8_t* frame, uint32_t frameLength) { + DecodedFrame d; + DecodeBacnetFrame(frame, frameLength, &d); + + printf("\n \n", (unsigned)frameLength); + if (!d.bacnetIp) { + printf(" not a recognizable BACnet/IP (BVLC) frame\n \n"); + return; + } + printf(" \n", (unsigned)d.bvlcFunction); + if (d.hasNpdu) { + printf(" \n", (unsigned)d.npduVersion, (unsigned)d.npduControl); + if (d.hasDest) { + printf(" 0 && d.dadr != NULL) { + printf(" mac=\""); + for (uint8_t i = 0; i < d.dadrLength; ++i) { + printf("%02X", d.dadr[i]); + } + printf("\""); + } + printf("/>\n"); + } + if (d.hasSrc) { + printf(" 0 && d.sadr != NULL) { + printf(" mac=\""); + for (uint8_t i = 0; i < d.sadrLength; ++i) { + printf("%02X", d.sadr[i]); + } + printf("\""); + } + printf("/>\n"); + } + if (d.isNetworkMessage) { + const char* name = NetworkMessageName(d.networkMessageType); + if (name != NULL) printf(" \n", (unsigned)d.networkMessageType, name); + else printf(" \n", (unsigned)d.networkMessageType); + } else if (d.hasApdu) { + static const char* const PDU_TYPE_NAMES[] = { + "ConfirmedRequest", "UnconfirmedRequest", "SimpleACK", "ComplexACK", + "SegmentACK", "Error", "Reject", "Abort" + }; + const char* pduTypeName = (d.pduType < 8) ? PDU_TYPE_NAMES[d.pduType] : "Unknown"; + printf(" \n"); + } else { + printf(">\n"); + const char* typeName = ObjectTypeName(d.objectType); + if (typeName != NULL) printf(" \n", + (unsigned)d.objectType, typeName, (unsigned)d.objectInstance); + else printf(" \n", + (unsigned)d.objectType, (unsigned)d.objectInstance); + const char* propName = PropertyName(d.propertyId); + if (propName != NULL) printf(" \n", (unsigned)d.propertyId, propName); + else printf(" \n", (unsigned)d.propertyId); + if (d.hasArrayIndex) { + printf(" \n", (unsigned)d.arrayIndex); + } + printf(" \n"); + } + } + printf(" \n"); + } + printf(" "); + for (uint32_t i = 0; i < frameLength; ++i) { + printf("%02X", frame[i]); + } + printf("\n \n"); +} + +} // namespace + +void CASExampleHelper::LogBacnetFrame(const uint8_t* frame, uint16_t frameLength) { + if (g_xmlFrameLoggingEnabled) { + PrintFrameAsXml(frame, frameLength); + return; + } + char summary[192]; + SummarizeBacnetFrame(frame, frameLength, summary, sizeof(summary)); + printf(" - %s\n", summary); +} + + namespace CASExampleHelper { void RequestRestart(const RestartKind kind, const uint32_t delayMilliseconds) { @@ -418,7 +1822,8 @@ void PrintHelp(const char* appName, const char* appVersion) { PrintInteractiveCommands(); } -bool HandleHelpAndVersionArgs(const int argc, char** argv, const char* appName, const char* appVersion) { +bool HandleHelpAndVersionArgs(const int argc, char** argv, const char* appName, const char* appVersion, + const bool showDccPasswordCliOption) { for (int i = 1; i < argc; ++i) { if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "/?") == 0) { @@ -435,6 +1840,16 @@ bool HandleHelpAndVersionArgs(const int argc, char** argv, const char* appName, printf(" --port BACnet/IP UDP port (1..65535). Default 47808 (0xBAC0).\n"); printf(" Use a non-default port to avoid clashing with another\n"); printf(" BACnet device already on 47808 on this host.\n"); + if (showDccPasswordCliOption) { + printf(" --dcc-password \n"); + printf(" DeviceCommunicationControl password. Default \"\" (no\n"); + printf(" password required). Set to require a matching password on\n"); + printf(" DeviceCommunicationControl requests - see this example's own\n"); + printf(" DeviceCommunicationControl callback for how it is checked.\n"); + } + printf(" --xml Print every RX/TX BACnet message as an indented XML\n"); + printf(" block instead of a one-line summary. Off by default -\n"); + printf(" very verbose, meant for deep protocol debugging.\n"); printf("\n"); PrintInteractiveCommands(); // version banner already printed above return true; @@ -447,6 +1862,15 @@ bool HandleHelpAndVersionArgs(const int argc, char** argv, const char* appName, return false; } +const char* ParseDccPasswordArg(const int argc, char** argv, const char* defaultPassword) { + for (int i = 1; i + 1 < argc; ++i) { + if (strcmp(argv[i], "--dcc-password") == 0) { + return argv[i + 1]; // argv's own storage outlives main() - safe to return directly + } + } + return defaultPassword; +} + // Parse a whole-number argument value. Returns true only if the ENTIRE token is // a valid non-negative integer. This exists because atoi()/atol() silently // return 0 on garbage - so `--deviceID abc` would parse as device 0, a valid @@ -552,6 +1976,10 @@ bool GetLocalIPv4(uint8_t ipAddress[4], uint8_t subnetMask[4]) { return GetPrimaryIPv4(ipAddress, subnetMask); } +bool GetLocalLinkSpeedBitsPerSecond(double* bitsPerSecond) { + return GetPrimaryLinkSpeedBitsPerSecond(bitsPerSecond); +} + void SendIAm(const uint32_t deviceInstance, const uint32_t networkPortInstance) { // Target the LOCAL subnet broadcast (the device's own network) rather than // the global 255.255.255.255 / network 0xFFFF. The broadcast is ip | ~mask @@ -607,6 +2035,7 @@ KeyCommand PollKey() { if (c == 'w' || c == 'W') return KeyCommand::WriteGroupDemo; if (c == 'd' || c == 'D') return KeyCommand::DiscoverRemote; if (c == 'r' || c == 'R') return KeyCommand::RouterAnnounce; + if (c == 'm' || c == 'M') return KeyCommand::Metrics; return KeyCommand::None; #else EnableRawInput(); @@ -626,6 +2055,7 @@ KeyCommand PollKey() { if (buf[0] == 'w' || buf[0] == 'W') return KeyCommand::WriteGroupDemo; if (buf[0] == 'd' || buf[0] == 'D') return KeyCommand::DiscoverRemote; if (buf[0] == 'r' || buf[0] == 'R') return KeyCommand::RouterAnnounce; + if (buf[0] == 'm' || buf[0] == 'M') return KeyCommand::Metrics; return KeyCommand::None; #endif } diff --git a/common/CASExampleHelper.h b/common/CASExampleHelper.h index 546f375..b84f804 100644 --- a/common/CASExampleHelper.h +++ b/common/CASExampleHelper.h @@ -32,6 +32,7 @@ // ============================================================================= #include +#include namespace CASExampleHelper { @@ -40,7 +41,7 @@ namespace CASExampleHelper { // version). Bump it whenever anything in common/ changes, and record the // change in common/CHANGELOG.md - every example in the series must then be // re-synced to the same common/ version. -static const char* COMMON_VERSION = "2.5.0"; +static const char* COMMON_VERSION = "3.0.0"; // Print the example's name + version, the linked CAS BACnet Stack version, // and the common/ helper version. @@ -61,7 +62,20 @@ void PrintHelp(const char* appName, const char* appVersion); // Returns true if it printed something and the caller should exit(0); false to // carry on starting up. Every example in the series supports --help/--version, // so this lives here rather than in each main.cpp. -bool HandleHelpAndVersionArgs(int argc, char** argv, const char* appName, const char* appVersion); +// +// showDccPasswordCliOption (added common/ 2.7.0, default true so every +// EXISTING call site - one positional argument short - keeps printing the +// "--dcc-password " line exactly as before): pass false when an +// example does NOT accept --dcc-password on the command line (e.g. +// BACnetProfileExample-B-SCHUB-CPP as of its 2026-09 secrets-handling pass, +// which accepts the DCC password only via its --config file - see that +// repo's README.md "Configuration file" section for why: a CLI argument is +// visible in process listings/shell history). This is a non-breaking, +// default-valued addition - it does not remove ParseDccPasswordArg() (still +// here, still usable by any example that still wants --dcc-password) or +// change any existing 4-argument call site's behaviour. +bool HandleHelpAndVersionArgs(int argc, char** argv, const char* appName, const char* appVersion, + bool showDccPasswordCliOption = true); // Return the UDP port to use: the value after "--port" if present, else // defaultPort. Common to every example. @@ -72,6 +86,17 @@ uint16_t ParsePortArg(int argc, char** argv, uint16_t defaultPort); // Common to every example. uint32_t ParseDeviceIdArg(int argc, char** argv, uint32_t defaultDeviceId); +// Return the DeviceCommunicationControl (and ReinitializeDevice) password to +// require: the value after "--dcc-password" if present, else defaultPassword. +// Returns a pointer into argv (the "--dcc-password" case) or defaultPassword +// itself (the not-given case) - same "caller does not own the returned +// storage" contract an example already relies on for its own DCC_PASSWORD +// constant; NOT a callback-owned buffer, so do not free() or modify it. +// defaultPassword is conventionally "" (no password required, today's +// behaviour for every example that does not opt in) - see main.cpp's +// DeviceCommunicationControl callback for how the result is used. +const char* ParseDccPasswordArg(int argc, char** argv, const char* defaultPassword); + // --- Networking ------------------------------------------------------------ // Bind the UDP socket used by the CURRENT Network Port instance (the one last // named by SetNetworkPortInstance(), or instance 1 if that was never called - @@ -141,6 +166,14 @@ void SendIAm(uint32_t deviceInstance, uint32_t networkPortInstance); // true on success; on failure the buffers are left untouched. bool GetLocalIPv4(uint8_t ipAddress[4], uint8_t subnetMask[4]); +// Get the primary network interface's negotiated link speed in bits per +// second - the value a Network Port object's Link_Speed property reports +// (REAL; 0.0 means "indeterminable" per Clause 12.56.15, so a caller that +// gets false back should report 0.0, not fabricate a number). Windows reads +// it via GetIfEntry(); POSIX reads /sys/class/net//speed. Returns +// true on success; on failure *bitsPerSecond is left untouched. +bool GetLocalLinkSpeedBitsPerSecond(double* bitsPerSecond); + // --- Deferred device restart (DM-RD-B) ------------------------------------- // Only for examples whose profile includes DM-RD-B (i.e. that register a // ReinitializeDevice callback). Most profiles in this series do not. @@ -197,6 +230,46 @@ void RequestRestart(RestartKind kind, uint32_t delayMilliseconds); // a second one on the next tick. bool RestartDue(RestartKind* outKind); +// --- Diagnostics ------------------------------------------------------------- +// Everything here is wired up automatically by the transport callbacks in +// CASExampleHelper.cpp - no example's main.cpp ever needs to call any of +// these directly. They exist in the header only so ParseXmlLogArg can be +// called once from main() at startup (the same pattern as ParsePortArg / +// ParseDeviceIdArg), and so SummarizeBacnetFrame is available if an example +// ever wants the one-line summary for something other than the RX/TX log. + +// Detects "--xml" / "--xmlLog" on the command line and turns on XML frame +// logging (see LogBacnetFrame) for the life of the process. Off by default - +// call this once in main(), alongside ParsePortArg/ParseDeviceIdArg: +// +// CASExampleHelper::ParseXmlLogArg(argc, argv); +void ParseXmlLogArg(int argc, char** argv); +bool IsXmlFrameLoggingEnabled(); + +// Decode just enough of a raw BACnet/IP datagram (the exact bytes handed to the +// transport callbacks - BVLC header + NPDU + APDU, as sent on the wire) to +// produce a short human-readable summary, e.g. "Unconfirmed: Who-Is", +// "ConfirmedRequest: ReadProperty Device 389001.Object_Name", "Reject: +// unrecognized-service". Falls back to "service=" / "property=" +// (matching the stack's own log wording) for anything not in the lookup +// tables, and to a short structural label ("Network-Layer-Message: ...", +// "BVLC function=0x..") for non-APDU frames. A trailing " DNET= +// [DADR=]" is appended whenever the NPDU carries a destination +// specifier (i.e. this is a routed frame). Never reads past frameLength or +// writes past outSummaryLength - a truncated or malformed frame degrades to +// "?" rather than being mis-parsed. +void SummarizeBacnetFrame(const uint8_t* frame, uint16_t frameLength, + char* outSummary, size_t outSummaryLength); + +// Prints the trailing part of an RX/TX log line for 'frame' (frameLength +// bytes), ending in a newline: either " - ", or, +// when XML frame logging is enabled, a full indented XML block instead (see +// ParseXmlLogArg). Called automatically by the transport callbacks after +// they print the leading "RX/TX bytes ... (Network Port N)" text (no +// trailing newline on that part) - this is what makes the XML option "off by +// default, and invisible to every example's main.cpp" at the same time. +void LogBacnetFrame(const uint8_t* frame, uint16_t frameLength); + // --- Keyboard input (common to every example) ------------------------------ enum class KeyCommand { None, // nothing pressed @@ -211,9 +284,16 @@ enum class KeyCommand { // triggers it normally DiscoverRemote, // 'd' - send a demo SendWhoIs to discover a remote device // this example writes to or reads from - RouterAnnounce // 'r' - manually (re-)send I-Am-Router-To-Network now, instead + RouterAnnounce, // 'r' - manually (re-)send I-Am-Router-To-Network now, instead // of waiting for the one sent at start-up, so routing // can be demonstrated on demand + Metrics // 'm' - print a health/metrics snapshot (uptime, connection + // counts, rate-limit rejections, RX/TX counters, etc.) + // to stdout. Added in common/ 2.7.0 for + // BACnetProfileExample-B-SCHUB-CPP's health/metrics + // keypress (Task 2); generic (not BACnet/SC-specific) + // so any example that later tracks its own + // connections/throughput can reuse it the same way. }; // Non-blocking: returns a pending key command, or None if nothing was pressed. diff --git a/common/CASExampleLog.cpp b/common/CASExampleLog.cpp new file mode 100644 index 0000000..864e017 --- /dev/null +++ b/common/CASExampleLog.cpp @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: CC0-1.0 +// Public-domain example code (CC0) - see ../LICENSE. +// CASExampleLog.cpp +// ============================================================================= +// Implementation of the shared example logging facility. See CASExampleLog.h. +// ============================================================================= + +#include "CASExampleLog.h" + +#include +#include +#include + +namespace { + +// Defaults to Info: Debug output is suppressed until an example explicitly +// calls CASExampleHelper::SetLogLevel(LogLevel::Debug) - see the header. +CASExampleHelper::LogLevel g_logLevel = CASExampleHelper::LogLevel::Info; + +const char* LevelName(const CASExampleHelper::LogLevel level) { + switch (level) { + case CASExampleHelper::LogLevel::Debug: return "DEBUG"; + case CASExampleHelper::LogLevel::Info: return "INFO"; + case CASExampleHelper::LogLevel::Warning: return "WARNING"; + case CASExampleHelper::LogLevel::Error: return "ERROR"; + default: return "UNKNOWN"; + } +} + +// A message this long already indicates something has gone wrong at the call +// site; vsnprintf still null-terminates and truncates cleanly within this, no +// heap allocation needed (same "small fixed buffer, no allocation" style as +// CASExampleHelper.cpp's own g_udpBindings table). +const size_t MAX_LOG_MESSAGE_LENGTH = 1024; + +} // namespace + +namespace CASExampleHelper { + +void SetLogLevel(const LogLevel level) { + g_logLevel = level; +} + +LogLevel GetLogLevel() { + return g_logLevel; +} + +void Log(const LogLevel level, const char* fmt, ...) { + if (static_cast(level) < static_cast(g_logLevel)) { + return; // below the current minimum - do not even format the message + } + + // UTC timestamp (YYYY-MM-DD HH:MM:SS), the same gmtime_s/gmtime_r pattern + // main.cpp already uses for the File objects' Modification_Date - UTC + // rather than localtime() so a log line means the same wall-clock instant + // regardless of the host's configured timezone (see main.cpp's + // GetPropertyDate/GetPropertyTime for the identical rationale). + const time_t now = time(NULL); + struct tm utc; +#if defined(_WIN32) + gmtime_s(&utc, &now); +#else + gmtime_r(&now, &utc); +#endif + char timestamp[32]; + snprintf(timestamp, sizeof(timestamp), "%04d-%02d-%02d %02d:%02d:%02d", + utc.tm_year + 1900, utc.tm_mon + 1, utc.tm_mday, + utc.tm_hour, utc.tm_min, utc.tm_sec); + + char message[MAX_LOG_MESSAGE_LENGTH]; + va_list args; + va_start(args, fmt); + vsnprintf(message, sizeof(message), fmt, args); + va_end(args); + + // Debug/Info -> stdout, Warning/Error -> stderr - see Log()'s own doc + // comment in CASExampleLog.h for why. + FILE* const stream = (level == LogLevel::Warning || level == LogLevel::Error) ? stderr : stdout; + fprintf(stream, "%s [%s] %s\n", timestamp, LevelName(level), message); +} + +} // namespace CASExampleHelper diff --git a/common/CASExampleLog.h b/common/CASExampleLog.h new file mode 100644 index 0000000..6112b03 --- /dev/null +++ b/common/CASExampleLog.h @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: CC0-1.0 +// Public-domain example code (CC0) - see ../LICENSE. +#ifndef CAS_EXAMPLE_LOG_H +#define CAS_EXAMPLE_LOG_H + +// CASExampleLog.h +// ============================================================================= +// A minimal, dependency-free structured logging facility for the example +// series, added in common/ 2.6.0. +// +// Every example today (this repo included) logs via bare printf()/ +// fprintf(stderr, ...) calls scattered through main.cpp and its own transport +// code - no levels, no timestamps, and no way to turn the noise up or down. +// This is a drop-in REPLACEMENT for that pattern, not a new subsystem: +// +// * No external logging library - just //, the same +// headers CASExampleHelper.cpp already includes. +// * No file output or log rotation - out of scope for this pass. Output +// still goes to stdout/stderr, exactly like the printf/fprintf calls it +// replaces (see Log()'s own comment below for which stream each level +// uses and why). +// * Printf-style formatting (const char* fmt, ...), so converting an +// existing call site is a small, mechanical edit: +// +// printf("Warning: ignoring invalid --port \"%s\"\n", argv[i + 1]); +// // becomes: +// CASExampleHelper::Log(CASExampleHelper::LogLevel::Warning, +// "ignoring invalid --port \"%s\"", argv[i + 1]); +// +// This commit adds the facility itself, working and proven to compile, plus a +// FEW demonstrative call sites in main.cpp (see that file). It deliberately +// does NOT sweep every existing printf/fprintf call in this series over to +// it - that is a large, separate, mechanical change with real regression risk +// (this repo's RX/TX log line format, startup banner, etc. are matched +// byte-for-byte by scripts in tests/sc/) - see common/CHANGELOG.md's 2.6.0 +// entry. Any later example in the series can adopt it the same way this one +// did: a #include and a handful of converted call sites, at its own pace. +// ============================================================================= + +namespace CASExampleHelper { + +// Severity, lowest to highest. Debug is for verbose diagnostic output (e.g. +// the rate-limiting/audit-trail work planned for this repo) that must stay +// silent during normal operation unless explicitly turned on - see +// SetLogLevel below. +enum class LogLevel { + Debug, + Info, + Warning, + Error +}; + +// The minimum level that actually gets printed; anything below it is +// suppressed before any formatting or I/O happens. Defaults to Info, so +// Debug output stays off unless an example opts in with +// SetLogLevel(LogLevel::Debug) - typically from its own --verbose/--debug +// command-line flag. +void SetLogLevel(LogLevel level); +LogLevel GetLogLevel(); + +// Print one log line - " [] \n" - if level is +// at or above the current minimum (GetLogLevel()); otherwise this is a no-op +// (the message is not even formatted). Printf-style formatting: fmt and the +// varargs after it are passed straight to vsnprintf, same rules as printf. +// +// Debug/Info go to stdout, Warning/Error go to stderr - the same split +// sc_transport/ScTransport.cpp and ScTransportRouter.cpp already use for +// their own printf (FYI/status) vs fprintf(stderr, ...) (problem) calls; +// common/'s OWN printf calls (e.g. ParsePortArg's "Warning: ..." lines) predate +// this facility and are unchanged by it - see common/CHANGELOG.md. +void Log(LogLevel level, const char* fmt, ...); + +} // namespace CASExampleHelper + +#endif // CAS_EXAMPLE_LOG_H diff --git a/common/CHANGELOG.md b/common/CHANGELOG.md index bb76c17..bd644c3 100644 --- a/common/CHANGELOG.md +++ b/common/CHANGELOG.md @@ -12,6 +12,233 @@ entry here, and must then be re-copied into **every** example in the series. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the folder adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.0.0] - 2026-09-22 + +### Added + +- **Reconciled two `common/` lines that had independently diverged from + 2.5.0 and both reused version numbers for different content**: + `BACnetProfileExample-B-BC-CPP` had carried its own 2.6.0-2.9.0 (RX/TX + service decode, object/property-naming `SummarizeBacnetFrame()`, the + `--xml` frame dump, `GetLocalLinkSpeedBitsPerSecond()`, and the Device + `Local_Date`/`Local_Time` property IDs) without those changes being + re-copied series-wide, while `BACnetProfileExample-B-SCHUB-CPP` separately + carried its own, differently-numbered 2.6.0-2.7.0 (the `CASExampleLog` + structured-logging facility, `ParseDccPasswordArg()`, the + `HandleHelpAndVersionArgs()` `showDccPasswordCliOption` parameter, and + `KeyCommand::Metrics`). The two lines touch disjoint code (confirmed by + diffing each against the shared 2.5.0 ancestor) and merge cleanly with no + functional conflicts. This version combines both: every symbol and file + from both lines is present, `COMMON_VERSION` moves to `3.0.0` (a new major + number precisely because this single version now carries two + independently-versioned change sets, not because anything is + backward-incompatible), and this is the version every example in the + series is re-synced to. + +## [2.7.0] - 2026-09-21 (from the BACnetProfileExample-B-SCHUB-CPP line, merged in 3.0.0) + +### Added + +- **`KeyCommand::Metrics` ('m'/'M')** - a generic "print a health/metrics + snapshot" interactive key command, added for + `BACnetProfileExample-B-SCHUB-CPP`'s health/metrics keypress (this batch's + Task 2: uptime, connection counts, rate-limit rejections, RX/TX counters). + `'h'`/`'q'`/arrows/`'s'`/`'w'`/`'d'`/`'r'` were already taken (see + `KeyCommand`'s own comment), so this is a new enum value, not a repurposed + one. `PollKey()` (both the Windows `_kbhit`/`_getch` branch and the POSIX + raw-input branch) now recognises `'m'`/`'M'` and returns it. This is a + purely additive, backward-compatible enum/switch change - existing + `switch (PollKey())` call sites elsewhere in the series that do not handle + `Metrics` are unaffected (an unhandled `default:`/no `case` simply does + nothing, same as any other key this repo's own `main.cpp` chooses not to + act on). Kept generic (not BACnet/SC-specific) so any other example that + later tracks its own connection/throughput counters can reuse the same key + rather than main.cpp inventing a parallel one. + +### Changed + +- **`HandleHelpAndVersionArgs()` gains an optional 5th parameter, + `showDccPasswordCliOption` (default `true`).** Added so + `BACnetProfileExample-B-SCHUB-CPP` can stop advertising `--dcc-password` in + its own `--help` output (this batch's Task 1: the CLI form of + `--dcc-password` was removed from that repo - config-file only now, since a + CLI argument is visible in process listings/shell history) without + affecting any other example's `--help` output. This is a **non-breaking** + change: every existing 4-argument call site (`HandleHelpAndVersionArgs(argc, + argv, APP_NAME, APP_VERSION)`) keeps compiling and keeps printing the + `--dcc-password` line exactly as before, because the new parameter defaults + to `true`. `ParseDccPasswordArg()` itself is UNCHANGED and NOT removed - + see that function's own entry in the 2.6.0 section below; any example + (including this one, previously) can still call it directly for a CLI + `--dcc-password` flag if it wants one. Evaluated and rejected: removing + `ParseDccPasswordArg()` outright, since (a) it was only added in 2.6.0 (this + same day) with no evidence any other example in the series has adopted it + yet, but (b) removing a just-published shared function the moment after + publishing it, rather than simply not using it in this one repo, is more + invasive than the problem calls for - see + `BACnetProfileExample-B-SCHUB-CPP`'s own `CHANGELOG.md` for the full + reasoning. + +## [2.6.0] - 2026-09-21 (from the BACnetProfileExample-B-SCHUB-CPP line, merged in 3.0.0) + +### Added + +- **`CASExampleLog.h` / `CASExampleLog.cpp` - a minimal, dependency-free + structured logging facility.** Every example in the series (this repo + included) has always logged via bare `printf()`/`fprintf(stderr, ...)` + calls scattered through `main.cpp` and its own transport code - no levels, + no timestamps, no way to turn the noise up or down. `CASExampleHelper::Log()` + is a drop-in replacement for that pattern, not a new subsystem: no external + logging library (just ``/``/``, already pulled in + elsewhere in `common/`), no file output or rotation (explicitly out of + scope for this pass), still stdout/stderr underneath. `LogLevel` - + `Debug`/`Info`/`Warning`/`Error` - matches the naming already used for + `CASExampleHelper::RestartKind`/`KeyCommand` in this same namespace. + `SetLogLevel()`/`GetLogLevel()` give a runtime-configurable minimum + (default `Info`, so `Debug` stays silent unless an example opts in) - this + is what the rate-limiting/audit-trail work planned for + `BACnetProfileExample-B-SCHUB-CPP` will use for verbose diagnostics without + spamming normal operation. `Log(level, fmt, ...)` is printf-style + (`const char* fmt, ...`) specifically to keep call-site conversions small: + an existing `printf("Warning: ...", x)` becomes + `CASExampleHelper::Log(CASExampleHelper::LogLevel::Warning, "...", x)` and + nothing else changes. Each line is ` [] `; + the timestamp uses the same `gmtime_s`/`gmtime_r` pattern + `BACnetProfileExample-B-SCHUB-CPP`'s own `main.cpp` already uses for its + File objects' `Modification_Date` (UTC, not `localtime()`, so a line means + the same instant regardless of the host's configured timezone). `Debug`/ + `Info` go to stdout, `Warning`/`Error` go to stderr, matching the split + `sc_transport/ScTransport.cpp`/`ScTransportRouter.cpp` already use for their + own `printf` (FYI/status) vs `fprintf(stderr, ...)` (problem) calls. + First consumer: `BACnetProfileExample-B-SCHUB-CPP`, which converts 3 + `main.cpp` call sites (the DeviceCommunicationControl password-failure + rejection, the "could not read a local IPv4 address" fallback, and the + BACnet/SC hub accept-URI failure) as a proof it compiles and works. This is + deliberately NOT a sweep of every `printf`/`fprintf` call in that repo - + that is a large, separate, mechanical change with real regression risk + (its RX/TX log line format, startup banner, etc. are read, though not + matched verbatim, by `tests/sc/*.py`) - left for a later, dedicated pass. + Any other example in the series can adopt the facility the same way, at its + own pace: a `#include "CASExampleLog.h"` and converting call sites as it + goes. +- **`ParseDccPasswordArg()` - `--dcc-password ` as a shared CLI + argument.** Every example that implements DM-DCC-B has, until now, hardcoded + its DeviceCommunicationControl password at compile time (e.g. + `BACnetProfileExample-B-SCHUB-CPP`'s `main.cpp` had + `static const char* DCC_PASSWORD = "";`) - there was no way to set or test a + non-empty password without rebuilding. `ParseDccPasswordArg(argc, argv, + defaultPassword)` follows the exact same shape as `ParsePortArg`/ + `ParseDeviceIdArg`: scan `argv` for the flag, return the following token if + present, else `defaultPassword`. It differs from those two only in return + type - `const char*` into `argv`'s own storage, not a parsed numeric value - + because a password is carried through verbatim, not converted; `argv` + outlives `main()`, so returning a pointer into it is safe, and the contract + matches an example's own (now non-`static const`) `DCC_PASSWORD`-equivalent + global. Default `""` (no password required) preserves today's behaviour for + every example that does not opt in - nothing that does not call this + function changes. Added to `HandleHelpAndVersionArgs()`'s `--help` output, + in the same option-list style as `--port`/`--deviceID`, so it is + discoverable series-wide once adopted. First consumer: + `BACnetProfileExample-B-SCHUB-CPP`, which parses it alongside `--port`/ + `--deviceID` in `main()` and renamed its own constant to `g_dccPassword` + (no longer `static const`, since it is now assigned at start-up) feeding the + existing `DeviceCommunicationControl` callback's password check - unchanged + otherwise. + +## [2.9.0] - 2026-09-17 (from the BACnetProfileExample-B-BC-CPP line, merged in 3.0.0) + +### Added + +- `CASExampleHelper::GetLocalLinkSpeedBitsPerSecond()` - reads the primary + network interface's actual negotiated link speed (Windows: `GetIfEntry()`; + POSIX: `/sys/class/net//speed`), for a Network Port object's + `Link_Speed` property. First consumer: `BACnetProfileExample-B-BC-CPP`, + whose `Link_Speed` previously read back a hardcoded `0.0` because nothing + served it - the stack's "no callback answered" REAL-property fallback, + not a real "indeterminable" answer. `PROPERTY_IDENTIFIER_LINK_SPEED = 420` + added to `CASBACnetStackExampleConstants.h` alongside it. + +## [2.8.0] - 2026-09-17 (from the BACnetProfileExample-B-BC-CPP line, merged in 3.0.0) + +### Added + +- `PROPERTY_IDENTIFIER_LOCAL_DATE = 56` and `PROPERTY_IDENTIFIER_LOCAL_TIME = 57` + in `CASBACnetStackExampleConstants.h`. First consumer: + `BACnetProfileExample-B-BC-CPP` (chipkin/BACnetProfileExample-B-BC-CPP#7) - + a device claiming DM-TS-B/DM-UTC-B needs to actually serve the Device's + `Local_Date`/`Local_Time` (the properties a client reads back to confirm a + time sync took), not leave the stack's "callback declined, no default" + `read-access-denied` fallback in place. + +## [2.7.0] - 2026-09-17 (from the BACnetProfileExample-B-BC-CPP line, merged in 3.0.0) + +### Added + +- **RX/TX log lines now name the object/property being requested, and any + NPDU routing destination**, e.g.: + `RX 17 bytes from ... (Network Port 1) - ConfirmedRequest: ReadProperty Device 389001.Device_Address_Binding` + `RX 18 bytes from ... (Network Port 1) - ConfirmedRequest: ReadProperty Analog_Input 1.Present_Value DNET=1234 DADR=0A0B0C` + `SummarizeBacnetFrame()` decodes the shared ObjectIdentifier(tag0) + + PropertyIdentifier(tag1) + optional PropertyArrayIndex(tag2) layout common + to ReadProperty-Request, ReadProperty-ACK and WriteProperty-Request, using + two new lookup tables (`ObjectTypeName()`, `PropertyName()` - 65 object + types, 522 properties, generated from the pinned stack's own + `BACnetObjectType.h`/`BACnetPropertyIdentifier.h`, so the names are exactly + what this stack build uses, not hand-transcribed from the spec). Falls back + to `object-type= .property=` for anything outside the + tables. A trailing ` DNET=[ DADR=]` is appended whenever the NPDU + carries a destination specifier, regardless of PDU type. + Other confirmed services (ReadPropertyMultiple, WritePropertyMultiple, ...) + are intentionally NOT decoded this deeply - their nested + read-access-specification/property-reference structure reuses the same + tag numbers at a different nesting level, which this generic tag-walker + doesn't disambiguate. Those still print with just the service name, same + as before. +- **New `--xml` / `--xmlLog` command-line option (off by default).** When + set, every RX/TX frame prints as a full indented XML block (BVLC function, + NPDU version/control/routing, APDU type/invoke-id/service/object/property, + and always the complete raw hex of the frame as a fallback) instead of the + one-line summary - for deep protocol debugging sessions where the one-line + summary isn't enough. `CASExampleHelper::ParseXmlLogArg(argc, argv)` is the + only thing an example's `main()` needs to call (same pattern as + `ParsePortArg`/`ParseDeviceIdArg`); everything else - the flag itself, the + decode, the XML rendering - lives entirely in `common/`, invisible to + `main.cpp`. +- New shared internals backing both of the above: a generic BACnet tag-header + decoder (`DecodeTagHeader`, handles extended tag numbers and all four + length/value/type encodings including the 1/2/4-byte extended-length + escapes) and one `DecodeBacnetFrame()` parse pass that both + `SummarizeBacnetFrame()` and the new XML dump build their output from - the + two views can never disagree about what a frame contains, because they're + reading the same `DecodedFrame` struct. +- Both the object/property table generation and the on-disk lookup tables are + regenerable if the stack pin changes: `python3 -c "..."` extracts every + `name = value,` line from `BACnetObjectType.h`/`BACnetPropertyIdentifier.h`, + converts camelCase to `Title_Case`, sorts by value, and emits `{ id, "name" },` + rows - see this entry's originating commit for the exact script. + +## [2.6.0] - 2026-09-17 (from the BACnetProfileExample-B-BC-CPP line, merged in 3.0.0) + +### Added + +- **RX/TX log lines now decode and print the BACnet service**, e.g. + `RX 21 bytes from ... (Network Port 1) - Unconfirmed: I-Am` or + `RX 17 bytes from ... (Network Port 1) - ConfirmedRequest: ReadProperty`, + instead of stopping at the byte count and Network Port. New + `CASExampleHelper::SummarizeBacnetFrame()` walks the raw BVLC + NPDU + APDU + bytes handed to the transport callbacks (the exact wire bytes - the stack + decodes them again itself; this is a read-only, best-effort peek purely for + the console log) far enough to name the PDU type (ConfirmedRequest / + Unconfirmed / SimpleACK / ComplexACK / SegmentACK / Error / Reject / Abort / + a network-layer message) and, where applicable, the Clause 21 + confirmed/unconfirmed service choice or reject/abort reason - falling back + to `service=` / `reason=` (matching the stack's own log wording) for + anything outside the lookup tables, and to `?` for a truncated/malformed + frame rather than misreading it. Handles the DNET/SNET/hop-count routing + fields in the NPDU so this doesn't mis-decode a routed frame. + First landed in `BACnetProfileExample-B-SS-CPP`; every sibling example + re-syncs to this `common/` version to pick it up. + ## [2.5.0] - 2026-09-15 ### Added diff --git a/common/README.md b/common/README.md index 6b9e31a..5ae9380 100644 --- a/common/README.md +++ b/common/README.md @@ -22,7 +22,8 @@ example repository. | File | Purpose | |------|---------| -| `CASExampleHelper.h` / `.cpp` | UDP socket on the BACnet/IP port + the transport/time callbacks; version/help printing; `--port` / `--deviceID` parsing; keyboard commands (h/q/up/down); local-IPv4 / broadcast helpers; broadcast I-Am on start-up. | +| `CASExampleHelper.h` / `.cpp` | UDP socket on the BACnet/IP port + the transport/time callbacks; version/help printing; `--port` / `--deviceID` / `--dcc-password` parsing; RX/TX frame summarizing and optional `--xml` frame dump; keyboard commands (h/q/up/down); local-IPv4 / broadcast / link-speed helpers; broadcast I-Am on start-up. | +| `CASExampleLog.h` / `.cpp` | A minimal, dependency-free structured logging facility (`Debug`/`Info`/`Warning`/`Error`, timestamped, runtime-configurable minimum level) - a drop-in replacement for bare `printf`/`fprintf(stderr, ...)` calls. | | `SimpleUDP.h` / `.cpp` | A tiny cross-platform UDP socket wrapper (Winsock on Windows, BSD sockets on Linux/macOS). | | `CASBACnetStackExampleConstants.h` | A self-contained, series-wide copy of the BACnet enumeration values the examples use (each section names the CAS BACnet Stack header that defines the full enumeration). Identical in every example — the union of what the series needs. | | `CHANGELOG.md` | The changelog of this folder itself (see Versioning above). | diff --git a/docs/PICS.md b/docs/PICS.md index 5e7a814..33a9826 100644 --- a/docs/PICS.md +++ b/docs/PICS.md @@ -94,7 +94,7 @@ No object is dynamically creatable or deletable. | Object type | Instance | Object_Name | Writable | Optional properties supported | |---|:---:|---|:---:|---| -| Device | 389018 | Rainbow | no | Description | +| Device | 389018 | Chipkin Example B-RTR | no | Description | | Analog Input | 1 | Bronze | no | - | | Binary Input | 1 | Emerald | no | - | | Multi-State Input | 1 | Hot Pink | no | State_Text | @@ -176,7 +176,7 @@ handle data in all character sets. Every object this example creates, and every REQUIRED property of each (per ANSI/ASHRAE 135-2024 clause 12 and the stack's `docs/property-profile-reference.md`), plus the optional properties the example turns on. **Served by** says who answers a ReadProperty: the **stack** generates it, or the **app** serves it from a `GetProperty*` callback in `main.cpp`. A ⚠ row is a required property the app does not serve and the stack would fill with a default - that is a defect, not a feature. -### Device 389018 "Rainbow" - vendor 389 (Chipkin Automation Systems); instance configurable with --deviceID. The stack rows are device-wide facts only the stack knows - the protocol version and revision it implements, the services and object types it was configured with, the live object list and address-binding table. The accepted rows are the stack's configured defaults for APDU limits, segmentation, system status and database revision; an application that answered them from its own constants could contradict the stack, so this example does not +### Device 389018 "Chipkin Example B-RTR" - vendor 389 (Chipkin Automation Systems); instance configurable with --deviceID. The stack rows are device-wide facts only the stack knows - the protocol version and revision it implements, the services and object types it was configured with, the live object list and address-binding table. The accepted rows are the stack's configured defaults for APDU limits, segmentation, system status and database revision; an application that answered them from its own constants could contradict the stack, so this example does not | Property | Datatype | Served by | Writable | |---|---|---|:---:| diff --git a/docs/objects.json b/docs/objects.json index aab79d4..5973c57 100644 --- a/docs/objects.json +++ b/docs/objects.json @@ -3,7 +3,7 @@ { "type": "Device", "instance": 389018, - "name": "Rainbow", + "name": "Chipkin Example B-RTR", "app": ["Object_Name", "Vendor_Name", "Vendor_Identifier", "Model_Name", "Firmware_Revision", "Application_Software_Version", "Description"], "enabled": ["Description"], diff --git a/main.cpp b/main.cpp index 7cd63f1..67e20d9 100644 --- a/main.cpp +++ b/main.cpp @@ -32,7 +32,7 @@ // inputs and three commandable outputs a client drives with WriteProperty - PLUS // a SECOND Network Port object, one per side of the router: // -// Device 389018 "Rainbow" (instance configurable with --deviceID) +// Device 389018 "Chipkin Example B-RTR" (instance configurable with --deviceID) // Analog Input 1 "Bronze" (REAL, degrees Celsius; read-only) // Binary Input 1 "Emerald" (active / inactive; read-only) // Multi-State Input 1 "Hot Pink" (state 1..3; read-only) @@ -86,6 +86,7 @@ #include #include +#include #include #include @@ -101,7 +102,7 @@ using namespace CASBACnetStackExampleConstants; // 1. Example + device configuration // ----------------------------------------------------------------------------- static const char* APP_NAME = "BACnet B-RTR (Router) Example - C++"; -static const char* APP_VERSION = "1.0.0"; +static const char* APP_VERSION = "1.0.2"; // The device instance. BACnet requires this to be configurable, so it defaults // to 389018 (docs/colour-table.md) and can be overridden with --deviceID. @@ -129,7 +130,7 @@ static const uint32_t VENDOR_IDENTIFIER = 389; // a real product: a real device needs a name that is unique PER UNIT, not per // firmware build - derive it from a serial number, DIP switches, a config // file, or a --deviceName command-line argument, not a hardcoded string. -static const char* DEVICE_NAME = "Rainbow"; +static const char* DEVICE_NAME = "Chipkin Example B-RTR"; // Change to a description of what YOUR device actually is. static const char* DEVICE_DESCRIPTION = @@ -144,10 +145,11 @@ static const char* VENDOR_NAME = "Chipkin Automation Systems"; // Change to your product's own model designation. static const char* MODEL_NAME = "CAS BACnet Stack Example - B-RTR"; -// Change both to your product's REAL firmware / application software -// versions - these are not the example's own version (APP_VERSION above). -static const char* FIRMWARE_REVISION = "1.0.0"; -static const char* APPLICATION_SOFTWARE_VERSION = "1.0.0"; +// Firmware_Revision reflects the underlying CAS BACnet Stack's REAL version, +// built at runtime (once LoadBACnetFunctions() has loaded the stack DLL) from +// BACnetStack_GetAPI{Major,Minor,Patch,Build}Version() - see main() below. +// Application_Software_Version is this example's own version: APP_VERSION above. +static std::string g_firmwareRevision; // The sensor objects (all instance 1) and their colour names - unchanged from // B-SA/B-ASC. @@ -587,9 +589,9 @@ bool GetPropertyCharString(const uint32_t deviceInstance, const uint16_t objectT case PROPERTY_IDENTIFIER_MODEL_NAME: return ReturnCharacterString(MODEL_NAME, value, valueElementCount, maxElementCount, encodingType); case PROPERTY_IDENTIFIER_FIRMWARE_REVISION: - return ReturnCharacterString(FIRMWARE_REVISION, value, valueElementCount, maxElementCount, encodingType); + return ReturnCharacterString(g_firmwareRevision.c_str(), value, valueElementCount, maxElementCount, encodingType); case PROPERTY_IDENTIFIER_APPLICATION_SOFTWARE_VERSION: - return ReturnCharacterString(APPLICATION_SOFTWARE_VERSION, value, valueElementCount, maxElementCount, encodingType); + return ReturnCharacterString(APP_VERSION, value, valueElementCount, maxElementCount, encodingType); default: break; } @@ -757,6 +759,14 @@ int main(int argc, char** argv) { return 1; } + { + char buf[32]; + snprintf(buf, sizeof(buf), "%u.%u.%u.%u", + BACnetStack_GetAPIMajorVersion(), BACnetStack_GetAPIMinorVersion(), + BACnetStack_GetAPIPatchVersion(), BACnetStack_GetAPIBuildVersion()); + g_firmwareRevision = buf; + } + if (CASExampleHelper::HandleHelpAndVersionArgs(argc, argv, APP_NAME, APP_VERSION)) { return 0; }