-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.cpp
More file actions
2186 lines (2086 loc) · 119 KB
/
Copy pathmain.cpp
File metadata and controls
2186 lines (2086 loc) · 119 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// SPDX-License-Identifier: CC0-1.0
// Public-domain example code (CC0) - see LICENSE. The CAS BACnet Stack itself is
// a separate, commercially licensed product and is not covered by CC0.
// =============================================================================
// BACnet Profile Example - B-BC (BACnet Building Controller) - C++
//
// This is the series CAPSTONE: the B-BC (Building Controller, ANSI/ASHRAE 135,
// Annex L.4.1) profile is the largest single controller profile in the series,
// combining nearly every shared feature already proven by the sibling examples
// (B-SA outputs, B-ASC device-communication-control, B-AAC alarming/scheduling/
// time-sync/reinit, B-ACC backup and restore, B-LS external-write scheduling)
// plus one genuinely new capability this example DEFINES for the series:
// trending (Trend Log + Trend Log Multiple + ReadRange).
//
// A B-BC must support:
//
// DS-RP-A,B, DS-RPM-A,B - ReadProperty (+ initiate) + ReadPropertyMultiple,
// DS-WP-A,B, DS-WPM-B - WriteProperty (+ initiate) + WritePropertyMultiple,
// AE-N-I-B - generate intrinsic alarm/event notifications,
// AE-ACK-B - accept AcknowledgeAlarm,
// AE-INFO-B - answer GetEventInformation,
// AE-CRL-B - configurable, writable event-recipient list,
// SCHED-E-B - a Schedule that drives a write, including to a
// remote device's object (external scheduling),
// T-VMT-I-B, T-ATR-B - Trend Log + Trend Log Multiple, polled logging,
// and ReadRange to retrieve the logged records,
// DM-DDB-A,B, DM-DOB-B - Who-Is/I-Am (answer + initiate), Who-Has/I-Have,
// DM-DCC-B - DeviceCommunicationControl,
// DM-TS-B / DM-UTC-B - TimeSynchronization / UTCTimeSynchronization,
// DM-RD-B - ReinitializeDevice,
// DM-BR-B - Backup and Restore (AtomicReadFile/WriteFile
// against a File object, driven by ReinitializeDevice).
//
// Deliberately OMITTED: AE-ESUM-B (GetAlarmSummary) is not required at or above
// Protocol_Revision 13, so it is not implemented here.
//
// WHAT IS NOT IMPLEMENTED (see README.md "What this example does NOT do" + TODO.md):
// - Calendar 1 "Cream"'s Date_List: there is no customer-facing export or
// callback to populate a Calendar object's Date_List (cas-bacnet-stack
// issue #963), so Schedule 1 "Saffron"'s one-off exception uses an inline
// calendar-date entry rather than a reference to Cream. Inherited from every
// prior example that carries a Calendar (B-AAC, B-ACC, B-LS).
//
// The device keeps the full B-AAC object set (three read-only inputs, three
// commandable outputs, the alarm-capable Analog Value + Notification Class,
// Schedule + Calendar, Network Port) and ADDS a File object (backup/restore),
// a Trend Log and a Trend Log Multiple. Each object has a colour name (the
// convention shared across this example series):
//
// Device 389005 "Rainbow" (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)
// Analog Output 1 "Chartreuse" (REAL setpoint; WRITABLE, commandable)
// Binary Output 1 "Fuchsia" (active / inactive; WRITABLE, commandable)
// Multi-State Output 1 "Indigo" (state 1..3; WRITABLE, commandable)
// Analog Value 1 "Diamond" (REAL, WRITABLE; intrinsic OutOfRange alarm)
// Notification Class 1 "Crimson" (routes Diamond's alarms; Recipient_List WRITABLE)
// Network Port 1 "Vermilion" (the BACnet/IP port - required)
// Schedule 1 "Saffron" (drives Chartreuse on a weekly + exception basis,
// and a remote peer object - SCHED-E-B)
// Calendar 1 "Cream" (see TODO.md - Date_List not evaluated)
// File 1 "Ivory" (backup/restore payload - DM-BR-B)
// Trend Log 1 "Lilac" (polled log of Analog Input 1 "Bronze")
// Trend Log Multiple 1 "Magenta" (polled log of several points)
//
// Output objects are COMMANDABLE: their Present_Value is driven by a 16-slot
// BACnet Priority_Array. A WriteProperty(Present_Value, value, priority) sets a
// slot; writing NULL relinquishes it; the stack reports the highest-priority
// non-null slot (or Relinquish_Default) as the effective Present_Value.
//
// ALARMING: Analog Value 1 ("Diamond") has an intrinsic OutOfRange event algorithm
// with a low/high limit. When its Present_Value crosses a limit, the stack sends an
// UnconfirmedEventNotification to the recipients of Notification Class 1 ("Crimson")
// (unconfirmed + broadcast by default - see the recipient note in main). Drive
// Diamond out of range with a WriteProperty to its Present_Value to see it.
//
// To be a conformant BACnet device (Protocol_Revision 24) each object must
// expose its full set of REQUIRED properties. Most are generated by the stack
// (Object_Identifier, Object_Type, Status_Flags, Object_List, Protocol_*).
// Event_State is subtle here: for the objects with NO alarming it just reads its
// datatype default of normal(0) (correct by coincidence, not computed). But this
// example ARMS an intrinsic OutOfRange algorithm on the Analog Value "Diamond"
// below, and for that object the stack genuinely COMPUTES Event_State from the
// algorithm (normal / high-limit / low-limit). The handful that the application
// must supply are served by the
// Get*Property callbacks below, and a few are turned on with SetPropertyEnabled.
//
// Interactive keys (handled by the shared helper): h = help, q = quit,
// up/down = nudge Analog Input 1 by +/-1.1, s = advance Schedule 1 ("Saffron")
// with a Weekly_Schedule transition for right now. To fire an alarm, WriteProperty
// the Analog Value's Present_Value above 90 or below 10. Command line: --port <n>,
// --deviceID <n>.
//
// All the UDP/stack plumbing lives in common/CASExampleHelper so this file can
// stay focused on the BACnet logic.
// =============================================================================
#include "CASExampleHelper.h"
#include "CASBACnetStackExampleConstants.h"
#include "CASBACnetStackAdapter.h" // the CAS BACnet Stack C API (BACnetStack_*); call
// LoadBACnetFunctions() before any BACnetStack_* call -
// see the top of main() below.
#include <stdio.h>
#include <string.h>
#include <time.h> // time(), localtime_[sr]() - the SCHED-I-B demo-advance key (see KeyCommand::DemoAdvance)
#if defined(_WIN32)
#include <windows.h> // Sleep()
#else
#include <unistd.h> // usleep()
#endif
using namespace CASBACnetStackExampleConstants;
// -----------------------------------------------------------------------------
// 1. Example + device configuration
// -----------------------------------------------------------------------------
static const char* APP_NAME = "BACnet B-BC (Building Controller) Example - C++";
static const char* APP_VERSION = "1.0.2";
// The device instance. BACnet requires this to be configurable, so it defaults
// to 389005 and can be overridden on the command line with --deviceID. Keep it
// configurable in your product: it must be unique across the internetwork.
static uint32_t g_deviceInstance = 389005;
// ---- Device identity: CHANGE ALL OF THIS BEFORE YOU SHIP --------------------
// Everything in this block is read by clients and shown to the operator in every
// discovery tool on the network. Left as-is, your product will appear on a real
// site announcing itself as a Chipkin demo. None of it is cosmetic:
// Object_Name must be unique across the BACnet internetwork, and Model_Name /
// Vendor_Identifier are what a building operator uses to identify your device.
//
// This block is the ship checklist. Every constant below has a note saying what
// to change it to; nothing here is safe to leave at its example value.
// -----------------------------------------------------------------------------
// Your BACnet Vendor Identifier. 389 = Chipkin Automation Systems; change this
// to YOUR company's vendor ID before shipping a product. Vendor IDs are assigned
// by ASHRAE - request one (free) at https://bacnet.org/assigned-vendor-ids/.
// Update VENDOR_NAME below to match.
static const uint32_t VENDOR_IDENTIFIER = 389;
// The Device object's Object_Name.
//
// THIS IS THE ONE THAT WILL BITE YOU. Object_Name must be unique across the
// whole BACnet internetwork, and here it is a COMPILE-TIME constant. The device
// instance is runtime-configurable via --deviceID, so it is easy to ship two
// units, configure their instances correctly, and still have BOTH announce
// Object_Name "Rainbow" - a spec violation, and a hard BTL failure. In a real
// product Object_Name must be per-unit configurable too: derive it from a serial
// number, DIP switches, a config file, or add a --deviceName argument.
static const char* DEVICE_NAME = "Rainbow";
// The Device object's Description. Change it to what YOUR device actually is;
// this string describes this tutorial.
// Kept well under 256 chars (STACK_OPTION_MAX_CHARACTER_STRING_SIZE on a full
// build) - see the ReturnCharacterString() comment below for why "truncate to
// fit" is NOT a safe fallback for this string: reading a Description longer
// than the stack's limit aborts the whole read rather than returning a
// shortened string (chipkin/BACnetProfileExample-B-BC-CPP#7). If you lengthen
// this, check the new length against that limit for real, on the target you
// actually build for.
static const char* DEVICE_DESCRIPTION =
"Chipkin CAS BACnet Stack example - B-BC (Building Controller) profile, "
"the series capstone. Demonstrates DS-RP/RPM/WP/WPM-A/B, intrinsic "
"alarming, SCHED-E-B, trending, DM-DCC-B, DM-TS-B/UTC-B, DM-RD-B, and "
"DM-BR-B.";
// Device identity strings (read by clients, and used to populate I-Am).
// VENDOR_NAME - your company name; it must match VENDOR_IDENTIFIER above.
// MODEL_NAME - your model designation. This is what a building operator reads
// to identify your device in a discovery tool.
static const char* VENDOR_NAME = "Chipkin Automation Systems";
static const char* MODEL_NAME = "CAS BACnet Stack Example - B-BC";
// DeviceCommunicationControl password. A management station may include a password
// with a DeviceCommunicationControl (or ReinitializeDevice) request; the device
// accepts the command only if it matches. Set to NULL/empty to accept any request
// (no password required). Change this to your device's secret before shipping -
// it crosses the wire in PLAINTEXT, so treat it as a guard against accidents,
// not a security boundary.
static const char* DCC_PASSWORD = ""; // "" = no password required
// FIRMWARE_REVISION / APPLICATION_SOFTWARE_VERSION - your real versions. Wire
// them to your build rather than hard-coding a number that will go stale.
static const char* FIRMWARE_REVISION = "1.0.0";
static const char* APPLICATION_SOFTWARE_VERSION = "1.0.0";
// The sensor objects (all instance 1) and their colour names.
static const uint32_t ANALOG_INPUT_INSTANCE = 1; // "Bronze"
static const uint32_t BINARY_INPUT_INSTANCE = 1; // "Emerald"
static const uint32_t MULTI_STATE_INPUT_INSTANCE = 1; // "Hot Pink"
static const uint32_t MULTI_STATE_INPUT_NUMBER_OF_STATES = 3;
// The Network Port object - every BACnet device must have one. It represents
// the BACnet/IP port this device communicates on.
static const uint32_t NETWORK_PORT_INSTANCE = 1; // "Vermilion"
static const uint32_t MAX_APDU_LENGTH = 1476; // BACnet/IP APDU length
// BACnet/IP addressing the Network Port reports. The IP address and subnet mask
// are filled in at start-up from the host's primary interface; the gateway is
// left unset (0.0.0.0) for this example. The stack also uses IP_Address +
// BACnet_IP_UDP_Port to build the port's MAC_Address automatically.
static uint8_t g_ipAddress[4] = { 0, 0, 0, 0 };
static uint8_t g_ipSubnetMask[4] = { 0, 0, 0, 0 };
static uint8_t g_ipDefaultGateway[4] = { 0, 0, 0, 0 };
static uint16_t g_bacnetIpUdpPort = 47808;
// Network Port's Link_Speed (REAL, bits/sec; 0.0 means "indeterminable" per
// Clause 12.56.15). Read once at start-up, same as the IP addressing above -
// this example's link speed does not change at runtime, and re-querying it
// on every poll would be pointless work in the Tick loop for a value that
// never changes. Genuinely 0.0 (not just uninitialized) is a valid, honest
// answer if CASExampleHelper::GetLocalLinkSpeedBitsPerSecond() can't
// determine it - it is NOT this example fabricating a number either way.
static float g_linkSpeedBitsPerSecond = 0.0f;
// Analog Input 1's live present value (degrees Celsius). Starts at 21.5 and is
// nudged by the up/down arrow keys. A real sensor would update this from
// hardware instead.
static float g_analogInput1Value = 21.5f;
// The commandable OUTPUT objects (all instance 1) and their colour names. These
// are what make this a B-SA actuator: clients drive them with WriteProperty.
static const uint32_t ANALOG_OUTPUT_INSTANCE = 1; // "Chartreuse"
static const uint32_t BINARY_OUTPUT_INSTANCE = 1; // "Fuchsia"
static const uint32_t MULTI_STATE_OUTPUT_INSTANCE = 1; // "Indigo"
static const uint32_t MULTI_STATE_OUTPUT_NUMBER_OF_STATES = 3;
static const uint32_t BACNET_PRIORITY_ARRAY_SIZE = 16;
// A BACnet commandable value: a 16-slot Priority_Array plus a Relinquish_Default.
// Each slot is either null (relinquished) or holds a commanded value. A real
// device would map the resolved Present_Value onto its physical output; here we
// just store the commands. The values are kept as double and cast per object
// type (REAL for AO, 0/1 for BO, state number for MSO).
struct Commandable {
bool isSet[16]; // is slot i (1..16) commanded?
double value[16]; // the commanded value at slot i
double relinquishDefault; // used when every slot is null
};
// The { { false }, { 0 }, default } initializer zero-fills all 16 slots of isSet
// and value (C++ aggregate rules: the remaining elements are value-initialized),
// so every priority slot starts null and Present_Value reports relinquishDefault.
static Commandable g_analogOutput = { { false }, { 0 }, 20.0 }; // setpoint, default 20.0 C
static Commandable g_binaryOutput = { { false }, { 0 }, 0.0 }; // default inactive (0)
static Commandable g_multiStateOutput = { { false }, { 0 }, 1.0 }; // default state 1
// --- The alarm-capable Analog Value + its Notification Class (the B-AAC additions)
// Analog Value 1 "Diamond" carries an intrinsic OutOfRange event algorithm. When
// its Present_Value leaves [LOW_LIMIT, HIGH_LIMIT] for longer than the time delay,
// the stack fires an EventNotification to Notification Class 1 "Crimson"'s recipients.
static const uint32_t ANALOG_VALUE_INSTANCE = 1; // "Diamond"
static float g_analogValue1Value = 50.0f; // a process value (%)
static const float ANALOG_VALUE_LOW_LIMIT = 10.0f;
static const float ANALOG_VALUE_HIGH_LIMIT = 90.0f;
static const float ANALOG_VALUE_DEADBAND = 2.0f; // hysteresis returning to normal
static const uint32_t ANALOG_VALUE_TIME_DELAY = 0; // seconds the limit must hold
static const uint32_t NOTIFICATION_CLASS_INSTANCE = 1; // "Crimson"
// Notification priorities for the three transitions (lower = more urgent). The
// to-fault priority is supplied for completeness, but this example's OutOfRange
// algorithm has no fault source, so to-fault is left disabled below.
static const uint8_t NC_PRIORITY_TO_OFFNORMAL = 100;
static const uint8_t NC_PRIORITY_TO_FAULT = 50;
static const uint8_t NC_PRIORITY_TO_NORMAL = 200;
// Where Diamond's alarms are sent. This is the AE-CRL-B recipient list (AddRecipientToNotificationClass
// seeds it at start-up; Recipient_List is also registered WRITABLE below, so a management station can
// redirect it at run time - see BACnetStack_SetPropertyWritable(..., PROPERTY_IDENTIFIER_RECIPIENT_LIST, true)).
//
// A recipient can be named two ways: by DEVICE instance (the stack resolves the address itself, via its
// Device-Address-Binding cache and a Who-Is heartbeat) or by ADDRESS. This example seeds the ADDRESS
// form, defaulting to the LOCAL SUBNET BROADCAST with UNCONFIRMED notifications, so any BACnet client on
// the subnet sees Diamond's alarms without us knowing its address ahead of time. For a single known
// client, set RECIPIENT_USE_BROADCAST = false and fill in RECIPIENT_IP[]. A client that WriteProperty's
// Recipient_List with a device-instance recipient instead works too: the stack's Acquire()/Resolve() DAB
// path (cas-bacnet-stack issue #1328) chases it with Who-Is and starts delivering once it resolves.
static const uint32_t RECIPIENT_PROCESS_IDENTIFIER = 1;
static const bool RECIPIENT_USE_BROADCAST = true;
static uint8_t RECIPIENT_IP[4] = { 0, 0, 0, 0 }; // used when not broadcasting
// --- SCHED-I-B: Schedule 1 "Saffron" drives Analog Output 1 (Chartreuse) -------
// A weekly transition sets Chartreuse to SCHEDULE_DEMO_VALUE; outside any scheduled
// window Schedule_Default applies instead. Calendar 1 "Cream" exists as a readable
// object alongside the exception (see TODO.md for why it is not wired to the
// exception's period - cas-bacnet-stack issue #963).
static const uint32_t SCHEDULE_INSTANCE = 1; // "Saffron"
static const uint32_t CALENDAR_INSTANCE = 1; // "Cream"
static const uint8_t SCHEDULE_WRITE_PRIORITY = 8; // mid-range: below manual overrides at 1-7
static const float SCHEDULE_DEFAULT_VALUE = 20.0f; // Chartreuse's steady-state setpoint
static const float SCHEDULE_DEMO_VALUE = 75.0f; // the value a scheduled/demo transition applies
static const float SCHEDULE_EXCEPTION_VALUE = 5.0f; // the value the one-off exception applies
// BACnet object type / property identifier numbers not already in
// CASBACnetStackExampleConstants.h (verified against BACnetObjectType.h /
// BACnetPropertyIdentifier.h at the pin).
static const uint16_t OBJECT_TYPE_SCHEDULE = 17;
static const uint16_t OBJECT_TYPE_CALENDAR = 6;
static const uint32_t PROPERTY_IDENTIFIER_RELIABILITY = 103;
static const uint32_t PROPERTY_IDENTIFIER_RECIPIENT_LIST = 102;
static const uint32_t RELIABILITY_NO_FAULT_DETECTED = 0;
// --- DM-BR-B: File 1 "Ivory" (backup/restore payload carrier) - the B-ACC addition ---
// A small in-memory STREAM-access file. BACnetStack_SetBackupAndRestoreEnabled plus
// all four Prepare/Complete Backup/Restore callbacks are registered; ReinitializeDevice
// accepts the five backup/restore states (startBackup/endBackup/startRestore/
// endRestore/abortRestore, values 2-6) in addition to COLDSTART/WARMSTART.
static const uint16_t OBJECT_TYPE_FILE = 10;
static const uint32_t FILE_INSTANCE = 1; // "Ivory"
static const uint32_t FILE_ACCESS_METHOD_STREAM = 1;
static const uint32_t FILE_MAX_SIZE = 4096;
static const uint32_t PROPERTY_IDENTIFIER_FILE_SIZE = 42;
static const uint32_t PROPERTY_IDENTIFIER_FILE_TYPE = 43;
static const uint32_t PROPERTY_IDENTIFIER_ARCHIVE = 13;
static const uint32_t PROPERTY_IDENTIFIER_READ_ONLY = 99;
static const uint32_t PROPERTY_IDENTIFIER_MODIFICATION_DATE = 71;
static uint8_t g_fileData[FILE_MAX_SIZE];
static uint32_t g_fileDataLength = 0;
static bool g_fileArchive = false;
static const uint32_t REINITIALIZE_STATE_STARTBACKUP = 2;
static const uint32_t REINITIALIZE_STATE_ENDBACKUP = 3;
static const uint32_t REINITIALIZE_STATE_STARTRESTORE = 4;
static const uint32_t REINITIALIZE_STATE_ENDRESTORE = 5;
static const uint32_t REINITIALIZE_STATE_ABORTRESTORE = 6;
static const uint32_t SERVICE_ATOMIC_READ_FILE = 6;
static const uint32_t SERVICE_ATOMIC_WRITE_FILE = 7;
// --- SCHED-E-B: the remote fan-out target for Schedule 1 "Saffron" ------------
// A peer device this example writes to over the wire - the F-EXTWRITE / SCHED-E-B
// demo. Default is BACnetProfileExample-B-SA-CPP's default device instance and
// its commandable Analog Output 1 ("Chartreuse"); override with a locally-built
// B-SA-CPP instance (or any other device with a writable Analog Output 1) running
// at this instance for the write to actually reach a live device. Without a
// running peer at this instance the write is still SENT (Device Address Binding
// resolves it once the peer answers Who-Is/I-Am) but nothing answers.
static const uint32_t REMOTE_DEVICE_INSTANCE = 389002; // B-SA-CPP's default instance
static const uint32_t REMOTE_ANALOG_OUTPUT_INSTANCE = 1; // its "Chartreuse"
// --- F-TREND (T-VMT-I-B / T-ATR-B) - the series-new headline feature this example DEFINES ---
// Trend Log 1 "Lilac" polls Analog Input 1 (Bronze)'s Present_Value; Trend Log
// Multiple 1 "Magenta" polls several points at once. Both use POLLED logging
// (SetTrendLogTypeToPolled); ReadRange (service 35) retrieves the accumulated
// Log_Buffer records - a plain ReadProperty of Log_Buffer is REJECTED by the
// stack itself (Error(OBJECT, READ_ACCESS_DENIED); see AddTrendLogObject's doc
// comment). BACnetStack_InsertTrendLogRecord (used for the backup/restore half
// of a Trend Log, e.g. restoring one from a File 1 (Ivory) backup) is genuinely
// customer-facing at this pin - CASBACnetStackDLL.h, not the test-tool header -
// confirmed by reading both headers directly (issue #1016 moved it there; its
// Event Log counterpart, InsertEventLogRecord, stayed test-tool-only, the same
// trap B-ALSC hit with a similarly-named function).
static const uint16_t OBJECT_TYPE_TREND_LOG = 20;
static const uint16_t OBJECT_TYPE_TREND_LOG_MULTIPLE = 27;
static const uint32_t TREND_LOG_INSTANCE = 1; // "Lilac"
static const uint32_t TREND_LOG_MULTIPLE_INSTANCE = 1; // "Magenta"
static const uint32_t TREND_LOG_MAX_BUFFER_SIZE = 200;
static const uint32_t TREND_LOG_MULTIPLE_MAX_BUFFER_SIZE = 200;
static const uint32_t TREND_LOG_POLL_INTERVAL_HUNDREDTHS = 100; // 1 second (integer-divided by 100)
static const uint32_t SERVICE_READ_RANGE = 35;
// A WriteProperty to a commandable Present_Value carries a priority 1..16. When a
// client omits it, BACnet uses 16 (the lowest priority) - so normalise anything
// out of range to 16, matching the stack's own behaviour.
static uint8_t EffectivePriority(uint8_t priority) {
return (priority >= 1 && priority <= BACNET_PRIORITY_ARRAY_SIZE) ? priority : 16;
}
// Store a commanded value at a priority slot (a WriteProperty of a value).
static void CommandWrite(Commandable* c, uint8_t priority, double value) {
const uint8_t p = EffectivePriority(priority);
c->isSet[p - 1] = true;
c->value[p - 1] = value;
}
// Relinquish (clear) a priority slot - i.e. a WriteProperty of NULL.
static void CommandRelinquish(Commandable* c, uint8_t priority) {
const uint8_t p = EffectivePriority(priority);
c->isSet[p - 1] = false;
}
// Resolve which Commandable an (objectType, objectInstance) maps to, or NULL.
static Commandable* GetCommandable(uint16_t objectType, uint32_t objectInstance) {
if (objectType == OBJECT_TYPE_ANALOG_OUTPUT && objectInstance == ANALOG_OUTPUT_INSTANCE) {
return &g_analogOutput;
}
if (objectType == OBJECT_TYPE_BINARY_OUTPUT && objectInstance == BINARY_OUTPUT_INSTANCE) {
return &g_binaryOutput;
}
if (objectType == OBJECT_TYPE_MULTI_STATE_OUTPUT && objectInstance == MULTI_STATE_OUTPUT_INSTANCE) {
return &g_multiStateOutput;
}
return NULL;
}
// Is this read a single Priority_Array element (Priority_Array[1..16])? If so,
// report whether that slot is commanded (*slotIsSet) and its value (*slotValue).
// The typed Get callbacks use this to serve a commandable object's Priority_Array
// and to let the stack compute Present_Value from the highest non-null slot.
static bool ReadPrioritySlot(const Commandable* c, uint32_t propertyIdentifier,
bool useArrayIndex, uint32_t propertyArrayIndex,
bool* slotIsSet, double* slotValue) {
if (propertyIdentifier != PROPERTY_IDENTIFIER_PRIORITY_ARRAY || !useArrayIndex ||
propertyArrayIndex < 1 || propertyArrayIndex > BACNET_PRIORITY_ARRAY_SIZE) {
return false;
}
*slotIsSet = c->isSet[propertyArrayIndex - 1];
*slotValue = c->value[propertyArrayIndex - 1];
return true;
}
// -----------------------------------------------------------------------------
// 2. Property "get" callbacks
//
// The stack calls these when a client reads a property. For each data type the
// stack uses a separate callback. We return true (and fill *value) when we
// recognise the (object, property) pair, and false otherwise.
//
// THE errorCode OUT-PARAMETER. Every Get callback ends with uint32_t* errorCode.
// The stack PRESETS it to success (84) before the call, and reads it only if you
// return false. That gives a declining callback two distinct meanings:
//
// 1. return false and LEAVE errorCode ALONE -> "I have no opinion on this
// property." The stack falls back to its own handling (see below).
// 2. return false and SET *errorCode -> "This read fails, with THIS
// BACnet error." The client gets exactly that Error-PDU.
//
// Option 2 is new (CAS BACnet Stack issue #974); before it, a Get callback had
// no way to name an error at all. Do not reach for it reflexively - option 1 is
// still the right answer most of the time, for the reason in the next paragraph.
//
// WHAT false-WITHOUT-AN-ERROR-CODE ACTUALLY DOES - the most important paragraph
// in this file, and the opposite of what most people assume. It does NOT
// reliably produce a BACnet error. The stack errors only for the handful of
// properties it refuses to invent: Present_Value, Number_Of_States,
// Relinquish_Default, Local_Date, Local_Time, and a Network Port's APDU_Length
// (declining one of those now reads back as Error: read-access-denied, where
// older stack versions said value-not-initialized).
// For EVERYTHING ELSE, a false return means the stack SILENTLY SUBSTITUTES a
// default:
// Object_Name -> the literal string "undefined"
// Units -> no-units (95)
// otherwise -> a datatype zero-value
//
// AND THAT FALLBACK IS LOAD-BEARING, WHICH IS WHY IT IS NOT "FIXED" HERE. It is
// tempting to end every callback with *errorCode = unknown-property so nothing is
// ever silently invented. That breaks the device. The stack relies on the
// decline-and-fabricate path to answer required properties the application is
// not expected to serve - the Device's Max_APDU_Length_Accepted, APDU_Timeout
// and Number_Of_APDU_Retries among them. Name an error on the catch-all return
// and those required properties start failing instead of answering.
// So: set *errorCode ONLY where THIS device knows the read is wrong. There is
// exactly one such case below (State_Text with an out-of-range array index); the
// catch-all `return false` at the end of each callback deliberately leaves
// errorCode alone.
//
// ADDING AN OBJECT? READ THIS FIRST.
// The consequence is the opposite of reassuring. These callbacks are not
// uniformly strict:
// - GetPropertyReal / GetPropertyEnumerated / GetPropertyUnsignedInteger match
// on object type AND INSTANCE (directly, or via GetCommandable(), which
// looks up the exact type+instance pair). A new instance falls through every
// one of those checks.
// - GetPropertyBool serves Out_Of_Service on object TYPE ONLY, so a new
// instance of an existing type gets Out_Of_Service for free.
// So a half-added object does NOT fail loudly. Its Present_Value errors (that
// one is in the list above) - but its Object_Name reads back as "undefined" and
// its Units as no-units, with no error at all. Add two objects that way and BOTH
// report Object_Name "undefined": duplicate object names within one device, which
// is a spec violation and a hard BTL failure, and which every scan tool will show
// you as a healthy object. The device looks fine and is non-conformant.
//
// So: when you add an instance, walk EVERY callback below, then read back every
// required property of the new object and DIFF IT against the existing one. Do
// not trust "it scanned OK" - that is exactly the failure mode.
// -----------------------------------------------------------------------------
// REAL (floating point) - the Analog Input's Present_Value.
bool GetPropertyReal(const uint32_t deviceInstance, const uint16_t objectType,
const uint32_t objectInstance, const uint32_t propertyIdentifier,
float* value, const bool useArrayIndex,
const uint32_t propertyArrayIndex, uint32_t* errorCode) {
(void)errorCode; // see "THE errorCode OUT-PARAMETER" below: every catch-all here declines without naming an error
if (deviceInstance != g_deviceInstance) {
return false;
}
if (objectType == OBJECT_TYPE_ANALOG_INPUT &&
objectInstance == ANALOG_INPUT_INSTANCE &&
propertyIdentifier == PROPERTY_IDENTIFIER_PRESENT_VALUE) {
// ON REAL HARDWARE: return the live sensor reading here. Read it from a
// cached variable that your hardware updates (as g_analogInput1Value is),
// NOT directly from a slow/blocking device (I2C, SPI, ADC conversion):
// this callback runs on the BACnetStack_Tick() thread, so blocking it
// delays all BACnet processing. Sample the sensor on a timer/another
// thread and just hand back the latest value from here.
*value = g_analogInput1Value;
return true;
}
// Network Port "Vermilion" - Link_Speed, the negotiated speed of the
// physical interface this example's BACnet/IP traffic actually goes out
// over. Cached once at start-up into g_linkSpeedBitsPerSecond (see its
// declaration above for why). 0.0 ("indeterminable") is the correct,
// spec-honest answer when the host OS can't report a speed - not a bug.
if (objectType == OBJECT_TYPE_NETWORK_PORT && objectInstance == NETWORK_PORT_INSTANCE &&
propertyIdentifier == PROPERTY_IDENTIFIER_LINK_SPEED) {
*value = g_linkSpeedBitsPerSecond;
return true;
}
// Analog Value 1 "Diamond" - the alarm-capable process value. Its Present_Value
// is what the intrinsic OutOfRange algorithm watches; a client writes its
// Present_Value across a limit (>90 or <10) to fire an EventNotification.
if (objectType == OBJECT_TYPE_ANALOG_VALUE &&
objectInstance == ANALOG_VALUE_INSTANCE &&
propertyIdentifier == PROPERTY_IDENTIFIER_PRESENT_VALUE) {
*value = g_analogValue1Value;
return true;
}
// Analog Output (commandable): serve its Priority_Array slots and
// Relinquish_Default. The stack reads each slot to compute Present_Value and
// to answer a ReadProperty of the whole array. For a null (relinquished) slot
// we return false - the stack then takes the "slot is null" answer from
// GetPropertyBool below.
const Commandable* c = GetCommandable(objectType, objectInstance);
if (c != NULL && objectType == OBJECT_TYPE_ANALOG_OUTPUT) {
bool slotIsSet = false;
double slotValue = 0.0;
if (ReadPrioritySlot(c, propertyIdentifier, useArrayIndex, propertyArrayIndex,
&slotIsSet, &slotValue)) {
if (!slotIsSet) {
return false;
}
*value = (float)slotValue;
return true;
}
if (propertyIdentifier == PROPERTY_IDENTIFIER_RELINQUISH_DEFAULT) {
*value = (float)c->relinquishDefault;
return true;
}
}
return false;
}
// ENUMERATED - the Binary Input's Present_Value (0 = inactive, 1 = active) and
// the Analog Input's Units (degrees Celsius).
bool GetPropertyEnumerated(const uint32_t deviceInstance, const uint16_t objectType,
const uint32_t objectInstance, const uint32_t propertyIdentifier,
uint32_t* value, const bool useArrayIndex,
const uint32_t propertyArrayIndex, uint32_t* errorCode) {
(void)errorCode;
(void)useArrayIndex;
(void)propertyArrayIndex;
if (deviceInstance != g_deviceInstance) {
return false;
}
// Reliability (required) on Schedule 1 (Saffron) and Calendar 1 (Cream): this
// example never detects a fault on either, so it is always "no-fault-detected".
if (propertyIdentifier == PROPERTY_IDENTIFIER_RELIABILITY &&
((objectType == OBJECT_TYPE_SCHEDULE && objectInstance == SCHEDULE_INSTANCE) ||
(objectType == OBJECT_TYPE_CALENDAR && objectInstance == CALENDAR_INSTANCE))) {
*value = RELIABILITY_NO_FAULT_DETECTED;
return true;
}
if (objectType == OBJECT_TYPE_BINARY_INPUT &&
objectInstance == BINARY_INPUT_INSTANCE) {
if (propertyIdentifier == PROPERTY_IDENTIFIER_PRESENT_VALUE) {
*value = 1; // active
return true;
}
if (propertyIdentifier == PROPERTY_IDENTIFIER_POLARITY) {
*value = POLARITY_NORMAL; // required property of a Binary Input
return true;
}
}
// Analog Value 1 "Diamond" Units - it is a process value in percent.
if (objectType == OBJECT_TYPE_ANALOG_VALUE &&
objectInstance == ANALOG_VALUE_INSTANCE &&
propertyIdentifier == PROPERTY_IDENTIFIER_UNITS) {
*value = ENGINEERING_UNITS_PERCENT;
return true;
}
// Binary Output (commandable): Present_Value is an enumerated active/inactive
// driven through the Priority_Array. Serve the array slots and Relinquish_Default
// (plus its required Polarity).
const Commandable* c = GetCommandable(objectType, objectInstance);
if (c != NULL && objectType == OBJECT_TYPE_BINARY_OUTPUT) {
bool slotIsSet = false;
double slotValue = 0.0;
if (ReadPrioritySlot(c, propertyIdentifier, useArrayIndex, propertyArrayIndex,
&slotIsSet, &slotValue)) {
if (!slotIsSet) {
return false;
}
*value = (uint32_t)slotValue;
return true;
}
if (propertyIdentifier == PROPERTY_IDENTIFIER_RELINQUISH_DEFAULT) {
*value = (uint32_t)c->relinquishDefault;
return true;
}
if (propertyIdentifier == PROPERTY_IDENTIFIER_POLARITY) {
*value = POLARITY_NORMAL; // required property of a Binary Output
return true;
}
}
// Units is REQUIRED on an Analog Input AND on an Analog Output. Serve BOTH.
// If you only serve the input's, the output does not error - it silently
// reports no-units(95), because Units is not in the stack's
// valueShouldBeInitialized list and so falls through to a substituted default
// (see the note at the top of this section). A setpoint that reads back "no
// units" next to a degC sensor is the kind of thing nobody notices until
// commissioning.
if (propertyIdentifier == PROPERTY_IDENTIFIER_UNITS &&
((objectType == OBJECT_TYPE_ANALOG_INPUT && objectInstance == ANALOG_INPUT_INSTANCE) ||
(objectType == OBJECT_TYPE_ANALOG_OUTPUT && objectInstance == ANALOG_OUTPUT_INSTANCE))) {
*value = ENGINEERING_UNITS_DEGREES_CELSIUS;
return true;
}
if (objectType == OBJECT_TYPE_NETWORK_PORT &&
objectInstance == NETWORK_PORT_INSTANCE &&
propertyIdentifier == PROPERTY_IDENTIFIER_BACNET_IP_MODE) {
*value = BACNET_IP_MODE_NORMAL; // not foreign-device, not BBMD
return true;
}
return false;
}
// UNSIGNED INTEGER - the Multi-State Input's Present_Value, and the Device's
// Vendor_Identifier (the stack also uses Vendor_Identifier to build I-Am).
bool GetPropertyUnsignedInteger(const uint32_t deviceInstance, const uint16_t objectType,
const uint32_t objectInstance, const uint32_t propertyIdentifier,
uint32_t* value, const bool useArrayIndex,
const uint32_t propertyArrayIndex, uint32_t* errorCode) {
(void)errorCode;
if (deviceInstance != g_deviceInstance) {
return false;
}
if (objectType == OBJECT_TYPE_MULTI_STATE_INPUT &&
objectInstance == MULTI_STATE_INPUT_INSTANCE) {
if (propertyIdentifier == PROPERTY_IDENTIFIER_PRESENT_VALUE) {
*value = 1; // state 1 (valid range is 1..Number_Of_States)
return true;
}
if (propertyIdentifier == PROPERTY_IDENTIFIER_NUMBER_OF_STATES) {
*value = MULTI_STATE_INPUT_NUMBER_OF_STATES; // required property
return true;
}
// State_Text is an array. The stack asks for its LENGTH here (array
// index 0) before reading each element via GetPropertyCharString.
if (propertyIdentifier == PROPERTY_IDENTIFIER_STATE_TEXT &&
useArrayIndex && propertyArrayIndex == 0) {
*value = MULTI_STATE_INPUT_NUMBER_OF_STATES;
return true;
}
}
if (objectType == OBJECT_TYPE_DEVICE && objectInstance == g_deviceInstance &&
propertyIdentifier == PROPERTY_IDENTIFIER_VENDOR_IDENTIFIER) {
*value = VENDOR_IDENTIFIER;
return true;
}
if (objectType == OBJECT_TYPE_NETWORK_PORT && objectInstance == NETWORK_PORT_INSTANCE) {
if (propertyIdentifier == PROPERTY_IDENTIFIER_APDU_LENGTH) {
*value = MAX_APDU_LENGTH;
return true;
}
if (propertyIdentifier == PROPERTY_IDENTIFIER_REFERENCE_PORT) {
*value = NETWORK_PORT_REFERENCE_PORT_NONE;
return true;
}
if (propertyIdentifier == PROPERTY_IDENTIFIER_BACNET_IP_UDP_PORT) {
*value = g_bacnetIpUdpPort;
return true;
}
}
// Multi-State Output (commandable): Present_Value is an unsigned state number
// driven through the Priority_Array. Serve the array slots, Relinquish_Default,
// and the required Number_Of_States.
const Commandable* c = GetCommandable(objectType, objectInstance);
if (c != NULL && objectType == OBJECT_TYPE_MULTI_STATE_OUTPUT) {
bool slotIsSet = false;
double slotValue = 0.0;
if (ReadPrioritySlot(c, propertyIdentifier, useArrayIndex, propertyArrayIndex,
&slotIsSet, &slotValue)) {
if (!slotIsSet) {
return false;
}
*value = (uint32_t)slotValue;
return true;
}
if (propertyIdentifier == PROPERTY_IDENTIFIER_RELINQUISH_DEFAULT) {
*value = (uint32_t)c->relinquishDefault;
return true;
}
if (propertyIdentifier == PROPERTY_IDENTIFIER_NUMBER_OF_STATES) {
*value = MULTI_STATE_OUTPUT_NUMBER_OF_STATES;
return true;
}
}
// Ivory (File 1) - File_Size is REQUIRED with no stack default.
if (objectType == OBJECT_TYPE_FILE && objectInstance == FILE_INSTANCE &&
propertyIdentifier == PROPERTY_IDENTIFIER_FILE_SIZE) {
*value = g_fileDataLength;
return true;
}
return false;
}
// BOOLEAN - Out_Of_Service is a required property of every input object and of
// the Network Port. This is a read-only sensor, so nothing is ever out of
// service: always false.
bool GetPropertyBool(const uint32_t deviceInstance, const uint16_t objectType,
const uint32_t objectInstance, const uint32_t propertyIdentifier,
bool* value, const bool useArrayIndex,
const uint32_t propertyArrayIndex, uint32_t* errorCode) {
(void)errorCode;
if (deviceInstance != g_deviceInstance) {
return false;
}
// Calendar 1 (Cream) Present_Value (required): true when today's date is in
// Date_List. This example cannot populate a Calendar object's Date_List
// through the customer API (cas-bacnet-stack issue #963 - see TODO.md), so
// there is nothing to evaluate against; always answer false rather than
// fabricate a match.
if (objectType == OBJECT_TYPE_CALENDAR && objectInstance == CALENDAR_INSTANCE &&
propertyIdentifier == PROPERTY_IDENTIFIER_PRESENT_VALUE) {
*value = false;
return true;
}
// Commandable outputs: the stack asks "is this Priority_Array slot null?" with
// the boolean getter. Answer true (1) for a relinquished slot, false (0) for a
// commanded one. This is how the stack knows which slots to skip when computing
// Present_Value and how it encodes the NULLs in a ReadProperty of the array.
const Commandable* c = GetCommandable(objectType, objectInstance);
if (c != NULL && propertyIdentifier == PROPERTY_IDENTIFIER_PRIORITY_ARRAY &&
useArrayIndex && propertyArrayIndex >= 1 &&
propertyArrayIndex <= BACNET_PRIORITY_ARRAY_SIZE) {
*value = !c->isSet[propertyArrayIndex - 1];
return true;
}
// Out_Of_Service is a required property of every input and output object and of
// the Network Port. This example never takes anything out of service: false.
if (propertyIdentifier == PROPERTY_IDENTIFIER_OUT_OF_SERVICE &&
(objectType == OBJECT_TYPE_ANALOG_INPUT ||
objectType == OBJECT_TYPE_BINARY_INPUT ||
objectType == OBJECT_TYPE_MULTI_STATE_INPUT ||
objectType == OBJECT_TYPE_ANALOG_OUTPUT ||
objectType == OBJECT_TYPE_BINARY_OUTPUT ||
objectType == OBJECT_TYPE_MULTI_STATE_OUTPUT ||
objectType == OBJECT_TYPE_ANALOG_VALUE ||
objectType == OBJECT_TYPE_NETWORK_PORT ||
objectType == OBJECT_TYPE_SCHEDULE ||
objectType == OBJECT_TYPE_CALENDAR)) {
*value = false;
return true;
}
// Ivory (File 1) - Archive (writable, DM-BR-B configuration-file marker) and
// Read_Only are both REQUIRED with no stack default.
if (objectType == OBJECT_TYPE_FILE && objectInstance == FILE_INSTANCE) {
if (propertyIdentifier == PROPERTY_IDENTIFIER_ARCHIVE) {
*value = g_fileArchive;
return true;
}
if (propertyIdentifier == PROPERTY_IDENTIFIER_READ_ONLY) {
*value = false; // this example's File is writable (AddFileObject isWritable=true)
return true;
}
}
return false;
}
// OCTET STRING - the Network Port's BACnet/IP addressing. The stack cannot know
// the host's IP, so the application must supply IP_Address and IP_Subnet_Mask
// (and IP_Default_Gateway). Each is four octets. The stack also reads IP_Address
// (with BACnet_IP_UDP_Port) to build the port's six-octet MAC_Address.
bool GetPropertyOctetString(const uint32_t deviceInstance, const uint16_t objectType,
const uint32_t objectInstance, const uint32_t propertyIdentifier,
uint8_t* value, uint32_t* valueElementCount,
const uint32_t maxElementCount, const bool useArrayIndex,
const uint32_t propertyArrayIndex, uint32_t* errorCode) {
(void)useArrayIndex;
(void)errorCode;
(void)propertyArrayIndex;
if (deviceInstance != g_deviceInstance ||
objectType != OBJECT_TYPE_NETWORK_PORT ||
objectInstance != NETWORK_PORT_INSTANCE ||
maxElementCount < 4) {
return false;
}
const uint8_t* source = NULL;
switch (propertyIdentifier) {
case PROPERTY_IDENTIFIER_IP_ADDRESS: source = g_ipAddress; break;
case PROPERTY_IDENTIFIER_IP_SUBNET_MASK: source = g_ipSubnetMask; break;
case PROPERTY_IDENTIFIER_IP_DEFAULT_GATEWAY: source = g_ipDefaultGateway; break;
default: return false;
}
memcpy(value, source, 4);
*valueElementCount = 4;
return true;
}
// Small helper: copy a C string into the stack's character-string buffer and
// set the element count + encoding. Returns true (so callers can `return`).
static bool ReturnCharacterString(const char* text, char* value,
uint32_t* valueElementCount,
const uint32_t maxElementCount,
uint8_t* encodingType) {
uint32_t length = (uint32_t)strlen(text);
if (length > maxElementCount) {
// A previous version of this comment claimed a served string always
// fits under maxElementCount (MAX_CHARACTER_STRING_SIZE - 256 on a
// full build, 64 on STACK_OPTION_TARGET_EMBEDDED) "with room to
// spare." That was false: DEVICE_DESCRIPTION shipped at 286 chars,
// over the 256-char full-build limit, and the read didn't even get
// truncated - it Aborted outright before reaching this clamp
// (chipkin/BACnetProfileExample-B-BC-CPP#7). Whatever the actual
// on-the-wire behavior turns out to be for an over-length string, a
// silent truncation here is the wrong fallback for a tutorial
// example either way - it teaches "this is fine" when it isn't. Warn
// loudly instead, and keep every served string under the limit for
// real rather than relying on this clamp to save you.
printf("Warning: property value %u chars, longer than the stack's "
"%u-char buffer - truncating (and the actual read may fail "
"before this point; see issue #7).\n",
(unsigned)length, (unsigned)maxElementCount);
length = maxElementCount;
}
memcpy(value, text, length);
*valueElementCount = length;
*encodingType = CHARACTER_STRING_ENCODING_UTF8;
return true;
}
// CHARACTER STRING - Object_Name for each object, and the device Description.
bool GetPropertyCharString(const uint32_t deviceInstance, const uint16_t objectType,
const uint32_t objectInstance, const uint32_t propertyIdentifier,
char* value, uint32_t* valueElementCount,
const uint32_t maxElementCount, uint8_t* encodingType,
const bool useArrayIndex, const uint32_t propertyArrayIndex,
uint32_t* errorCode) {
if (deviceInstance != g_deviceInstance) {
return false;
}
// State_Text (optional) - one label per state of the Multi-State Input. It is
// a BACnet array, so the stack asks for one element at a time by index
// (1..Number_Of_States). Present_Value 1 -> "On", 2 -> "Off", 3 -> "Auto".
if (objectType == OBJECT_TYPE_MULTI_STATE_INPUT &&
objectInstance == MULTI_STATE_INPUT_INSTANCE &&
propertyIdentifier == PROPERTY_IDENTIFIER_STATE_TEXT && useArrayIndex) {
static const char* const stateText[] = { "On", "Off", "Auto" };
if (propertyArrayIndex >= 1 && propertyArrayIndex <= MULTI_STATE_INPUT_NUMBER_OF_STATES) {
return ReturnCharacterString(stateText[propertyArrayIndex - 1], value,
valueElementCount, maxElementCount, encodingType);
}
// The one place in this file where naming an error is clearly right: the
// client asked for State_Text[n] and this object has no element n. That
// is not "no opinion" - it is a wrong read, and the spec has a code for
// it. Without this the client would silently receive an empty string.
*errorCode = ERROR_CODE_INVALID_ARRAY_INDEX;
return false;
}
// Object_Name - the colour name for each object.
if (propertyIdentifier == PROPERTY_IDENTIFIER_OBJECT_NAME) {
if (objectType == OBJECT_TYPE_DEVICE && objectInstance == g_deviceInstance) {
return ReturnCharacterString(DEVICE_NAME, value, valueElementCount, maxElementCount, encodingType);
}
if (objectType == OBJECT_TYPE_ANALOG_INPUT && objectInstance == ANALOG_INPUT_INSTANCE) {
return ReturnCharacterString("Bronze", value, valueElementCount, maxElementCount, encodingType);
}
if (objectType == OBJECT_TYPE_BINARY_INPUT && objectInstance == BINARY_INPUT_INSTANCE) {
return ReturnCharacterString("Emerald", value, valueElementCount, maxElementCount, encodingType);
}
if (objectType == OBJECT_TYPE_MULTI_STATE_INPUT && objectInstance == MULTI_STATE_INPUT_INSTANCE) {
return ReturnCharacterString("Hot Pink", value, valueElementCount, maxElementCount, encodingType);
}
if (objectType == OBJECT_TYPE_ANALOG_OUTPUT && objectInstance == ANALOG_OUTPUT_INSTANCE) {
return ReturnCharacterString("Chartreuse", value, valueElementCount, maxElementCount, encodingType);
}
if (objectType == OBJECT_TYPE_BINARY_OUTPUT && objectInstance == BINARY_OUTPUT_INSTANCE) {
return ReturnCharacterString("Fuchsia", value, valueElementCount, maxElementCount, encodingType);
}
if (objectType == OBJECT_TYPE_ANALOG_VALUE && objectInstance == ANALOG_VALUE_INSTANCE) {
return ReturnCharacterString("Diamond", value, valueElementCount, maxElementCount, encodingType);
}
if (objectType == OBJECT_TYPE_NOTIFICATION_CLASS && objectInstance == NOTIFICATION_CLASS_INSTANCE) {
return ReturnCharacterString("Crimson", value, valueElementCount, maxElementCount, encodingType);
}
if (objectType == OBJECT_TYPE_MULTI_STATE_OUTPUT && objectInstance == MULTI_STATE_OUTPUT_INSTANCE) {
return ReturnCharacterString("Indigo", value, valueElementCount, maxElementCount, encodingType);
}
if (objectType == OBJECT_TYPE_NETWORK_PORT && objectInstance == NETWORK_PORT_INSTANCE) {
return ReturnCharacterString("Vermilion", value, valueElementCount, maxElementCount, encodingType);
}
if (objectType == OBJECT_TYPE_SCHEDULE && objectInstance == SCHEDULE_INSTANCE) {
return ReturnCharacterString("Saffron", value, valueElementCount, maxElementCount, encodingType);
}
if (objectType == OBJECT_TYPE_CALENDAR && objectInstance == CALENDAR_INSTANCE) {
return ReturnCharacterString("Cream", value, valueElementCount, maxElementCount, encodingType);
}
if (objectType == OBJECT_TYPE_FILE && objectInstance == FILE_INSTANCE) {
return ReturnCharacterString("Ivory", value, valueElementCount, maxElementCount, encodingType);
}
if (objectType == OBJECT_TYPE_TREND_LOG && objectInstance == TREND_LOG_INSTANCE) {
return ReturnCharacterString("Lilac", value, valueElementCount, maxElementCount, encodingType);
}
if (objectType == OBJECT_TYPE_TREND_LOG_MULTIPLE && objectInstance == TREND_LOG_MULTIPLE_INSTANCE) {
return ReturnCharacterString("Magenta", value, valueElementCount, maxElementCount, encodingType);
}
}
// Ivory (File 1) - File_Type is REQUIRED with no stack default.
if (objectType == OBJECT_TYPE_FILE && objectInstance == FILE_INSTANCE &&
propertyIdentifier == PROPERTY_IDENTIFIER_FILE_TYPE) {
return ReturnCharacterString("application/octet-stream", value, valueElementCount,
maxElementCount, encodingType);
}
// The remaining strings are all on the Device object - its identity, read
// by clients and used to populate the device's I-Am / object list.
if (objectType == OBJECT_TYPE_DEVICE && objectInstance == g_deviceInstance) {
switch (propertyIdentifier) {
case PROPERTY_IDENTIFIER_DESCRIPTION:
return ReturnCharacterString(DEVICE_DESCRIPTION, value, valueElementCount, maxElementCount, encodingType);
case PROPERTY_IDENTIFIER_VENDOR_NAME:
return ReturnCharacterString(VENDOR_NAME, value, valueElementCount, maxElementCount, encodingType);
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);
case PROPERTY_IDENTIFIER_APPLICATION_SOFTWARE_VERSION:
return ReturnCharacterString(APPLICATION_SOFTWARE_VERSION, value, valueElementCount, maxElementCount, encodingType);
default:
break;
}
}
return false;
}
// The Device's Local_Time / Local_Date (DM-TS-B, DM-UTC-B): the stack refuses
// to invent a default for either of these if the app declines (see "WHAT
// false-WITHOUT-AN-ERROR-CODE ACTUALLY DOES" above) - a device that never
// serves them reads back Error: read-access-denied, which is exactly how a
// client normally confirms a TimeSynchronization/UTCTimeSynchronization
// actually took (chipkin/BACnetProfileExample-B-BC-CPP#7). This example
// claims both BIBBs, so it has to actually answer these, not just leave the
// stack's "declined" fallback in place. Real wall-clock local time - the
// simplest honest answer, and consistent with HelperGetSystemTime().
static bool GetCurrentLocalTm(struct tm* out) {
const time_t nowSeconds = time(NULL);
#if defined(_WIN32)
return localtime_s(out, &nowSeconds) == 0;
#else
return localtime_r(&nowSeconds, out) != NULL;
#endif
}
// Ivory (File 1) - Modification_Date's Time half. Fixed at a nominal start-up
// value; a real device would stamp this on every WriteFile.
bool GetPropertyTime(const uint32_t deviceInstance, const uint16_t objectType,
const uint32_t objectInstance, const uint32_t propertyIdentifier,
uint8_t* hour, uint8_t* minute, uint8_t* second, uint8_t* hundredthSecond,
const bool useArrayIndex, const uint32_t propertyArrayIndex, uint32_t* errorCode) {
(void)useArrayIndex;
(void)propertyArrayIndex;
(void)errorCode;
if (deviceInstance != g_deviceInstance) {
return false;
}
if (objectType == OBJECT_TYPE_FILE && objectInstance == FILE_INSTANCE &&
propertyIdentifier == PROPERTY_IDENTIFIER_MODIFICATION_DATE) {
*hour = 0; *minute = 0; *second = 0; *hundredthSecond = 0;
return true;
}
if (objectType == OBJECT_TYPE_DEVICE && objectInstance == g_deviceInstance &&
propertyIdentifier == PROPERTY_IDENTIFIER_LOCAL_TIME) {
struct tm nowTm;
if (!GetCurrentLocalTm(&nowTm)) {
return false;
}
*hour = (uint8_t)nowTm.tm_hour;
*minute = (uint8_t)nowTm.tm_min;
*second = (uint8_t)nowTm.tm_sec;
*hundredthSecond = 0;
return true;
}
return false;
}
// Ivory (File 1) - Modification_Date's Date half. Fixed at a nominal start-up
// value; a real device would stamp this on every WriteFile.
bool GetPropertyDate(const uint32_t deviceInstance, const uint16_t objectType,
const uint32_t objectInstance, const uint32_t propertyIdentifier,
uint8_t* yearMinus1900, uint8_t* month, uint8_t* day, uint8_t* weekday,
const bool useArrayIndex, const uint32_t propertyArrayIndex, uint32_t* errorCode) {
(void)useArrayIndex;
(void)propertyArrayIndex;
(void)errorCode;
if (deviceInstance != g_deviceInstance) {
return false;