From da2dc9841acf40040f487bc9ec9bd8d2919ab00c Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Thu, 28 Sep 2023 10:50:32 -0500 Subject: [PATCH] Feature/raspberry pi blinkt color objects (#503) * fixed BACnetXYcolor and BACnetColorCommand encode and decoding and improved unit test coverage. Refactored BACnetXYcolor to/from ascii into lighting module. * added to the color, color temperature, and lighting output objects a fade/ramp/step engine. Added color and color command coercion into the channel object and enabled color temperature object coercion. Added CreateObject and DeleteObject service handling to the color, color temperature, lighting output, and channel objects. * added blinkt demo app for Raspberry Pi [WIP] * updated gitignore to simplify handling of apps folder contents * fixed piface demo build * added RaspiOS to pipeline for piface and blinkt! demo builds * added device object timer function for child object types into example Device object. Refactored device object to increment database revision for create or delete object services. Refactored example app/server to use mstimer library and device child object timers. --------- Co-authored-by: Steve Karg --- .github/workflows/raspi.yml | 75 + .gitignore | 62 +- CHANGELOG.md | 13 +- CMakeLists.txt | 2 + Makefile | 8 + apps/Makefile | 12 +- .../Blinkt-Raspberry-Pi-GPIO-Pinout.png | Bin 0 -> 135260 bytes apps/blinkt/Makefile | 56 + apps/blinkt/README.md | 26 + apps/blinkt/blinkt.c | 213 ++ apps/blinkt/blinkt.h | 23 + apps/blinkt/device.c | 1951 +++++++++++++++++ apps/blinkt/main.c | 465 ++++ apps/piface/Makefile | 4 +- apps/piface/device.c | 622 ++++-- apps/server/main.c | 87 +- apps/writeprop/Makefile | 3 + apps/writepropm/Makefile | 3 + src/bacnet/bacapp.c | 26 +- src/bacnet/bacdevobjpropref.c | 21 +- src/bacnet/bacenum.h | 3 +- src/bacnet/bactext.c | 16 +- src/bacnet/basic/object/ao.c | 12 +- src/bacnet/basic/object/bacfile.c | 12 +- src/bacnet/basic/object/bo.c | 12 +- src/bacnet/basic/object/channel.c | 891 ++++---- src/bacnet/basic/object/channel.h | 284 ++- .../basic/object/client/device-client.c | 34 +- src/bacnet/basic/object/color_object.c | 382 +++- src/bacnet/basic/object/color_object.h | 5 +- src/bacnet/basic/object/color_temperature.c | 609 ++++- src/bacnet/basic/object/color_temperature.h | 5 +- src/bacnet/basic/object/device.c | 94 +- src/bacnet/basic/object/device.h | 13 + src/bacnet/basic/object/lo.c | 1776 ++++++++++++--- src/bacnet/basic/object/lo.h | 54 + src/bacnet/basic/object/mso.c | 12 +- src/bacnet/basic/sys/color_rgb.c | 138 +- src/bacnet/basic/sys/color_rgb.h | 7 + src/bacnet/basic/sys/linear.c | 128 ++ src/bacnet/basic/sys/linear.h | 34 + src/bacnet/lighting.c | 593 ++--- src/bacnet/lighting.h | 17 + src/bacnet/wp.c | 3 - test/CMakeLists.txt | 1 + .../basic/object/channel/CMakeLists.txt | 3 +- test/bacnet/basic/object/channel/src/main.c | 86 +- .../basic/object/color_object/CMakeLists.txt | 1 + .../basic/object/color_object/src/main.c | 90 +- .../object/color_temperature/CMakeLists.txt | 1 + .../basic/object/color_temperature/src/main.c | 77 +- .../bacnet/basic/object/device/CMakeLists.txt | 1 + test/bacnet/basic/object/lo/CMakeLists.txt | 9 +- test/bacnet/basic/object/lo/src/main.c | 96 +- test/bacnet/basic/sys/color_rgb/src/main.c | 122 +- test/bacnet/basic/sys/linear/CMakeLists.txt | 44 + test/bacnet/basic/sys/linear/src/main.c | 136 ++ test/bacnet/lighting/CMakeLists.txt | 2 + test/bacnet/lighting/src/main.c | 165 +- zephyr/CMakeLists.txt | 2 + .../basic/object/color_object/CMakeLists.txt | 1 + .../object/color_temperature/CMakeLists.txt | 1 + .../bacnet/basic/object/lo/CMakeLists.txt | 3 + zephyr/tests/bacnet/lighting/CMakeLists.txt | 2 + 64 files changed, 8012 insertions(+), 1637 deletions(-) create mode 100644 .github/workflows/raspi.yml create mode 100644 apps/blinkt/Blinkt-Raspberry-Pi-GPIO-Pinout.png create mode 100644 apps/blinkt/Makefile create mode 100644 apps/blinkt/README.md create mode 100644 apps/blinkt/blinkt.c create mode 100644 apps/blinkt/blinkt.h create mode 100644 apps/blinkt/device.c create mode 100644 apps/blinkt/main.c create mode 100644 src/bacnet/basic/sys/linear.c create mode 100644 src/bacnet/basic/sys/linear.h create mode 100644 test/bacnet/basic/sys/linear/CMakeLists.txt create mode 100644 test/bacnet/basic/sys/linear/src/main.c diff --git a/.github/workflows/raspi.yml b/.github/workflows/raspi.yml new file mode 100644 index 00000000..6d02912f --- /dev/null +++ b/.github/workflows/raspi.yml @@ -0,0 +1,75 @@ +name: Raspberry Pi + +on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + type: choice + options: + - info + - warning + - debug + tags: + description: 'Test scenario tags' + required: false + type: boolean + environment: + description: 'Environment to run tests against' + type: environment + required: true + +jobs: + log-the-inputs: + runs-on: raspios/base:latest + steps: + - run: | + echo "Log level: $LEVEL" + echo "Tags: $TAGS" + echo "Environment: $ENVIRONMENT" + env: + LEVEL: ${{ inputs.logLevel }} + TAGS: ${{ inputs.tags }} + ENVIRONMENT: ${{ inputs.environment }} + + raspi-bip-apps: + runs-on: raspios/base:latest + steps: + - uses: actions/checkout@v4 + - name: Create Build Environment + run: | + sudo apt-get update -qq + sudo apt-get install -qq libconfig-dev + - name: Build Demo Apps + run: | + gcc --version + make clean + make all + + piface: + runs-on: raspios/base:latest + steps: + - uses: actions/checkout@v4 + - name: Create Build Environment + run: | + sudo apt-get update -qq + - name: Build Demo Apps + run: | + gcc --version + cd apps/piface && ./configure && cd ../../ + make piface + + blinkt: + runs-on: raspios/base:latest + steps: + - uses: actions/checkout@v4 + - name: Create Build Environment + run: | + sudo apt-get update -qq + sudo apt-get install -qq libpigpio-dev libpigpiod-if-dev pigpiod + - name: Build Demo Apps + run: | + gcc --version + make blinkt diff --git a/.gitignore b/.gitignore index 25b102f0..9115a2b3 100644 --- a/.gitignore +++ b/.gitignore @@ -57,7 +57,6 @@ test-results.xml Debug/ settings/ *.componentinfo.xml -bin/ Backup* BACnet_BDT_table address_cache @@ -67,50 +66,29 @@ CMakeLists.txt.user **/out/* Obj/ Release/ -apps/piface/libmcp23s17/ -apps/piface/libpifacedigital/ /test/build/ /cmake-build-* /.idea -/apps/abort/bacabort -/apps/ack-alarm/bacackalarm -/apps/readfile/bacarf -/apps/writefile/bacawf -/apps/dcc/bacdcc -/apps/epics/bacepics -/apps/error/bacerror -/apps/event/bacevent -/apps/getevent/bacge -/apps/iam/baciam -/apps/iamrouter/baciamr -/apps/initrouter/bacinitr -/apps/netnumis/bacnni -/apps/server-client/bacpoll -/apps/readbdt/bacrbdt -/apps/reinit/bacrd -/apps/readfdt/bacrfdt -/apps/readprop/bacrp -/apps/readpropm/bacrpm -/apps/readrange/bacrr -/apps/scov/bacscov -/apps/server/bacserv -/apps/timesync/bacts -/apps/ucov/bacucov -/apps/uevent/bacuevent -/apps/uptransfer/bacupt -/apps/writebdt/bacwbdt -/apps/whohas/bacwh -/apps/whois/bacwi -/apps/whatisnetnum/bacwinn -/apps/whoisrouter/bacwir -/apps/writeprop/bacwp -/apps/writepropm/bacwpm -/apps/mstpcap/mstpcap -/apps/mstpcrc/mstpcrc -/apps/add-list-element/bacale -/apps/remove-list-element/bacrle -/apps/create-object/bacco -/apps/delete-object/bacdo +# exclude the bin folder, except for some files +bin/* +!bin/*.sh +!bin/*.bat +!bin/*.txt + +# exclude the apps folder, except for some source files +apps/* +!apps/Makefile +!apps/*.md +apps/**/* +!apps/**/ +!apps/**/*.c +!apps/**/*.h +!apps/**/*.txt +!apps/**/*.md +!apps/**/Makefile +!apps/**/CMakeLists.txt +apps/piface/libmcp23s17/ +apps/piface/libpifacedigital/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 54bd7810..91952f1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,14 +18,25 @@ The git repositories are hosted at the following sites: ### Added -Added [feature#14] EventTimeStamp decoding from ReadPropertyMultiple app +- Added [feature#14] EventTimeStamp decoding from ReadPropertyMultiple app. +- Added Channel, Color, Color Temperature, & Lighting Output demo app with Blinkt! +- Added pipeline build of piface and blinkt apps with Raspberry Pi OS image. +- Added linear interpolation library functions used in fading and ramping. ### Changed +- Added Device timer API to feed elapsed milliseconds to children objects. +- Changed gitignore to ease the maintainenance of source files in app folder +- Changed example server app device simulator to use mstimer instead of OS time. +- Changed example channel object to be dynamically created or deleted +- Changed example channel object to handle color & color temperature objects. + ### Fixed - Fixed datetime decode of invalid application tag. (#495) - Fixed extraneous SO_BINDTODEVICE error message in Linux BIP +- Fixed example Color, Color Temperature, and Lighting object fade, ramp, and step. +- Fixed and secured BACnetXYcolor and ColorCommand codecs. ## [1.2.0] - 2023-09-11 diff --git a/CMakeLists.txt b/CMakeLists.txt index 6ff09b25..4b9307dc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -362,6 +362,8 @@ add_library(${PROJECT_NAME} src/bacnet/basic/sys/key.h src/bacnet/basic/sys/keylist.c src/bacnet/basic/sys/keylist.h + src/bacnet/basic/sys/linear.c + src/bacnet/basic/sys/linear.h src/bacnet/basic/sys/mstimer.c src/bacnet/basic/sys/mstimer.h src/bacnet/basic/sys/ringbuf.c diff --git a/Makefile b/Makefile index c67a4aaf..02d2bfe2 100644 --- a/Makefile +++ b/Makefile @@ -69,6 +69,10 @@ ack-alarm: add-list-element: $(MAKE) -s -C apps $@ +.PHONY: blinkt +blinkt: + $(MAKE) -s -C apps $@ + .PHONY: dcc dcc: $(MAKE) -s -C apps $@ @@ -101,6 +105,10 @@ gateway: gateway-win32: $(MAKE) BACNET_PORT=win32 -s -C apps gateway +.PHONY: piface +piface: + $(MAKE) CSTANDARD="-std=gnu11" LEGACY=true -s -C apps $@ + .PHONY: readbdt readbdt: $(MAKE) -s -C apps $@ diff --git a/apps/Makefile b/apps/Makefile index ce264886..3edabab9 100644 --- a/apps/Makefile +++ b/apps/Makefile @@ -215,14 +215,14 @@ endif ##### # AFL -fuzz-afl: CC=afl-gcc +fuzz-afl: CC=afl-gcc fuzz-afl: FUZZ_FLAGS=-DFUZZING=1 fuzz-afl: LFLAGS += $(FUZZ_FLAGS) fuzz-afl: CFLAGS += $(FUZZ_FLAGS) fuzz-afl: export AFL_USE_ASAN=1 # LIBFUZZER -fuzz-libfuzzer: CC=clang +fuzz-libfuzzer: CC=clang fuzz-libfuzzer: FUZZ_FLAGS=-DFUZZING=1 -fsanitize=fuzzer,address -g3 -Og -fno-optimize-sibling-calls -fno-omit-frame-pointer fuzz-libfuzzer: LFLAGS += $(FUZZ_FLAGS) fuzz-libfuzzer: CFLAGS += $(FUZZ_FLAGS) @@ -263,6 +263,10 @@ ack-alarm: $(BACNET_LIB_TARGET) add-list-element: $(BACNET_LIB_TARGET) $(MAKE) -B -C $@ clean all +.PHONY: blinkt +blinkt: + $(MAKE) -C $@ + .PHONY: dcc dcc: $(BACNET_LIB_TARGET) $(MAKE) -B -C $@ @@ -311,6 +315,10 @@ mstpcap: mstpcrc: $(MAKE) -B -C $@ +.PHONY: piface +piface: + $(MAKE) -B -C $@ + .PHONY: ptransfer ptransfer: $(BACNET_LIB_TARGET) $(MAKE) -B -C $@ diff --git a/apps/blinkt/Blinkt-Raspberry-Pi-GPIO-Pinout.png b/apps/blinkt/Blinkt-Raspberry-Pi-GPIO-Pinout.png new file mode 100644 index 0000000000000000000000000000000000000000..2c33c133816754762cb907e1f931f389ca12b596 GIT binary patch literal 135260 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 literal 0 HcmV?d00001 diff --git a/apps/blinkt/Makefile b/apps/blinkt/Makefile new file mode 100644 index 00000000..d6fdb297 --- /dev/null +++ b/apps/blinkt/Makefile @@ -0,0 +1,56 @@ +############################################################################### +# Makefile for Project - BACnet Blinkt! +# sudo apt install libpigpio-dev libpigpiod-if-dev pigpiod +############################################################################### + +## General Flags +TARGET = bacblinkt +# BACnet objects that are used with this app +BACNET_OBJECT_DIR = $(BACNET_SRC_DIR)/bacnet/basic/object +SRC = main.c \ + blinkt.c \ + device.c \ + $(BACNET_OBJECT_DIR)/netport.c \ + $(BACNET_OBJECT_DIR)/channel.c \ + $(BACNET_OBJECT_DIR)/color_object.c \ + $(BACNET_OBJECT_DIR)/color_temperature.c \ + $(BACNET_OBJECT_DIR)/lc.c \ + $(BACNET_OBJECT_DIR)/lo.c + +CFLAGS += -DMAX_TSM_TRANSACTIONS=1 + +# TARGET_EXT is defined in apps/Makefile as .exe or nothing +TARGET_BIN = ${TARGET}$(TARGET_EXT) + +# note: link to pigpio daemon so that our app can run without root +# start pigio daemon with the command 'sudo pigpiod' +LD_PIGPIO = -Wl,-pthread,-lpigpiod_if2 +LFLAGS += $(LD_PIGPIO) + +OBJS += ${SRC:.c=.o} + +all: ${BACNET_LIB_TARGET} Makefile ${TARGET_BIN} + +${TARGET_BIN}: ${OBJS} Makefile ${BACNET_LIB_TARGET} + ${CC} ${PFLAGS} ${OBJS} ${LFLAGS} -o $@ + size $@ + cp $@ ../../bin + +${BACNET_LIB_TARGET}: + ( cd ${BACNET_LIB_DIR} ; $(MAKE) clean ; $(MAKE) -s ) + +.c.o: + ${CC} -c ${CFLAGS} $*.c -o $@ + +.PHONY: depend +depend: + rm -f .depend + ${CC} -MM ${CFLAGS} *.c >> .depend + +.PHONY: clean +clean: + rm -f core ${TARGET_BIN} ${OBJS} $(TARGET).map ${BACNET_LIB_TARGET} + +.PHONY: include +include: .depend + diff --git a/apps/blinkt/README.md b/apps/blinkt/README.md new file mode 100644 index 00000000..8698e19b --- /dev/null +++ b/apps/blinkt/README.md @@ -0,0 +1,26 @@ +# Raspberry Pi connected to a Blinkt! RGB card demo + +This demo includes a BACnet server with Color objects and service handlers +for the eight RGB (red-green-blue) LEDs attached to the Blinkt! card. + +## Installation + +The demo uses pigpiod (Pi GPIO Daemon) and developer library. To install +and run the daemon at powerup (and immediately): + + $ sudo apt install libpigpio-dev libpigpiod-if-dev pigpiod + $ sudo systemctl enable pigpiod + $ sudo systemctl start pigpiod + +## Building + +Build from the root folder: + + $ make blinkt + +## Running + +Run from the bin/ folder: + + $ ./bin/bacblinkt 9009 + diff --git a/apps/blinkt/blinkt.c b/apps/blinkt/blinkt.c new file mode 100644 index 00000000..5e0953a8 --- /dev/null +++ b/apps/blinkt/blinkt.c @@ -0,0 +1,213 @@ +/** + * @file + * @brief API for Blinkt! daughter board for Raspberry Pi + * + * SPDX-License-Identifier: MIT + */ +#include +#include +#include +#include +#include +#include "blinkt.h" + +#define BLINKT_DEFAULT_BRIGHTNESS 7 +#define BLINKT_NUM_LEDS 8 + +/* GPIO pin numbers */ +#define BLINKT_MOSI 23 +#define BLINKT_SCLK 24 + +/* RGBb data for each LED */ +static uint32_t Blinkt_LED[BLINKT_NUM_LEDS]; +/* handle to the pigpiod */ +static int Blinkt_Pi; + +/** + * @brief Get the number of LEDS + * @return Number of LEDs + */ +uint8_t blinkt_led_count(void) +{ + return BLINKT_NUM_LEDS; +} + +/** + * @brief Set all the LEDS to default brightness (7/31) + */ +void blinkt_clear(void) +{ + int x; + for (x = 0; x < BLINKT_NUM_LEDS; x++) { + Blinkt_LED[x] = BLINKT_DEFAULT_BRIGHTNESS; + } +} + +/** + * @brief Set one LED to specific RGB color + * @param led index 0..#BLINKT_NUM_LEDS + * @param r color red from 0..255 + * @param g color green from 0..255 + * @param b color blue from 0..255 + */ +void blinkt_set_pixel(uint8_t led, uint8_t r, uint8_t g, uint8_t b) +{ + if (led >= BLINKT_NUM_LEDS) + return; + + Blinkt_LED[led] = blinkt_rgbb(r, g, b, Blinkt_LED[led] & 0x1F); +} + +/** + * @brief Set one LED to specific intensity + * @param led index 0..#BLINKT_NUM_LEDS + * @param brightness intensity from 0..31, 0=OFF, 1=dimmest, 31=brightest + */ +void blinkt_set_pixel_brightness(uint8_t led, uint8_t brightness) +{ + if (led >= BLINKT_NUM_LEDS) + return; + + Blinkt_LED[led] = (Blinkt_LED[led] & 0xFFFFFF00) | (brightness & 0x1F); +} + +/** + * @brief Set one LED to RGB color and brightness + * @param led index 0..#BLINKT_NUM_LEDS + * @param color encoded as 32-bit RGBb (red | green | blue | brightness) + */ +void blinkt_set_pixel_uint32(uint8_t led, uint32_t color) +{ + if (led >= BLINKT_NUM_LEDS) + return; + + Blinkt_LED[led] = color; +} + +/** + * @brief Encode RGB color and brightness into 32-bit RGBb + * @param r color red from 0..255 + * @param g color green from 0..255 + * @param b color blue from 0..255 + * @param brightness intensity from 0..31, 0=OFF, 1=dimmest, 31=brightest + * @return color encoded as 32-bit RGBb (red | green | blue | brightness) + */ +uint32_t blinkt_rgbb(uint8_t r, uint8_t g, uint8_t b, uint8_t brightness) +{ + uint32_t result = 0; + result = (brightness & 0x1F); + result |= ((uint32_t)r << 24); + result |= ((uint32_t)g << 16); + result |= ((uint16_t)b << 8); + return result; +} + +/** + * @brief Encode RGB color at default brightness into 32-bit RGBb + * @param r color red from 0..255 + * @param g color green from 0..255 + * @param b color blue from 0..255 + * @return color encoded as 32-bit RGBb (red | green | blue | brightness) + */ +uint32_t blinkt_rgb(uint8_t r, uint8_t g, uint8_t b) +{ + return blinkt_rgbb(r, g, b, BLINKT_DEFAULT_BRIGHTNESS); +} + +/** + * @brief Write a byte via shift register to LEDs + * @param byte to be written + */ +inline static void write_byte(uint8_t byte) +{ + int n; + for (n = 0; n < 8; n++) { + gpio_write(Blinkt_Pi, BLINKT_MOSI, (byte & (1 << (7 - n))) > 0); + gpio_write(Blinkt_Pi, BLINKT_SCLK, 1); + gpio_write(Blinkt_Pi, BLINKT_SCLK, 0); + } +} + +/** + * @brief Shift LED values to the actual LEDs via shift registers + */ +void blinkt_show(void) +{ + int x; + + write_byte(0); + write_byte(0); + write_byte(0); + write_byte(0); + for (x = 0; x < BLINKT_NUM_LEDS; x++) { + write_byte(0xE0 | (Blinkt_LED[x] & 0x1F)); + write_byte((Blinkt_LED[x] >> 8) & 0xFF); + write_byte((Blinkt_LED[x] >> 16) & 0xFF); + write_byte((Blinkt_LED[x] >> 24) & 0xFF); + } + write_byte(0xff); +} + +/** + * @brief Disable the GPIO hardware to the Blinkt! board + */ +void blinkt_stop(void) +{ + /* Disconnect from local Pi. */ + pigpio_stop(Blinkt_Pi); + printf("PiGPIO stopped\n"); +} + +/** + * @brief Initialize the GPIO hardware for the Blinkt! board + */ +int blinkt_init(void) +{ + char *optHost = NULL; + char *optPort = NULL; + + /* Connect to local Pi. */ + Blinkt_Pi = pigpio_start(optHost, optPort); + if (Blinkt_Pi < 0) { + perror("PiGPIO failed to start!"); + return -1; + } + set_mode(Blinkt_Pi, BLINKT_MOSI, PI_OUTPUT); + gpio_write(Blinkt_Pi, BLINKT_MOSI, 0); + set_mode(Blinkt_Pi, BLINKT_SCLK, PI_OUTPUT); + gpio_write(Blinkt_Pi, BLINKT_SCLK, 0); + blinkt_clear(); + printf("PiGPIO started\n"); + + return 0; +} + +static int test_column = 0; +static int test_z, test_y; +/** + * @brief Test the Blinkt! board with a simple changing pattern + */ +void blinkt_test_task(void) +{ + for (test_z = 0; test_z < BLINKT_NUM_LEDS; test_z++) { + switch (test_column) { + case 0: + blinkt_set_pixel_uint32(test_z, blinkt_rgb(test_y, 0, 0)); + break; + case 1: + blinkt_set_pixel_uint32(test_z, blinkt_rgb(0, test_y, 0)); + break; + case 2: + blinkt_set_pixel_uint32(test_z, blinkt_rgb(0, 0, test_y)); + break; + default: + break; + } + } + blinkt_show(); + test_y += 1; + if (test_y > 254) + test_column++; + test_column %= 3; + test_y %= 255; +} diff --git a/apps/blinkt/blinkt.h b/apps/blinkt/blinkt.h new file mode 100644 index 00000000..1d312811 --- /dev/null +++ b/apps/blinkt/blinkt.h @@ -0,0 +1,23 @@ +/** + * @file + * @brief API for Blinkt! daughter board for Raspberry Pi + * + * SPDX-License-Identifier: MIT + */ +#ifndef BLINKT_H +#define BLINKT_H + +void blinkt_clear(void); +void blinkt_set_pixel_uint32(uint8_t led, uint32_t color); +void blinkt_set_pixel(uint8_t led, uint8_t r, uint8_t g, uint8_t b); +void blinkt_set_pixel_brightness(uint8_t led, uint8_t brightness); +uint32_t blinkt_rgbb(uint8_t r, uint8_t g, uint8_t b, uint8_t brightness); +uint32_t blinkt_rgb(uint8_t r, uint8_t g, uint8_t b); +void blinkt_stop(void); +void blinkt_show(void); +uint8_t blinkt_led_count(void); + +int blinkt_init(void); +void blinkt_test_task(void); + +#endif diff --git a/apps/blinkt/device.c b/apps/blinkt/device.c new file mode 100644 index 00000000..262f914a --- /dev/null +++ b/apps/blinkt/device.c @@ -0,0 +1,1951 @@ +/************************************************************************** + * + * Copyright (C) 2005,2006,2009 Steve Karg + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + *********************************************************************/ + +/** @file device.c Base "class" for handling all BACnet objects belonging + * to a BACnet device, as well as Device-specific properties. */ + +#include +#include +#include /* for memmove */ +#include "bacnet/bacdef.h" +#include "bacnet/bacdcode.h" +#include "bacnet/bacenum.h" +#include "bacnet/bacapp.h" +#include "bacnet/config.h" /* the custom stuff */ +#include "bacnet/datetime.h" +#include "bacnet/apdu.h" +#include "bacnet/wp.h" /* WriteProperty handling */ +#include "bacnet/rp.h" /* ReadProperty handling */ +#include "bacnet/dcc.h" /* DeviceCommunicationControl handling */ +#include "bacnet/version.h" +#include "bacnet/basic/object/device.h" /* me */ +#include "bacnet/basic/services.h" +#include "bacnet/datalink/datalink.h" +#include "bacnet/basic/binding/address.h" +/* include the device object */ +#include "bacnet/basic/object/device.h" +#include "bacnet/basic/object/lc.h" +#if (BACNET_PROTOCOL_REVISION >= 14) +#include "bacnet/basic/object/channel.h" +#include "bacnet/basic/object/lo.h" +#endif +#if (BACNET_PROTOCOL_REVISION >= 17) +#include "bacnet/basic/object/netport.h" +#endif +#if (BACNET_PROTOCOL_REVISION >= 24) +#include "bacnet/basic/object/color_object.h" +#include "bacnet/basic/object/color_temperature.h" +#endif + +/* local forward (semi-private) and external prototypes */ +int Device_Read_Property_Local(BACNET_READ_PROPERTY_DATA *rpdata); +bool Device_Write_Property_Local(BACNET_WRITE_PROPERTY_DATA *wp_data); +extern int Routed_Device_Read_Property_Local(BACNET_READ_PROPERTY_DATA *rpdata); +extern bool Routed_Device_Write_Property_Local( + BACNET_WRITE_PROPERTY_DATA *wp_data); + +/* may be overridden by outside table */ +static object_functions_t *Object_Table; + +static object_functions_t My_Object_Table[] = { + { OBJECT_DEVICE, NULL /* Init - don't init Device or it will recourse! */, + Device_Count, Device_Index_To_Instance, + Device_Valid_Object_Instance_Number, Device_Object_Name, + Device_Read_Property_Local, Device_Write_Property_Local, + Device_Property_Lists, DeviceGetRRInfo, NULL /* Iterator */, + NULL /* Value_Lists */, NULL /* COV */, NULL /* COV Clear */, + NULL /* Intrinsic Reporting */, NULL /* Add_List_Element */, + NULL /* Remove_List_Element */, + NULL /* Create */, NULL /* Delete */, NULL /* Timer */ }, +#if (BACNET_PROTOCOL_REVISION >= 17) + { OBJECT_NETWORK_PORT, Network_Port_Init, Network_Port_Count, + Network_Port_Index_To_Instance, Network_Port_Valid_Instance, + Network_Port_Object_Name, Network_Port_Read_Property, + Network_Port_Write_Property, Network_Port_Property_Lists, + NULL /* ReadRangeInfo */, NULL /* Iterator */, NULL /* Value_Lists */, + NULL /* COV */, NULL /* COV Clear */, NULL /* Intrinsic Reporting */, + NULL /* Add_List_Element */, NULL /* Remove_List_Element */, + NULL /* Create */, NULL /* Delete */, NULL /* Timer */ }, +#endif + { OBJECT_LOAD_CONTROL, Load_Control_Init, Load_Control_Count, + Load_Control_Index_To_Instance, Load_Control_Valid_Instance, + Load_Control_Object_Name, Load_Control_Read_Property, + Load_Control_Write_Property, Load_Control_Property_Lists, + NULL /* ReadRangeInfo */, NULL /* Iterator */, NULL /* Value_Lists */, + NULL /* COV */, NULL /* COV Clear */, NULL /* Intrinsic Reporting */, + NULL /* Add_List_Element */, NULL /* Remove_List_Element */, + NULL /* Create */, NULL /* Delete */ , NULL /* Timer */ }, +#if (BACNET_PROTOCOL_REVISION >= 14) + { OBJECT_LIGHTING_OUTPUT, Lighting_Output_Init, Lighting_Output_Count, + Lighting_Output_Index_To_Instance, Lighting_Output_Valid_Instance, + Lighting_Output_Object_Name, Lighting_Output_Read_Property, + Lighting_Output_Write_Property, Lighting_Output_Property_Lists, + NULL /* ReadRangeInfo */, NULL /* Iterator */, NULL /* Value_Lists */, + NULL /* COV */, NULL /* COV Clear */, NULL /* Intrinsic Reporting */, + NULL /* Add_List_Element */, NULL /* Remove_List_Element */, + Lighting_Output_Create, Lighting_Output_Delete, Lighting_Output_Timer}, + { OBJECT_CHANNEL, Channel_Init, Channel_Count, Channel_Index_To_Instance, + Channel_Valid_Instance, Channel_Object_Name, Channel_Read_Property, + Channel_Write_Property, Channel_Property_Lists, + NULL /* ReadRangeInfo */, NULL /* Iterator */, NULL /* Value_Lists */, + NULL /* COV */, NULL /* COV Clear */, NULL /* Intrinsic Reporting */, + NULL /* Add_List_Element */, NULL /* Remove_List_Element */, + Channel_Create, Channel_Delete, NULL /* Timer */ }, +#endif +#if (BACNET_PROTOCOL_REVISION >= 24) + { OBJECT_COLOR, Color_Init, Color_Count, Color_Index_To_Instance, + Color_Valid_Instance, Color_Object_Name, Color_Read_Property, + Color_Write_Property, Color_Property_Lists, NULL /* ReadRangeInfo */, + NULL /* Iterator */, NULL /* Value_Lists */, NULL /* COV */, + NULL /* COV Clear */, NULL /* Intrinsic Reporting */, + NULL /* Add_List_Element */, NULL /* Remove_List_Element */, + Color_Create, Color_Delete, Color_Timer}, + { OBJECT_COLOR_TEMPERATURE, Color_Temperature_Init, Color_Temperature_Count, + Color_Temperature_Index_To_Instance, Color_Temperature_Valid_Instance, + Color_Temperature_Object_Name, Color_Temperature_Read_Property, + Color_Temperature_Write_Property, Color_Temperature_Property_Lists, + NULL /* ReadRangeInfo */, NULL /* Iterator */, NULL /* Value_Lists */, + NULL /* COV */, NULL /* COV Clear */, NULL /* Intrinsic Reporting */, + NULL /* Add_List_Element */, NULL /* Remove_List_Element */, + Color_Temperature_Create, Color_Temperature_Delete, + Color_Temperature_Timer}, +#endif + { MAX_BACNET_OBJECT_TYPE, NULL /* Init */, NULL /* Count */, + NULL /* Index_To_Instance */, NULL /* Valid_Instance */, + NULL /* Object_Name */, NULL /* Read_Property */, + NULL /* Write_Property */, NULL /* Property_Lists */, + NULL /* ReadRangeInfo */, NULL /* Iterator */, NULL /* Value_Lists */, + NULL /* COV */, NULL /* COV Clear */, NULL /* Intrinsic Reporting */, + NULL /* Add_List_Element */, NULL /* Remove_List_Element */, + NULL /* Create */, NULL /* Delete */, NULL /* Timer */ } +}; + +/** Glue function to let the Device object, when called by a handler, + * lookup which Object type needs to be invoked. + * @ingroup ObjHelpers + * @param Object_Type [in] The type of BACnet Object the handler wants to + * access. + * @return Pointer to the group of object helper functions that implement this + * type of Object. + */ +static struct object_functions *Device_Objects_Find_Functions( + BACNET_OBJECT_TYPE Object_Type) +{ + struct object_functions *pObject = NULL; + + pObject = Object_Table; + while (pObject->Object_Type < MAX_BACNET_OBJECT_TYPE) { + /* handle each object type */ + if (pObject->Object_Type == Object_Type) { + return (pObject); + } + pObject++; + } + + return (NULL); +} + +/** Try to find a rr_info_function helper function for the requested object + * type. + * @ingroup ObjIntf + * + * @param object_type [in] The type of BACnet Object the handler wants to + * access. + * @return Pointer to the object helper function that implements the + * ReadRangeInfo function, Object_RR_Info, for this type of Object on + * success, else a NULL pointer if the type of Object isn't supported + * or doesn't have a ReadRangeInfo function. + */ +rr_info_function Device_Objects_RR_Info(BACNET_OBJECT_TYPE object_type) +{ + struct object_functions *pObject = NULL; + + pObject = Device_Objects_Find_Functions(object_type); + return (pObject != NULL ? pObject->Object_RR_Info : NULL); +} + +/** For a given object type, returns the special property list. + * This function is used for ReadPropertyMultiple calls which want + * just Required, just Optional, or All properties. + * @ingroup ObjIntf + * + * @param object_type [in] The desired BACNET_OBJECT_TYPE whose properties + * are to be listed. + * @param pPropertyList [out] Reference to the structure which will, on return, + * list, separately, the Required, Optional, and Proprietary object + * properties with their counts. + */ +void Device_Objects_Property_List(BACNET_OBJECT_TYPE object_type, + uint32_t object_instance, + struct special_property_list_t *pPropertyList) +{ + struct object_functions *pObject = NULL; + + (void)object_instance; + pPropertyList->Required.pList = NULL; + pPropertyList->Optional.pList = NULL; + pPropertyList->Proprietary.pList = NULL; + + /* If we can find an entry for the required object type + * and there is an Object_List_RPM fn ptr then call it + * to populate the pointers to the individual list counters. + */ + + pObject = Device_Objects_Find_Functions(object_type); + if ((pObject != NULL) && (pObject->Object_RPM_List != NULL)) { + pObject->Object_RPM_List(&pPropertyList->Required.pList, + &pPropertyList->Optional.pList, &pPropertyList->Proprietary.pList); + } + + /* Fetch the counts if available otherwise zero them */ + pPropertyList->Required.count = pPropertyList->Required.pList == NULL + ? 0 + : property_list_count(pPropertyList->Required.pList); + + pPropertyList->Optional.count = pPropertyList->Optional.pList == NULL + ? 0 + : property_list_count(pPropertyList->Optional.pList); + + pPropertyList->Proprietary.count = pPropertyList->Proprietary.pList == NULL + ? 0 + : property_list_count(pPropertyList->Proprietary.pList); + + return; +} + +/* These three arrays are used by the ReadPropertyMultiple handler */ +static const int Device_Properties_Required[] = { PROP_OBJECT_IDENTIFIER, + PROP_OBJECT_NAME, PROP_OBJECT_TYPE, PROP_SYSTEM_STATUS, PROP_VENDOR_NAME, + PROP_VENDOR_IDENTIFIER, PROP_MODEL_NAME, PROP_FIRMWARE_REVISION, + PROP_APPLICATION_SOFTWARE_VERSION, PROP_PROTOCOL_VERSION, + PROP_PROTOCOL_REVISION, PROP_PROTOCOL_SERVICES_SUPPORTED, + PROP_PROTOCOL_OBJECT_TYPES_SUPPORTED, PROP_OBJECT_LIST, + PROP_MAX_APDU_LENGTH_ACCEPTED, PROP_SEGMENTATION_SUPPORTED, + PROP_APDU_TIMEOUT, PROP_NUMBER_OF_APDU_RETRIES, PROP_DEVICE_ADDRESS_BINDING, + PROP_DATABASE_REVISION, -1 }; + +static const int Device_Properties_Optional[] = { +#if defined(BACDL_MSTP) + PROP_MAX_MASTER, PROP_MAX_INFO_FRAMES, +#endif + PROP_DESCRIPTION, PROP_LOCAL_TIME, PROP_UTC_OFFSET, PROP_LOCAL_DATE, + PROP_DAYLIGHT_SAVINGS_STATUS, PROP_LOCATION, PROP_ACTIVE_COV_SUBSCRIPTIONS, +#if defined(BACNET_TIME_MASTER) + PROP_TIME_SYNCHRONIZATION_RECIPIENTS, PROP_TIME_SYNCHRONIZATION_INTERVAL, + PROP_ALIGN_INTERVALS, PROP_INTERVAL_OFFSET, +#endif + -1 +}; + +static const int Device_Properties_Proprietary[] = { -1 }; + +void Device_Property_Lists( + const int **pRequired, const int **pOptional, const int **pProprietary) +{ + if (pRequired) { + *pRequired = Device_Properties_Required; + } + if (pOptional) { + *pOptional = Device_Properties_Optional; + } + if (pProprietary) { + *pProprietary = Device_Properties_Proprietary; + } + + return; +} + +/* note: you really only need to define variables for + properties that are writable or that may change. + The properties that are constant can be hard coded + into the read-property encoding. */ + +static uint32_t Object_Instance_Number = 260001; +static BACNET_CHARACTER_STRING My_Object_Name; +static BACNET_DEVICE_STATUS System_Status = STATUS_OPERATIONAL; +static char *Vendor_Name = BACNET_VENDOR_NAME; +static uint16_t Vendor_Identifier = BACNET_VENDOR_ID; +static char Model_Name[MAX_DEV_MOD_LEN + 1] = "GNU"; +static char Application_Software_Version[MAX_DEV_VER_LEN + 1] = "1.0"; +static const char *BACnet_Version = BACNET_VERSION_TEXT; +static char Location[MAX_DEV_LOC_LEN + 1] = "USA"; +static char Description[MAX_DEV_DESC_LEN + 1] = "server"; +/* static uint8_t Protocol_Version = 1; - constant, not settable */ +/* static uint8_t Protocol_Revision = 4; - constant, not settable */ +/* Protocol_Services_Supported - dynamically generated */ +/* Protocol_Object_Types_Supported - in RP encoding */ +/* Object_List - dynamically generated */ +/* static BACNET_SEGMENTATION Segmentation_Supported = SEGMENTATION_NONE; */ +/* static uint8_t Max_Segments_Accepted = 0; */ +/* VT_Classes_Supported */ +/* Active_VT_Sessions */ +static BACNET_TIME Local_Time; /* rely on OS, if there is one */ +static BACNET_DATE Local_Date; /* rely on OS, if there is one */ +/* NOTE: BACnet UTC Offset is inverse of common practice. + If your UTC offset is -5hours of GMT, + then BACnet UTC offset is +5hours. + BACnet UTC offset is expressed in minutes. */ +static int16_t UTC_Offset = 5 * 60; +static bool Daylight_Savings_Status = false; /* rely on OS */ +#if defined(BACNET_TIME_MASTER) +static bool Align_Intervals; +static uint32_t Interval_Minutes; +static uint32_t Interval_Offset_Minutes; +/* Time_Synchronization_Recipients */ +#endif +/* List_Of_Session_Keys */ +/* Max_Master - rely on MS/TP subsystem, if there is one */ +/* Max_Info_Frames - rely on MS/TP subsystem, if there is one */ +/* Device_Address_Binding - required, but relies on binding cache */ +static uint32_t Database_Revision = 0; +/* Configuration_Files */ +/* Last_Restore_Time */ +/* Backup_Failure_Timeout */ +/* Active_COV_Subscriptions */ +/* Slave_Proxy_Enable */ +/* Manual_Slave_Address_Binding */ +/* Auto_Slave_Discovery */ +/* Slave_Address_Binding */ +/* Profile_Name */ +static BACNET_REINITIALIZED_STATE Reinitialize_State = BACNET_REINIT_IDLE; +static const char *Reinit_Password = "filister"; + +/** Commands a Device re-initialization, to a given state. + * The request's password must match for the operation to succeed. + * This implementation provides a framework, but doesn't + * actually *DO* anything. + * @note You could use a mix of states and passwords to multiple outcomes. + * @note You probably want to restart *after* the simple ack has been sent + * from the return handler, so just set a local flag here. + * @ingroup ObjIntf + * + * @param rd_data [in,out] The information from the RD request. + * On failure, the error class and code will be set. + * @return True if succeeds (password is correct), else False. + */ +bool Device_Reinitialize(BACNET_REINITIALIZE_DEVICE_DATA *rd_data) +{ + bool status = false; + + /* From 16.4.1.1.2 Password + This optional parameter shall be a CharacterString of up to + 20 characters. For those devices that require the password as a + protection, the service request shall be denied if the parameter + is absent or if the password is incorrect. For those devices that + do not require a password, this parameter shall be ignored.*/ + if (characterstring_length(&rd_data->password) > 20) { + rd_data->error_class = ERROR_CLASS_SERVICES; + rd_data->error_code = ERROR_CODE_PARAMETER_OUT_OF_RANGE; + } else if (characterstring_ansi_same(&rd_data->password, Reinit_Password)) { + /* Note: you could use a mix of state and password to + accomplish multiple things before restarting */ + switch (rd_data->state) { + case BACNET_REINIT_COLDSTART: + case BACNET_REINIT_WARMSTART: + dcc_set_status_duration(COMMUNICATION_ENABLE, 0); + /* note: you probably want to restart *after* the + simple ack has been sent from the return handler + so just set a flag from here */ + Reinitialize_State = rd_data->state; + status = true; + break; + case BACNET_REINIT_STARTBACKUP: + case BACNET_REINIT_ENDBACKUP: + case BACNET_REINIT_STARTRESTORE: + case BACNET_REINIT_ENDRESTORE: + case BACNET_REINIT_ABORTRESTORE: + if (dcc_communication_disabled()) { + rd_data->error_class = ERROR_CLASS_SERVICES; + rd_data->error_code = ERROR_CODE_COMMUNICATION_DISABLED; + } else { + rd_data->error_class = ERROR_CLASS_SERVICES; + rd_data->error_code = + ERROR_CODE_OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED; + } + break; + default: + rd_data->error_class = ERROR_CLASS_SERVICES; + rd_data->error_code = ERROR_CODE_PARAMETER_OUT_OF_RANGE; + break; + } + } else { + rd_data->error_class = ERROR_CLASS_SECURITY; + rd_data->error_code = ERROR_CODE_PASSWORD_FAILURE; + } + + return status; +} + +BACNET_REINITIALIZED_STATE Device_Reinitialized_State(void) +{ + return Reinitialize_State; +} + +unsigned Device_Count(void) +{ + return 1; +} + +uint32_t Device_Index_To_Instance(unsigned index) +{ + (void)index; + return Object_Instance_Number; +} + +/* methods to manipulate the data */ + +/** Return the Object Instance number for our (single) Device Object. + * This is a key function, widely invoked by the handler code, since + * it provides "our" (ie, local) address. + * @ingroup ObjIntf + * @return The Instance number used in the BACNET_OBJECT_ID for the Device. + */ +uint32_t Device_Object_Instance_Number(void) +{ + return Object_Instance_Number; +} + +bool Device_Set_Object_Instance_Number(uint32_t object_id) +{ + bool status = true; /* return value */ + + if (object_id <= BACNET_MAX_INSTANCE) { + /* Make the change and update the database revision */ + Object_Instance_Number = object_id; + Device_Inc_Database_Revision(); + } else { + status = false; + } + + return status; +} + +bool Device_Valid_Object_Instance_Number(uint32_t object_id) +{ + return (Object_Instance_Number == object_id); +} + +bool Device_Object_Name( + uint32_t object_instance, BACNET_CHARACTER_STRING *object_name) +{ + bool status = false; + + if (object_instance == Object_Instance_Number) { + status = characterstring_copy(object_name, &My_Object_Name); + } + + return status; +} + +bool Device_Set_Object_Name(BACNET_CHARACTER_STRING *object_name) +{ + bool status = false; /*return value */ + + if (!characterstring_same(&My_Object_Name, object_name)) { + /* Make the change and update the database revision */ + status = characterstring_copy(&My_Object_Name, object_name); + Device_Inc_Database_Revision(); + } + + return status; +} + +bool Device_Object_Name_ANSI_Init(const char *value) +{ + return characterstring_init_ansi(&My_Object_Name, value); +} + +BACNET_DEVICE_STATUS Device_System_Status(void) +{ + return System_Status; +} + +int Device_Set_System_Status(BACNET_DEVICE_STATUS status, bool local) +{ + int result = 0; /*return value - 0 = ok, -1 = bad value, -2 = not allowed */ + + /* We limit the options available depending on whether the source is + * internal or external. */ + if (local) { + switch (status) { + case STATUS_OPERATIONAL: + case STATUS_OPERATIONAL_READ_ONLY: + case STATUS_DOWNLOAD_REQUIRED: + case STATUS_DOWNLOAD_IN_PROGRESS: + case STATUS_NON_OPERATIONAL: + System_Status = status; + break; + + /* Don't support backup at present so don't allow setting */ + case STATUS_BACKUP_IN_PROGRESS: + result = -2; + break; + + default: + result = -1; + break; + } + } else { + switch (status) { + /* Allow these for the moment as a way to easily alter + * overall device operation. The lack of password protection + * or other authentication makes allowing writes to this + * property a risky facility to provide. + */ + case STATUS_OPERATIONAL: + case STATUS_OPERATIONAL_READ_ONLY: + case STATUS_NON_OPERATIONAL: + System_Status = status; + break; + + /* Don't allow outsider set this - it should probably + * be set if the device config is incomplete or + * corrupted or perhaps after some sort of operator + * wipe operation. + */ + case STATUS_DOWNLOAD_REQUIRED: + /* Don't allow outsider set this - it should be set + * internally at the start of a multi packet download + * perhaps indirectly via PT or WF to a config file. + */ + case STATUS_DOWNLOAD_IN_PROGRESS: + /* Don't support backup at present so don't allow setting */ + case STATUS_BACKUP_IN_PROGRESS: + result = -2; + break; + + default: + result = -1; + break; + } + } + + return (result); +} + +const char *Device_Vendor_Name(void) +{ + return Vendor_Name; +} + +/** Returns the Vendor ID for this Device. + * See the assignments at + * http://www.bacnet.org/VendorID/BACnet%20Vendor%20IDs.htm + * @return The Vendor ID of this Device. + */ +uint16_t Device_Vendor_Identifier(void) +{ + return Vendor_Identifier; +} + +void Device_Set_Vendor_Identifier(uint16_t vendor_id) +{ + Vendor_Identifier = vendor_id; +} + +const char *Device_Model_Name(void) +{ + return Model_Name; +} + +bool Device_Set_Model_Name(const char *name, size_t length) +{ + bool status = false; /*return value */ + + if (length < sizeof(Model_Name)) { + memmove(Model_Name, name, length); + Model_Name[length] = 0; + status = true; + } + + return status; +} + +const char *Device_Firmware_Revision(void) +{ + return BACnet_Version; +} + +const char *Device_Application_Software_Version(void) +{ + return Application_Software_Version; +} + +bool Device_Set_Application_Software_Version(const char *name, size_t length) +{ + bool status = false; /*return value */ + + if (length < sizeof(Application_Software_Version)) { + memmove(Application_Software_Version, name, length); + Application_Software_Version[length] = 0; + status = true; + } + + return status; +} + +const char *Device_Description(void) +{ + return Description; +} + +bool Device_Set_Description(const char *name, size_t length) +{ + bool status = false; /*return value */ + + if (length < sizeof(Description)) { + memmove(Description, name, length); + Description[length] = 0; + status = true; + } + + return status; +} + +const char *Device_Location(void) +{ + return Location; +} + +bool Device_Set_Location(const char *name, size_t length) +{ + bool status = false; /*return value */ + + if (length < sizeof(Location)) { + memmove(Location, name, length); + Location[length] = 0; + status = true; + } + + return status; +} + +uint8_t Device_Protocol_Version(void) +{ + return BACNET_PROTOCOL_VERSION; +} + +uint8_t Device_Protocol_Revision(void) +{ + return BACNET_PROTOCOL_REVISION; +} + +BACNET_SEGMENTATION Device_Segmentation_Supported(void) +{ + return SEGMENTATION_NONE; +} + +uint32_t Device_Database_Revision(void) +{ + return Database_Revision; +} + +void Device_Set_Database_Revision(uint32_t revision) +{ + Database_Revision = revision; +} + +/* + * Shortcut for incrementing database revision as this is potentially + * the most common operation if changing object names and ids is + * implemented. + */ +void Device_Inc_Database_Revision(void) +{ + Database_Revision++; +} + +/** Get the total count of objects supported by this Device Object. + * @note Since many network clients depend on the object list + * for discovery, it must be consistent! + * @return The count of objects, for all supported Object types. + */ +unsigned Device_Object_List_Count(void) +{ + unsigned count = 0; /* number of objects */ + struct object_functions *pObject = NULL; + + /* initialize the default return values */ + pObject = Object_Table; + while (pObject->Object_Type < MAX_BACNET_OBJECT_TYPE) { + if (pObject->Object_Count) { + count += pObject->Object_Count(); + } + pObject++; + } + + return count; +} + +/** Lookup the Object at the given array index in the Device's Object List. + * Even though we don't keep a single linear array of objects in the Device, + * this method acts as though we do and works through a virtual, concatenated + * array of all of our object type arrays. + * + * @param array_index [in] The desired array index (1 to N) + * @param object_type [out] The object's type, if found. + * @param instance [out] The object's instance number, if found. + * @return True if found, else false. + */ +bool Device_Object_List_Identifier( + uint32_t array_index, BACNET_OBJECT_TYPE *object_type, uint32_t *instance) +{ + bool status = false; + uint32_t count = 0; + uint32_t object_index = 0; + uint32_t temp_index = 0; + struct object_functions *pObject = NULL; + + /* array index zero is length - so invalid */ + if (array_index == 0) { + return status; + } + object_index = array_index - 1; + /* initialize the default return values */ + pObject = Object_Table; + while (pObject->Object_Type < MAX_BACNET_OBJECT_TYPE) { + if (pObject->Object_Count) { + object_index -= count; + count = pObject->Object_Count(); + if (object_index < count) { + /* Use the iterator function if available otherwise + * look for the index to instance to get the ID */ + if (pObject->Object_Iterator) { + /* First find the first object */ + temp_index = pObject->Object_Iterator(~(unsigned)0); + /* Then step through the objects to find the nth */ + while (object_index != 0) { + temp_index = pObject->Object_Iterator(temp_index); + object_index--; + } + /* set the object_index up before falling through to next + * bit */ + object_index = temp_index; + } + if (pObject->Object_Index_To_Instance) { + *object_type = pObject->Object_Type; + *instance = pObject->Object_Index_To_Instance(object_index); + status = true; + break; + } + } + } + pObject++; + } + + return status; +} + +/** + * @brief Encode a BACnetARRAY property element + * @param object_instance [in] BACnet network port object instance number + * @param array_index [in] array index requested: + * 0 to N for individual array members + * @param apdu [out] Buffer in which the APDU contents are built, or NULL to + * return the length of buffer if it had been built + * @return The length of the apdu encoded or + * BACNET_STATUS_ERROR for ERROR_CODE_INVALID_ARRAY_INDEX + */ +int Device_Object_List_Element_Encode( + uint32_t object_instance, BACNET_ARRAY_INDEX array_index, uint8_t *apdu) +{ + int apdu_len = BACNET_STATUS_ERROR; + BACNET_OBJECT_TYPE object_type; + uint32_t instance; + bool found; + + if (object_instance == Device_Object_Instance_Number()) { + /* single element is zero based, add 1 for BACnetARRAY which is one + * based */ + array_index++; + found = + Device_Object_List_Identifier(array_index, &object_type, &instance); + if (found) { + apdu_len = + encode_application_object_id(apdu, object_type, instance); + } + } + + return apdu_len; +} + +/** Determine if we have an object with the given object_name. + * If the object_type and object_instance pointers are not null, + * and the lookup succeeds, they will be given the resulting values. + * @param object_name [in] The desired Object Name to look for. + * @param object_type [out] The BACNET_OBJECT_TYPE of the matching Object. + * @param object_instance [out] The object instance number of the matching + * Object. + * @return True on success or else False if not found. + */ +bool Device_Valid_Object_Name(BACNET_CHARACTER_STRING *object_name1, + BACNET_OBJECT_TYPE *object_type, + uint32_t *object_instance) +{ + bool found = false; + BACNET_OBJECT_TYPE type = OBJECT_NONE; + uint32_t instance; + uint32_t max_objects = 0, i = 0; + bool check_id = false; + BACNET_CHARACTER_STRING object_name2; + struct object_functions *pObject = NULL; + + max_objects = Device_Object_List_Count(); + for (i = 1; i <= max_objects; i++) { + check_id = Device_Object_List_Identifier(i, &type, &instance); + if (check_id) { + pObject = Device_Objects_Find_Functions(type); + if ((pObject != NULL) && (pObject->Object_Name != NULL) && + (pObject->Object_Name(instance, &object_name2) && + characterstring_same(object_name1, &object_name2))) { + found = true; + if (object_type) { + *object_type = type; + } + if (object_instance) { + *object_instance = instance; + } + break; + } + } + } + + return found; +} + +/** Determine if we have an object of this type and instance number. + * @param object_type [in] The desired BACNET_OBJECT_TYPE + * @param object_instance [in] The object instance number to be looked up. + * @return True if found, else False if no such Object in this device. + */ +bool Device_Valid_Object_Id( + BACNET_OBJECT_TYPE object_type, uint32_t object_instance) +{ + bool status = false; /* return value */ + struct object_functions *pObject = NULL; + + pObject = Device_Objects_Find_Functions(object_type); + if ((pObject != NULL) && (pObject->Object_Valid_Instance != NULL)) { + status = pObject->Object_Valid_Instance(object_instance); + } + + return status; +} + +/** Copy a child object's object_name value, given its ID. + * @param object_type [in] The BACNET_OBJECT_TYPE of the child Object. + * @param object_instance [in] The object instance number of the child Object. + * @param object_name [out] The Object Name found for this child Object. + * @return True on success or else False if not found. + */ +bool Device_Object_Name_Copy(BACNET_OBJECT_TYPE object_type, + uint32_t object_instance, + BACNET_CHARACTER_STRING *object_name) +{ + struct object_functions *pObject = NULL; + bool found = false; + + pObject = Device_Objects_Find_Functions(object_type); + if ((pObject != NULL) && (pObject->Object_Name != NULL)) { + found = pObject->Object_Name(object_instance, object_name); + } + + return found; +} + +static void Update_Current_Time(void) +{ + datetime_local( + &Local_Date, &Local_Time, &UTC_Offset, &Daylight_Savings_Status); +} + +void Device_getCurrentDateTime(BACNET_DATE_TIME *DateTime) +{ + Update_Current_Time(); + + DateTime->date = Local_Date; + DateTime->time = Local_Time; +} + +int32_t Device_UTC_Offset(void) +{ + Update_Current_Time(); + + return UTC_Offset; +} + +void Device_UTC_Offset_Set(int16_t offset) +{ + UTC_Offset = offset; +} + +bool Device_Daylight_Savings_Status(void) +{ + return Daylight_Savings_Status; +} + +#if defined(BACNET_TIME_MASTER) +/** + * Sets the time sync interval in minutes + * + * @param flag + * This property, of type BOOLEAN, specifies whether (TRUE) + * or not (FALSE) clock-aligned periodic time synchronization is + * enabled. If periodic time synchronization is enabled and the + * time synchronization interval is a factor of (divides without + * remainder) an hour or day, then the beginning of the period + * specified for time synchronization shall be aligned to the hour or + * day, respectively. If this property is present, it shall be writable. + */ +bool Device_Align_Intervals_Set(bool flag) +{ + Align_Intervals = flag; + + return true; +} + +bool Device_Align_Intervals(void) +{ + return Align_Intervals; +} + +/** + * Sets the time sync interval in minutes + * + * @param minutes + * This property, of type Unsigned, specifies the periodic + * interval in minutes at which TimeSynchronization and + * UTCTimeSynchronization requests shall be sent. If this + * property has a value of zero, then periodic time synchronization is + * disabled. If this property is present, it shall be writable. + */ +bool Device_Time_Sync_Interval_Set(uint32_t minutes) +{ + Interval_Minutes = minutes; + + return true; +} + +uint32_t Device_Time_Sync_Interval(void) +{ + return Interval_Minutes; +} + +/** + * Sets the time sync interval offset value. + * + * @param minutes + * This property, of type Unsigned, specifies the offset in + * minutes from the beginning of the period specified for time + * synchronization until the actual time synchronization requests + * are sent. The offset used shall be the value of Interval_Offset + * modulo the value of Time_Synchronization_Interval; + * e.g., if Interval_Offset has the value 31 and + * Time_Synchronization_Interval is 30, the offset used shall be 1. + * Interval_Offset shall have no effect if Align_Intervals is + * FALSE. If this property is present, it shall be writable. + */ +bool Device_Interval_Offset_Set(uint32_t minutes) +{ + Interval_Offset_Minutes = minutes; + + return true; +} + +uint32_t Device_Interval_Offset(void) +{ + return Interval_Offset_Minutes; +} +#endif + +/* return the length of the apdu encoded or BACNET_STATUS_ERROR for error or + BACNET_STATUS_ABORT for abort message */ +int Device_Read_Property_Local(BACNET_READ_PROPERTY_DATA *rpdata) +{ + int apdu_len = 0; /* return value */ + BACNET_BIT_STRING bit_string = { 0 }; + BACNET_CHARACTER_STRING char_string = { 0 }; + uint32_t i = 0; + uint32_t count = 0; + uint8_t *apdu = NULL; + struct object_functions *pObject = NULL; + uint16_t apdu_max = 0; + + if ((rpdata == NULL) || (rpdata->application_data == NULL) || + (rpdata->application_data_len == 0)) { + return 0; + } + apdu = rpdata->application_data; + apdu_max = rpdata->application_data_len; + switch (rpdata->object_property) { + case PROP_OBJECT_IDENTIFIER: + apdu_len = encode_application_object_id( + &apdu[0], OBJECT_DEVICE, Object_Instance_Number); + break; + case PROP_OBJECT_NAME: + apdu_len = + encode_application_character_string(&apdu[0], &My_Object_Name); + break; + case PROP_OBJECT_TYPE: + apdu_len = encode_application_enumerated(&apdu[0], OBJECT_DEVICE); + break; + case PROP_DESCRIPTION: + characterstring_init_ansi(&char_string, Description); + apdu_len = + encode_application_character_string(&apdu[0], &char_string); + break; + case PROP_SYSTEM_STATUS: + apdu_len = encode_application_enumerated(&apdu[0], System_Status); + break; + case PROP_VENDOR_NAME: + characterstring_init_ansi(&char_string, Vendor_Name); + apdu_len = + encode_application_character_string(&apdu[0], &char_string); + break; + case PROP_VENDOR_IDENTIFIER: + apdu_len = encode_application_unsigned(&apdu[0], Vendor_Identifier); + break; + case PROP_MODEL_NAME: + characterstring_init_ansi(&char_string, Model_Name); + apdu_len = + encode_application_character_string(&apdu[0], &char_string); + break; + case PROP_FIRMWARE_REVISION: + characterstring_init_ansi(&char_string, BACnet_Version); + apdu_len = + encode_application_character_string(&apdu[0], &char_string); + break; + case PROP_APPLICATION_SOFTWARE_VERSION: + characterstring_init_ansi( + &char_string, Application_Software_Version); + apdu_len = + encode_application_character_string(&apdu[0], &char_string); + break; + case PROP_LOCATION: + characterstring_init_ansi(&char_string, Location); + apdu_len = + encode_application_character_string(&apdu[0], &char_string); + break; + case PROP_LOCAL_TIME: + Update_Current_Time(); + apdu_len = encode_application_time(&apdu[0], &Local_Time); + break; + case PROP_UTC_OFFSET: + Update_Current_Time(); + apdu_len = encode_application_signed(&apdu[0], UTC_Offset); + break; + case PROP_LOCAL_DATE: + Update_Current_Time(); + apdu_len = encode_application_date(&apdu[0], &Local_Date); + break; + case PROP_DAYLIGHT_SAVINGS_STATUS: + Update_Current_Time(); + apdu_len = + encode_application_boolean(&apdu[0], Daylight_Savings_Status); + break; + case PROP_PROTOCOL_VERSION: + apdu_len = encode_application_unsigned( + &apdu[0], Device_Protocol_Version()); + break; + case PROP_PROTOCOL_REVISION: + apdu_len = encode_application_unsigned( + &apdu[0], Device_Protocol_Revision()); + break; + case PROP_PROTOCOL_SERVICES_SUPPORTED: + /* Note: list of services that are executed, not initiated. */ + bitstring_init(&bit_string); + for (i = 0; i < MAX_BACNET_SERVICES_SUPPORTED; i++) { + /* automatic lookup based on handlers set */ + bitstring_set_bit(&bit_string, (uint8_t)i, + apdu_service_supported((BACNET_SERVICES_SUPPORTED)i)); + } + apdu_len = encode_application_bitstring(&apdu[0], &bit_string); + break; + case PROP_PROTOCOL_OBJECT_TYPES_SUPPORTED: + /* Note: this is the list of objects that can be in this device, + not a list of objects that this device can access */ + bitstring_init(&bit_string); + for (i = 0; i < MAX_ASHRAE_OBJECT_TYPE; i++) { + /* initialize all the object types to not-supported */ + bitstring_set_bit(&bit_string, (uint8_t)i, false); + } + /* set the object types with objects to supported */ + + pObject = Object_Table; + while (pObject->Object_Type < MAX_BACNET_OBJECT_TYPE) { + if ((pObject->Object_Count) && (pObject->Object_Count() > 0)) { + bitstring_set_bit( + &bit_string, (uint8_t)pObject->Object_Type, true); + } + pObject++; + } + apdu_len = encode_application_bitstring(&apdu[0], &bit_string); + break; + case PROP_OBJECT_LIST: + count = Device_Object_List_Count(); + apdu_len = bacnet_array_encode(rpdata->object_instance, + rpdata->array_index, + Device_Object_List_Element_Encode, + count, apdu, apdu_max); + if (apdu_len == BACNET_STATUS_ABORT) { + rpdata->error_code = + ERROR_CODE_ABORT_SEGMENTATION_NOT_SUPPORTED; + } else if (apdu_len == BACNET_STATUS_ERROR) { + rpdata->error_class = ERROR_CLASS_PROPERTY; + rpdata->error_code = ERROR_CODE_INVALID_ARRAY_INDEX; + } + break; + case PROP_MAX_APDU_LENGTH_ACCEPTED: + apdu_len = encode_application_unsigned(&apdu[0], MAX_APDU); + break; + case PROP_SEGMENTATION_SUPPORTED: + apdu_len = encode_application_enumerated( + &apdu[0], Device_Segmentation_Supported()); + break; + case PROP_APDU_TIMEOUT: + apdu_len = encode_application_unsigned(&apdu[0], apdu_timeout()); + break; + case PROP_NUMBER_OF_APDU_RETRIES: + apdu_len = encode_application_unsigned(&apdu[0], apdu_retries()); + break; + case PROP_DEVICE_ADDRESS_BINDING: + apdu_len = address_list_encode(&apdu[0], apdu_max); + break; + case PROP_DATABASE_REVISION: + apdu_len = encode_application_unsigned(&apdu[0], Database_Revision); + break; +#if defined(BACDL_MSTP) + case PROP_MAX_INFO_FRAMES: + apdu_len = + encode_application_unsigned(&apdu[0], dlmstp_max_info_frames()); + break; + case PROP_MAX_MASTER: + apdu_len = + encode_application_unsigned(&apdu[0], dlmstp_max_master()); + break; +#endif +#if defined(BACNET_TIME_MASTER) + case PROP_TIME_SYNCHRONIZATION_RECIPIENTS: + apdu_len = handler_timesync_encode_recipients(&apdu[0], MAX_APDU); + if (apdu_len < 0) { + rpdata->error_code = + ERROR_CODE_ABORT_SEGMENTATION_NOT_SUPPORTED; + apdu_len = BACNET_STATUS_ABORT; + } + break; + case PROP_TIME_SYNCHRONIZATION_INTERVAL: + apdu_len = encode_application_unsigned( + &apdu[0], Device_Time_Sync_Interval()); + break; + case PROP_ALIGN_INTERVALS: + apdu_len = + encode_application_boolean(&apdu[0], Device_Align_Intervals()); + break; + case PROP_INTERVAL_OFFSET: + apdu_len = + encode_application_unsigned(&apdu[0], Device_Interval_Offset()); + break; +#endif + case PROP_ACTIVE_COV_SUBSCRIPTIONS: + apdu_len = handler_cov_encode_subscriptions(&apdu[0], apdu_max); + break; + default: + rpdata->error_class = ERROR_CLASS_PROPERTY; + rpdata->error_code = ERROR_CODE_UNKNOWN_PROPERTY; + apdu_len = BACNET_STATUS_ERROR; + break; + } + /* only array properties can have array options */ + if ((apdu_len >= 0) && (rpdata->object_property != PROP_OBJECT_LIST) && + (rpdata->array_index != BACNET_ARRAY_ALL)) { + rpdata->error_class = ERROR_CLASS_PROPERTY; + rpdata->error_code = ERROR_CODE_PROPERTY_IS_NOT_AN_ARRAY; + apdu_len = BACNET_STATUS_ERROR; + } + + return apdu_len; +} + +/** Looks up the common Object and Property, and encodes its Value in an + * APDU. Sets the error class and code if request is not appropriate. + * @param pObject - object table + * @param rpdata [in,out] Structure with the requested Object & Property info + * on entry, and APDU message on return. + * @return The length of the APDU on success, else BACNET_STATUS_ERROR + */ +static int Read_Property_Common( + struct object_functions *pObject, BACNET_READ_PROPERTY_DATA *rpdata) +{ + int apdu_len = BACNET_STATUS_ERROR; + BACNET_CHARACTER_STRING char_string; + uint8_t *apdu = NULL; +#if (BACNET_PROTOCOL_REVISION >= 14) + struct special_property_list_t property_list; +#endif + + if ((rpdata->application_data == NULL) || + (rpdata->application_data_len == 0)) { + return 0; + } + apdu = rpdata->application_data; + if (property_list_common(rpdata->object_property)) { + apdu_len = property_list_common_encode(rpdata, Object_Instance_Number); + } else if (rpdata->object_property == PROP_OBJECT_NAME) { + /* only array properties can have array options */ + if (rpdata->array_index != BACNET_ARRAY_ALL) { + rpdata->error_class = ERROR_CLASS_PROPERTY; + rpdata->error_code = ERROR_CODE_PROPERTY_IS_NOT_AN_ARRAY; + apdu_len = BACNET_STATUS_ERROR; + } else { + characterstring_init_ansi(&char_string, ""); + if (pObject->Object_Name) { + (void)pObject->Object_Name( + rpdata->object_instance, &char_string); + } + apdu_len = + encode_application_character_string(&apdu[0], &char_string); + } +#if (BACNET_PROTOCOL_REVISION >= 14) + } else if (rpdata->object_property == PROP_PROPERTY_LIST) { + Device_Objects_Property_List( + rpdata->object_type, rpdata->object_instance, &property_list); + apdu_len = property_list_encode(rpdata, property_list.Required.pList, + property_list.Optional.pList, property_list.Proprietary.pList); +#endif + } else if (pObject->Object_Read_Property) { + apdu_len = pObject->Object_Read_Property(rpdata); + } + + return apdu_len; +} + +/** Looks up the requested Object and Property, and encodes its Value in an + * APDU. + * @ingroup ObjIntf + * If the Object or Property can't be found, sets the error class and code. + * + * @param rpdata [in,out] Structure with the desired Object and Property info + * on entry, and APDU message on return. + * @return The length of the APDU on success, else BACNET_STATUS_ERROR + */ +int Device_Read_Property(BACNET_READ_PROPERTY_DATA *rpdata) +{ + int apdu_len = BACNET_STATUS_ERROR; + struct object_functions *pObject = NULL; + + /* initialize the default return values */ + rpdata->error_class = ERROR_CLASS_OBJECT; + rpdata->error_code = ERROR_CODE_UNKNOWN_OBJECT; + pObject = Device_Objects_Find_Functions(rpdata->object_type); + if (pObject != NULL) { + if (pObject->Object_Valid_Instance && + pObject->Object_Valid_Instance(rpdata->object_instance)) { + apdu_len = Read_Property_Common(pObject, rpdata); + } else { + rpdata->error_class = ERROR_CLASS_OBJECT; + rpdata->error_code = ERROR_CODE_UNKNOWN_OBJECT; + } + } else { + rpdata->error_class = ERROR_CLASS_OBJECT; + rpdata->error_code = ERROR_CODE_UNKNOWN_OBJECT; + } + + return apdu_len; +} + +/* returns true if successful */ +bool Device_Write_Property_Local(BACNET_WRITE_PROPERTY_DATA *wp_data) +{ + bool status = false; /* return value */ + int len = 0; + BACNET_APPLICATION_DATA_VALUE value; + BACNET_OBJECT_TYPE object_type = OBJECT_NONE; + uint32_t object_instance = 0; + int result = 0; +#if defined(BACNET_TIME_MASTER) + uint32_t minutes = 0; +#endif + + /* decode the some of the request */ + len = bacapp_decode_application_data( + wp_data->application_data, wp_data->application_data_len, &value); + if (len < 0) { + /* error while decoding - a value larger than we can handle */ + wp_data->error_class = ERROR_CLASS_PROPERTY; + wp_data->error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; + return false; + } + if ((wp_data->object_property != PROP_OBJECT_LIST) && + (wp_data->array_index != BACNET_ARRAY_ALL)) { + /* only array properties can have array options */ + wp_data->error_class = ERROR_CLASS_PROPERTY; + wp_data->error_code = ERROR_CODE_PROPERTY_IS_NOT_AN_ARRAY; + return false; + } + /* FIXME: len < application_data_len: more data? */ + switch (wp_data->object_property) { + case PROP_OBJECT_IDENTIFIER: + status = write_property_type_valid( + wp_data, &value, BACNET_APPLICATION_TAG_OBJECT_ID); + if (status) { + if ((value.type.Object_Id.type == OBJECT_DEVICE) && + (Device_Set_Object_Instance_Number( + value.type.Object_Id.instance))) { + /* FIXME: we could send an I-Am broadcast to let the world + * know */ + } else { + status = false; + wp_data->error_class = ERROR_CLASS_PROPERTY; + wp_data->error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; + } + } + break; + case PROP_NUMBER_OF_APDU_RETRIES: + status = write_property_type_valid( + wp_data, &value, BACNET_APPLICATION_TAG_UNSIGNED_INT); + if (status) { + /* FIXME: bounds check? */ + apdu_retries_set((uint8_t)value.type.Unsigned_Int); + } + break; + case PROP_APDU_TIMEOUT: + status = write_property_type_valid( + wp_data, &value, BACNET_APPLICATION_TAG_UNSIGNED_INT); + if (status) { + /* FIXME: bounds check? */ + apdu_timeout_set((uint16_t)value.type.Unsigned_Int); + } + break; + case PROP_VENDOR_IDENTIFIER: + status = write_property_type_valid( + wp_data, &value, BACNET_APPLICATION_TAG_UNSIGNED_INT); + if (status) { + /* FIXME: bounds check? */ + Device_Set_Vendor_Identifier((uint16_t)value.type.Unsigned_Int); + } + break; + case PROP_SYSTEM_STATUS: + status = write_property_type_valid( + wp_data, &value, BACNET_APPLICATION_TAG_ENUMERATED); + if (status) { + result = Device_Set_System_Status( + (BACNET_DEVICE_STATUS)value.type.Enumerated, false); + if (result != 0) { + /* result: - 0 = ok, -1 = bad value, -2 = not allowed */ + status = false; + wp_data->error_class = ERROR_CLASS_PROPERTY; + if (result == -1) { + wp_data->error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; + } else { + wp_data->error_code = + ERROR_CODE_OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED; + } + } + } + break; + case PROP_OBJECT_NAME: + status = write_property_string_valid( + wp_data, &value, characterstring_capacity(&My_Object_Name)); + if (status) { + /* All the object names in a device must be unique */ + if (Device_Valid_Object_Name(&value.type.Character_String, + &object_type, &object_instance)) { + if ((object_type == wp_data->object_type) && + (object_instance == wp_data->object_instance)) { + /* writing same name to same object */ + status = true; + } else { + status = false; + wp_data->error_class = ERROR_CLASS_PROPERTY; + wp_data->error_code = ERROR_CODE_DUPLICATE_NAME; + } + } else { + Device_Set_Object_Name(&value.type.Character_String); + } + } + break; + case PROP_LOCATION: + status = write_property_empty_string_valid( + wp_data, &value, MAX_DEV_LOC_LEN); + if (status) { + Device_Set_Location( + characterstring_value(&value.type.Character_String), + characterstring_length(&value.type.Character_String)); + } + break; + + case PROP_DESCRIPTION: + status = write_property_empty_string_valid( + wp_data, &value, MAX_DEV_DESC_LEN); + if (status) { + Device_Set_Description( + characterstring_value(&value.type.Character_String), + characterstring_length(&value.type.Character_String)); + } + break; + case PROP_MODEL_NAME: + status = write_property_empty_string_valid( + wp_data, &value, MAX_DEV_MOD_LEN); + if (status) { + Device_Set_Model_Name( + characterstring_value(&value.type.Character_String), + characterstring_length(&value.type.Character_String)); + } + break; +#if defined(BACNET_TIME_MASTER) + case PROP_TIME_SYNCHRONIZATION_INTERVAL: + status = write_property_type_valid( + wp_data, &value, BACNET_APPLICATION_TAG_UNSIGNED_INT); + if (status) { + if (value.type.Unsigned_Int < 65535) { + minutes = value.type.Unsigned_Int; + Device_Time_Sync_Interval_Set(minutes); + status = true; + } else { + wp_data->error_class = ERROR_CLASS_PROPERTY; + wp_data->error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; + } + } + break; + case PROP_ALIGN_INTERVALS: + status = write_property_type_valid( + wp_data, &value, BACNET_APPLICATION_TAG_BOOLEAN); + if (status) { + Device_Align_Intervals_Set(value.type.Boolean); + status = true; + } + break; + case PROP_INTERVAL_OFFSET: + status = write_property_type_valid( + wp_data, &value, BACNET_APPLICATION_TAG_UNSIGNED_INT); + if (status) { + if (value.type.Unsigned_Int < 65535) { + minutes = value.type.Unsigned_Int; + Device_Interval_Offset_Set(minutes); + status = true; + } else { + wp_data->error_class = ERROR_CLASS_PROPERTY; + wp_data->error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; + } + } + break; +#else + case PROP_TIME_SYNCHRONIZATION_INTERVAL: + case PROP_ALIGN_INTERVALS: + case PROP_INTERVAL_OFFSET: + wp_data->error_class = ERROR_CLASS_PROPERTY; + wp_data->error_code = ERROR_CODE_UNKNOWN_PROPERTY; + break; +#endif + case PROP_UTC_OFFSET: + status = write_property_type_valid( + wp_data, &value, BACNET_APPLICATION_TAG_SIGNED_INT); + if (status) { + if ((value.type.Signed_Int < (12 * 60)) && + (value.type.Signed_Int > (-12 * 60))) { + Device_UTC_Offset_Set(value.type.Signed_Int); + status = true; + } else { + wp_data->error_class = ERROR_CLASS_PROPERTY; + wp_data->error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; + } + } + break; +#if defined(BACDL_MSTP) + case PROP_MAX_INFO_FRAMES: + status = write_property_type_valid( + wp_data, &value, BACNET_APPLICATION_TAG_UNSIGNED_INT); + if (status) { + if (value.type.Unsigned_Int <= 255) { + dlmstp_set_max_info_frames( + (uint8_t)value.type.Unsigned_Int); + } else { + status = false; + wp_data->error_class = ERROR_CLASS_PROPERTY; + wp_data->error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; + } + } + break; + case PROP_MAX_MASTER: + status = write_property_type_valid( + wp_data, &value, BACNET_APPLICATION_TAG_UNSIGNED_INT); + if (status) { + if ((value.type.Unsigned_Int > 0) && + (value.type.Unsigned_Int <= 127)) { + dlmstp_set_max_master((uint8_t)value.type.Unsigned_Int); + } else { + status = false; + wp_data->error_class = ERROR_CLASS_PROPERTY; + wp_data->error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; + } + } + break; +#else + case PROP_MAX_INFO_FRAMES: + case PROP_MAX_MASTER: + wp_data->error_class = ERROR_CLASS_PROPERTY; + wp_data->error_code = ERROR_CODE_UNKNOWN_PROPERTY; + break; +#endif + case PROP_OBJECT_TYPE: + case PROP_VENDOR_NAME: + case PROP_FIRMWARE_REVISION: + case PROP_APPLICATION_SOFTWARE_VERSION: + case PROP_LOCAL_TIME: + case PROP_LOCAL_DATE: + case PROP_DAYLIGHT_SAVINGS_STATUS: + case PROP_PROTOCOL_VERSION: + case PROP_PROTOCOL_REVISION: + case PROP_PROTOCOL_SERVICES_SUPPORTED: + case PROP_PROTOCOL_OBJECT_TYPES_SUPPORTED: + case PROP_OBJECT_LIST: + case PROP_MAX_APDU_LENGTH_ACCEPTED: + case PROP_SEGMENTATION_SUPPORTED: + case PROP_DEVICE_ADDRESS_BINDING: + case PROP_DATABASE_REVISION: + case PROP_ACTIVE_COV_SUBSCRIPTIONS: +#if defined(BACNET_TIME_MASTER) + case PROP_TIME_SYNCHRONIZATION_RECIPIENTS: +#endif + wp_data->error_class = ERROR_CLASS_PROPERTY; + wp_data->error_code = ERROR_CODE_WRITE_ACCESS_DENIED; + break; + default: + wp_data->error_class = ERROR_CLASS_PROPERTY; + wp_data->error_code = ERROR_CODE_UNKNOWN_PROPERTY; + break; + } + + return status; +} + +/** Looks up the requested Object and Property, and set the new Value in it, + * if allowed. + * If the Object or Property can't be found, sets the error class and code. + * @ingroup ObjIntf + * + * @param wp_data [in,out] Structure with the desired Object and Property info + * and new Value on entry, and APDU message on return. + * @return True on success, else False if there is an error. + */ +bool Device_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) +{ + bool status = false; /* Ever the pessimist! */ + struct object_functions *pObject = NULL; + + /* initialize the default return values */ + wp_data->error_class = ERROR_CLASS_OBJECT; + wp_data->error_code = ERROR_CODE_UNKNOWN_OBJECT; + pObject = Device_Objects_Find_Functions(wp_data->object_type); + if (pObject != NULL) { + if (pObject->Object_Valid_Instance && + pObject->Object_Valid_Instance(wp_data->object_instance)) { + if (pObject->Object_Write_Property) { +#if (BACNET_PROTOCOL_REVISION >= 14) + if (wp_data->object_property == PROP_PROPERTY_LIST) { + wp_data->error_class = ERROR_CLASS_PROPERTY; + wp_data->error_code = ERROR_CODE_WRITE_ACCESS_DENIED; + } else +#endif + { + status = pObject->Object_Write_Property(wp_data); + } + } else { + wp_data->error_class = ERROR_CLASS_PROPERTY; + wp_data->error_code = ERROR_CODE_WRITE_ACCESS_DENIED; + } + } else { + wp_data->error_class = ERROR_CLASS_OBJECT; + wp_data->error_code = ERROR_CODE_UNKNOWN_OBJECT; + } + } else { + wp_data->error_class = ERROR_CLASS_OBJECT; + wp_data->error_code = ERROR_CODE_UNKNOWN_OBJECT; + } + + return (status); +} + +/** + * @brief AddListElement from an object list property + * @param list_element [in] Pointer to the BACnet_List_Element_Data structure, + * which is packed with the information from the request. + * @return The length of the apdu encoded or #BACNET_STATUS_ERROR or + * #BACNET_STATUS_ABORT or #BACNET_STATUS_REJECT. + */ +int Device_Add_List_Element( + BACNET_LIST_ELEMENT_DATA * list_element) +{ + int status = BACNET_STATUS_ERROR; + struct object_functions *pObject = NULL; + + pObject = Device_Objects_Find_Functions(list_element->object_type); + if (pObject != NULL) { + if (pObject->Object_Valid_Instance && + pObject->Object_Valid_Instance(list_element->object_instance)) { + if (pObject->Object_Add_List_Element) { + status = pObject->Object_Add_List_Element(list_element); + } else { + list_element->error_class = ERROR_CLASS_PROPERTY; + list_element->error_code = ERROR_CODE_WRITE_ACCESS_DENIED; + } + } else { + list_element->error_class = ERROR_CLASS_OBJECT; + list_element->error_code = ERROR_CODE_UNKNOWN_OBJECT; + } + } else { + list_element->error_class = ERROR_CLASS_OBJECT; + list_element->error_code = ERROR_CODE_UNKNOWN_OBJECT; + } + + return status; +} + +/** + * @brief RemoveListElement from an object list property + * @param list_element [in] Pointer to the BACnet_List_Element_Data structure, + * which is packed with the information from the request. + * @return The length of the apdu encoded or #BACNET_STATUS_ERROR or + * #BACNET_STATUS_ABORT or #BACNET_STATUS_REJECT. + */ +int Device_Remove_List_Element( + BACNET_LIST_ELEMENT_DATA * list_element) +{ + int status = BACNET_STATUS_ERROR; + struct object_functions *pObject = NULL; + + pObject = Device_Objects_Find_Functions(list_element->object_type); + if (pObject != NULL) { + if (pObject->Object_Valid_Instance && + pObject->Object_Valid_Instance(list_element->object_instance)) { + if (pObject->Object_Remove_List_Element) { + status = pObject->Object_Remove_List_Element(list_element); + } else { + list_element->error_class = ERROR_CLASS_PROPERTY; + list_element->error_code = ERROR_CODE_WRITE_ACCESS_DENIED; + } + } else { + list_element->error_class = ERROR_CLASS_OBJECT; + list_element->error_code = ERROR_CODE_UNKNOWN_OBJECT; + } + } else { + list_element->error_class = ERROR_CLASS_OBJECT; + list_element->error_code = ERROR_CODE_UNKNOWN_OBJECT; + } + + return status; +} + +/** Looks up the requested Object, and fills the Property Value list. + * If the Object or Property can't be found, returns false. + * @ingroup ObjHelpers + * @param [in] The object type to be looked up. + * @param [in] The object instance number to be looked up. + * @param [out] The value list + * @return True if the object instance supports this feature and value changed. + */ +bool Device_Encode_Value_List(BACNET_OBJECT_TYPE object_type, + uint32_t object_instance, + BACNET_PROPERTY_VALUE *value_list) +{ + bool status = false; /* Ever the pessimist! */ + struct object_functions *pObject = NULL; + + pObject = Device_Objects_Find_Functions(object_type); + if (pObject != NULL) { + if (pObject->Object_Valid_Instance && + pObject->Object_Valid_Instance(object_instance)) { + if (pObject->Object_Value_List) { + status = + pObject->Object_Value_List(object_instance, value_list); + } + } + } + + return (status); +} + +/** Checks the COV flag in the requested Object + * @ingroup ObjHelpers + * @param [in] The object type to be looked up. + * @param [in] The object instance to be looked up. + * @return True if the COV flag is set + */ +bool Device_COV(BACNET_OBJECT_TYPE object_type, uint32_t object_instance) +{ + bool status = false; /* Ever the pessamist! */ + struct object_functions *pObject = NULL; + + pObject = Device_Objects_Find_Functions(object_type); + if (pObject != NULL) { + if (pObject->Object_Valid_Instance && + pObject->Object_Valid_Instance(object_instance)) { + if (pObject->Object_COV) { + status = pObject->Object_COV(object_instance); + } + } + } + + return (status); +} + +/** Clears the COV flag in the requested Object + * @ingroup ObjHelpers + * @param [in] The object type to be looked up. + * @param [in] The object instance to be looked up. + */ +void Device_COV_Clear(BACNET_OBJECT_TYPE object_type, uint32_t object_instance) +{ + struct object_functions *pObject = NULL; + + pObject = Device_Objects_Find_Functions(object_type); + if (pObject != NULL) { + if (pObject->Object_Valid_Instance && + pObject->Object_Valid_Instance(object_instance)) { + if (pObject->Object_COV_Clear) { + pObject->Object_COV_Clear(object_instance); + } + } + } +} + +/** + * @brief Creates a child object, if supported + * @ingroup ObjHelpers + * @param data - CreateObject data, including error codes if failures + * @return true if object has been created + */ +bool Device_Create_Object( + BACNET_CREATE_OBJECT_DATA *data) +{ + bool status = false; + struct object_functions *pObject = NULL; + uint32_t object_instance; + + pObject = Device_Objects_Find_Functions(data->object_type); + if (pObject != NULL) { + if (!pObject->Object_Create) { + /* The device supports the object type and may have + sufficient space, but does not support the creation of the + object for some other reason.*/ + data->error_class = ERROR_CLASS_OBJECT; + data->error_code = ERROR_CODE_DYNAMIC_CREATION_NOT_SUPPORTED; + } else if (pObject->Object_Valid_Instance && + pObject->Object_Valid_Instance(data->object_instance)) { + /* The object being created already exists */ + data->error_class = ERROR_CLASS_OBJECT; + data->error_code = ERROR_CODE_OBJECT_IDENTIFIER_ALREADY_EXISTS; + } else { + if (data->list_of_initial_values) { + /* FIXME: add support for writing to list of initial values */ + /* A property specified by the Property_Identifier in the + List of Initial Values does not support initialization + during the CreateObject service. */ + data->first_failed_element_number = 1; + data->error_class = ERROR_CLASS_PROPERTY; + data->error_code = ERROR_CODE_WRITE_ACCESS_DENIED; + /* and the object shall not be created */ + } else { + object_instance = pObject->Object_Create(data->object_instance); + if (object_instance == BACNET_MAX_INSTANCE) { + /* The device cannot allocate the space needed + for the new object.*/ + data->error_class = ERROR_CLASS_RESOURCES; + data->error_code = ERROR_CODE_NO_SPACE_FOR_OBJECT; + } else { + /* required by ACK */ + data->object_instance = object_instance; + Device_Inc_Database_Revision(); + status = true; + } + } + } + } else { + /* The device does not support the specified object type. */ + data->error_class = ERROR_CLASS_OBJECT; + data->error_code = ERROR_CODE_UNSUPPORTED_OBJECT_TYPE; + } + + return status; +} + +/** + * @brief Deletes a child object, if supported + * @ingroup ObjHelpers + * @param data - DeleteObject data, including error codes if failures + * @return true if object has been deleted + */ +bool Device_Delete_Object( + BACNET_DELETE_OBJECT_DATA *data) +{ + bool status = false; + struct object_functions *pObject = NULL; + + pObject = Device_Objects_Find_Functions(data->object_type); + if (pObject != NULL) { + if (!pObject->Object_Delete) { + /* The device supports the object type + but does not support the deletion of the + object for some reason.*/ + data->error_class = ERROR_CLASS_OBJECT; + data->error_code = ERROR_CODE_OBJECT_DELETION_NOT_PERMITTED; + } else if (pObject->Object_Valid_Instance && + pObject->Object_Valid_Instance(data->object_instance)) { + /* The object being deleted must already exist */ + status = pObject->Object_Delete(data->object_instance); + if (status) { + Device_Inc_Database_Revision(); + } else { + /* The object exists but cannot be deleted. */ + data->error_class = ERROR_CLASS_OBJECT; + data->error_code = ERROR_CODE_OBJECT_DELETION_NOT_PERMITTED; + } + } else { + /* The object to be deleted does not exist. */ + data->error_class = ERROR_CLASS_OBJECT; + data->error_code = ERROR_CODE_UNKNOWN_OBJECT; + } + } else { + /* The device does not support the specified object type. */ + data->error_class = ERROR_CLASS_OBJECT; + data->error_code = ERROR_CODE_UNSUPPORTED_OBJECT_TYPE; + } + + return status; +} + +/** Looks up the requested Object to see if the functionality is supported. + * @ingroup ObjHelpers + * @param [in] The object type to be looked up. + * @return True if the object instance supports this feature. + */ +bool Device_Value_List_Supported(BACNET_OBJECT_TYPE object_type) +{ + bool status = false; /* Ever the pessamist! */ + struct object_functions *pObject = NULL; + + pObject = Device_Objects_Find_Functions(object_type); + if (pObject != NULL) { + if (pObject->Object_Value_List) { + status = true; + } + } + + return (status); +} + +/** Initialize the Device Object. + Initialize the group of object helper functions for any supported Object. + Initialize each of the Device Object child Object instances. + * @ingroup ObjIntf + * @param object_table [in,out] array of structure with object functions. + * Each Child Object must provide some implementation of each of these + * functions in order to properly support the default handlers. + */ +void Device_Init(object_functions_t *object_table) +{ + struct object_functions *pObject = NULL; + characterstring_init_ansi(&My_Object_Name, "Blinkt! Server"); + datetime_init(); + if (object_table) { + Object_Table = object_table; + } else { + Object_Table = &My_Object_Table[0]; + } + pObject = Object_Table; + while (pObject->Object_Type < MAX_BACNET_OBJECT_TYPE) { + if (pObject->Object_Init) { + pObject->Object_Init(); + } + pObject++; + } +} + +bool DeviceGetRRInfo(BACNET_READ_RANGE_DATA *pRequest, /* Info on the request */ + RR_PROP_INFO *pInfo) +{ /* Where to put the response */ + bool status = false; /* return value */ + + switch (pRequest->object_property) { + case PROP_VT_CLASSES_SUPPORTED: + case PROP_ACTIVE_VT_SESSIONS: + case PROP_LIST_OF_SESSION_KEYS: + case PROP_TIME_SYNCHRONIZATION_RECIPIENTS: + case PROP_MANUAL_SLAVE_ADDRESS_BINDING: + case PROP_SLAVE_ADDRESS_BINDING: + case PROP_RESTART_NOTIFICATION_RECIPIENTS: + case PROP_UTC_TIME_SYNCHRONIZATION_RECIPIENTS: + pInfo->RequestTypes = RR_BY_POSITION; + pRequest->error_class = ERROR_CLASS_PROPERTY; + pRequest->error_code = ERROR_CODE_UNKNOWN_PROPERTY; + break; + + case PROP_DEVICE_ADDRESS_BINDING: + pInfo->RequestTypes = RR_BY_POSITION; + pInfo->Handler = rr_address_list_encode; + status = true; + break; + + case PROP_ACTIVE_COV_SUBSCRIPTIONS: + pInfo->RequestTypes = RR_BY_POSITION; + pRequest->error_class = ERROR_CLASS_PROPERTY; + pRequest->error_code = ERROR_CODE_UNKNOWN_PROPERTY; + break; + default: + pRequest->error_class = ERROR_CLASS_SERVICES; + pRequest->error_code = ERROR_CODE_PROPERTY_IS_NOT_A_LIST; + break; + } + + return status; +} + +/** + * @brief Updates all the object timers with elapsed milliseconds + * @param milliseconds - number of milliseconds elapsed + */ +void Device_Timer( + uint16_t milliseconds) +{ + struct object_functions *pObject; + unsigned count = 0; + uint32_t instance; + + pObject = Object_Table; + while (pObject->Object_Type < MAX_BACNET_OBJECT_TYPE) { + if (pObject->Object_Count) { + count = pObject->Object_Count(); + } + while (count) { + count--; + if ((pObject->Object_Timer) && + (pObject->Object_Index_To_Instance)) { + instance = pObject->Object_Index_To_Instance(count); + pObject->Object_Timer(instance, milliseconds); + } + } + pObject++; + } +} diff --git a/apps/blinkt/main.c b/apps/blinkt/main.c new file mode 100644 index 00000000..dcbd3ae4 --- /dev/null +++ b/apps/blinkt/main.c @@ -0,0 +1,465 @@ +/************************************************************************** + * + * Copyright (C) 2006 Steve Karg + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + *********************************************************************/ +#include +#include +#include +#include +#include +#include "bacnet/config.h" +#include "bacnet/basic/binding/address.h" +#include "bacnet/bacdef.h" +#include "bacnet/basic/services.h" +#include "bacnet/basic/services.h" +#include "bacnet/datalink/dlenv.h" +#include "bacnet/bacdcode.h" +#include "bacnet/npdu.h" +#include "bacnet/apdu.h" +#include "bacnet/iam.h" +#include "bacnet/basic/tsm/tsm.h" +#include "bacnet/basic/object/device.h" +#include "bacnet/basic/object/bacfile.h" +#include "bacnet/datalink/datalink.h" +#include "bacnet/dcc.h" +#include "bacnet/getevent.h" +#include "bacnet/lighting.h" +#include "bacport.h" +#include "bacnet/basic/sys/mstimer.h" +#include "bacnet/basic/sys/color_rgb.h" +#include "bacnet/basic/sys/filename.h" +#include "bacnet/basic/sys/linear.h" +#include "bacnet/basic/tsm/tsm.h" +#include "bacnet/version.h" +/* include the device object */ +#include "bacnet/basic/object/device.h" +#include "bacnet/basic/object/lo.h" +#include "bacnet/basic/object/channel.h" +#include "bacnet/basic/object/color_object.h" +#include "bacnet/basic/object/color_temperature.h" +#include "blinkt.h" + +/** @file blinkt/main.c Example application using the BACnet Stack. */ + +/* (Doxygen note: The next two lines pull all the following Javadoc + * into the ServerDemo module.) */ +/** @addtogroup ServerDemo */ +/*@{*/ + +/** Buffer used for receiving */ +static uint8_t Rx_Buf[MAX_MPDU] = { 0 }; +/* current version of the BACnet stack */ +static const char *BACnet_Version = BACNET_VERSION_TEXT; +/* task timer for various BACnet timeouts */ +static struct mstimer BACnet_Task_Timer; +/* task timer for TSM timeouts */ +static struct mstimer BACnet_TSM_Timer; +/* task timer for address binding timeouts */ +static struct mstimer BACnet_Address_Timer; +/* task timer for object functionality */ +static struct mstimer BACnet_Object_Timer; + +/** Initialize the handlers we will utilize. + * @see Device_Init, apdu_set_unconfirmed_handler, apdu_set_confirmed_handler + */ +static void Init_Service_Handlers(void) +{ + Device_Init(NULL); + /* we need to handle who-is to support dynamic device binding */ + apdu_set_unconfirmed_handler(SERVICE_UNCONFIRMED_WHO_IS, handler_who_is); + apdu_set_unconfirmed_handler(SERVICE_UNCONFIRMED_WHO_HAS, handler_who_has); + /* handle i-am to support binding to other devices */ + apdu_set_unconfirmed_handler(SERVICE_UNCONFIRMED_I_AM, handler_i_am_bind); + /* set the handler for all the services we don't implement */ + /* It is required to send the proper reject message... */ + apdu_set_unrecognized_service_handler_handler(handler_unrecognized_service); + /* Set the handlers for any confirmed services that we support. */ + /* We must implement read property - it's required! */ + apdu_set_confirmed_handler( + SERVICE_CONFIRMED_READ_PROPERTY, handler_read_property); + apdu_set_confirmed_handler( + SERVICE_CONFIRMED_READ_PROP_MULTIPLE, handler_read_property_multiple); + apdu_set_confirmed_handler( + SERVICE_CONFIRMED_WRITE_PROPERTY, handler_write_property); + apdu_set_confirmed_handler( + SERVICE_CONFIRMED_WRITE_PROP_MULTIPLE, handler_write_property_multiple); + apdu_set_confirmed_handler( + SERVICE_CONFIRMED_READ_RANGE, handler_read_range); + apdu_set_confirmed_handler( + SERVICE_CONFIRMED_REINITIALIZE_DEVICE, handler_reinitialize_device); + apdu_set_unconfirmed_handler( + SERVICE_UNCONFIRMED_UTC_TIME_SYNCHRONIZATION, handler_timesync_utc); + apdu_set_unconfirmed_handler( + SERVICE_UNCONFIRMED_TIME_SYNCHRONIZATION, handler_timesync); + apdu_set_confirmed_handler( + SERVICE_CONFIRMED_SUBSCRIBE_COV, handler_cov_subscribe); + apdu_set_unconfirmed_handler( + SERVICE_UNCONFIRMED_COV_NOTIFICATION, handler_ucov_notification); + /* handle communication so we can shutup when asked */ + apdu_set_confirmed_handler(SERVICE_CONFIRMED_DEVICE_COMMUNICATION_CONTROL, + handler_device_communication_control); + /* handle the data coming back from private requests */ + apdu_set_unconfirmed_handler(SERVICE_UNCONFIRMED_PRIVATE_TRANSFER, + handler_unconfirmed_private_transfer); + /* configure the cyclic timers */ + mstimer_set(&BACnet_Task_Timer, 1000UL); + mstimer_set(&BACnet_TSM_Timer, 50UL); + mstimer_set(&BACnet_Address_Timer, 60UL * 1000UL); + mstimer_set(&BACnet_Object_Timer, 100UL); +} + +/** + * Clean up the Blinkt! interface + */ +static void blinkt_cleanup(void) +{ + blinkt_stop(); +} + +/** + * @brief Callback for tracking value + * @param object_instance - object-instance number of the object + * @param old_value - Color temperature value prior to write + * @param value - Color temperature value of the write + */ +static void Lighting_Output_Write_Value_Handler( + uint32_t object_instance, float old_value, float value) +{ + uint8_t index = 255; + uint8_t brightness; + + (void)old_value; + if (object_instance > 0) { + index = object_instance - 1; + } + if (index < blinkt_led_count()) { + /* brightness intensity from 0..31, 0=OFF, 1=dimmest, 31=brightest */ + if (isgreaterequal(value, 1.0)) { + brightness = linear_interpolate(1.0, value, 100.0, 1, 31); + } else { + brightness = 0; + } + blinkt_set_pixel_brightness(index, brightness); + printf("LED[%u]=%.1f%% (%u)\n", (unsigned)index, value, + (unsigned)brightness); + } +} + +/** + * @brief Callback for tracking value + * @param object_instance - object-instance number of the object + * @param old_value - Color temperature value prior to write + * @param value - Color temperature value of the write + */ +static void Color_Temperature_Write_Value_Handler( + uint32_t object_instance, uint32_t old_value, uint32_t value) +{ + uint8_t red, green, blue; + uint8_t index = 255; + + (void)old_value; + if (object_instance > 0) { + index = object_instance - 1; + } + if (index < blinkt_led_count()) { + color_rgb_from_temperature(value, &red, &green, &blue); + blinkt_set_pixel(index, red, green, blue); + printf("%u Kelvin RGB[%u]=%u,%u,%u\n", (unsigned)value, (unsigned)index, + (unsigned)red, (unsigned)green, (unsigned)blue); + } +} + +/** + * @brief Callback for tracking value + * @param object_instance - object-instance number of the object + * @param old_value - BACnetXYColor value prior to write + * @param value - BACnetXYColor value of the write + */ +static void Color_Write_Value_Handler(uint32_t object_instance, + BACNET_XY_COLOR *old_value, + BACNET_XY_COLOR *value) +{ + uint8_t red, green, blue; + float brightness_percent = 100.0; + uint8_t index = 255; + + (void)old_value; + if (object_instance > 0) { + index = object_instance - 1; + } + if (index < blinkt_led_count()) { + color_rgb_from_xy(&red, &green, &blue, value->x_coordinate, + value->y_coordinate, brightness_percent); + blinkt_set_pixel(index, red, green, blue); + printf("x,y=%0.2f,%0.2f(%.1f%%) RGB[%u]=%u,%u,%u\n", + value->x_coordinate, value->y_coordinate, brightness_percent, + (unsigned)index, (unsigned)red, (unsigned)green, (unsigned)blue); + } +} + +/** + * @brief Create the objects and configure the callbacks for BACnet objects + */ +static void bacnet_output_init(void) +{ + unsigned i = 0; + uint8_t led_max; + uint32_t object_instance = 1; + BACNET_COLOR_COMMAND command = { 0 }; + BACNET_OBJECT_ID object_id; + uint32_t light_channel_instance = 1; + uint32_t color_channel_instance = 2; + uint32_t temp_channel_instance = 3; + BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE member; + + Channel_Create(light_channel_instance); + Channel_Name_Set(light_channel_instance, "Lights"); + Channel_Create(color_channel_instance); + Channel_Name_Set(color_channel_instance, "Colors"); + Channel_Create(temp_channel_instance); + Channel_Name_Set(temp_channel_instance, "Color-Temperatures"); + led_max = blinkt_led_count(); + for (i = 0; i < led_max; i++) { + /* color */ + Color_Create(object_instance); + Color_Write_Enable(object_instance); + /* fade to black */ + Color_Command(object_instance, &command); + command.operation = BACNET_COLOR_OPERATION_FADE_TO_COLOR; + command.target.color.x_coordinate = 0.0; + command.target.color.y_coordinate = 0.0; + command.transit.fade_time = 0; + Color_Command_Set(object_instance, &command); + + /* configure channel members */ + member.objectIdentifier.type = OBJECT_COLOR; + member.objectIdentifier.instance = object_instance; + member.propertyIdentifier = PROP_PRESENT_VALUE; + member.arrayIndex = BACNET_ARRAY_ALL; + member.deviceIdentifier.type = OBJECT_DEVICE; + member.deviceIdentifier.instance = Device_Object_Instance_Number(); + Channel_Reference_List_Member_Element_Set( + color_channel_instance, 1 + i, &member); + + /* color temperature */ + Color_Temperature_Create(object_instance); + Color_Temperature_Write_Enable(object_instance); + /* stop the color temperature */ + Color_Temperature_Command(object_instance, &command); + command.operation = BACNET_COLOR_OPERATION_STOP; + Color_Temperature_Command_Set(object_instance, &command); + + /* configure channel members */ + member.objectIdentifier.type = OBJECT_COLOR_TEMPERATURE; + member.objectIdentifier.instance = object_instance; + member.propertyIdentifier = PROP_PRESENT_VALUE; + member.arrayIndex = BACNET_ARRAY_ALL; + member.deviceIdentifier.type = OBJECT_DEVICE; + member.deviceIdentifier.instance = Device_Object_Instance_Number(); + Channel_Reference_List_Member_Element_Set( + temp_channel_instance, 1 + i, &member); + + /* lighting output */ + Lighting_Output_Create(object_instance); + + /* configure references */ + object_id.type = OBJECT_COLOR; + object_id.instance = object_instance; + Lighting_Output_Color_Reference_Set(object_instance, &object_id); + + /* configure channel members */ + member.objectIdentifier.type = OBJECT_LIGHTING_OUTPUT; + member.objectIdentifier.instance = object_instance; + member.propertyIdentifier = PROP_PRESENT_VALUE; + member.arrayIndex = BACNET_ARRAY_ALL; + member.deviceIdentifier.type = OBJECT_DEVICE; + member.deviceIdentifier.instance = Device_Object_Instance_Number(); + Channel_Reference_List_Member_Element_Set( + light_channel_instance, 1 + i, &member); + + object_instance++; + } + Color_Write_Present_Value_Callback_Set(Color_Write_Value_Handler); + Color_Temperature_Write_Present_Value_Callback_Set( + Color_Temperature_Write_Value_Handler); + Lighting_Output_Write_Present_Value_Callback_Set( + Lighting_Output_Write_Value_Handler); + Channel_Write_Property_Internal_Callback_Set(Device_Write_Property); +} + +/** + * @brief Print the terse usage info + * @param filename - this application file name + */ +static void print_usage(const char *filename) +{ + printf("Usage: %s [device-instance]\n", filename); + printf(" [--device N][--test]\n"); + printf(" [--version][--help]\n"); +} + +/** + * @brief Print the verbose usage info + * @param filename - this application file name + */ +static void print_help(const char *filename) +{ + printf("BACnet Blinkt! server device.\n"); + printf("device-instance:\n" + "--device N:\n" + "BACnet Device Object Instance number of this device.\n" + "This number will be used when other devices\n" + "try and bind with this device using Who-Is and\n" + "I-Am services.\n"); + printf("\n"); + printf("--test:\n" + "Test the Blinkt! RGB LEDs with a cycling pattern.\n"); + printf("\n"); + printf("Example:\n" + "%s 9009\n", + filename); +} + +/** Main function of server demo. + * + * @see Device_Set_Object_Instance_Number, dlenv_init, Send_I_Am, + * datalink_receive, npdu_handler, + * dcc_timer_seconds, datalink_maintenance_timer, + * handler_cov_task, + * tsm_timer_milliseconds + * + * @param argc [in] Arg count. + * @param argv [in] Takes one argument: the Device Instance #. + * @return 0 on success. + */ +int main(int argc, char *argv[]) +{ + BACNET_ADDRESS src = { 0 }; /* address where message came from */ + uint16_t pdu_len = 0; + unsigned timeout_ms = 1; + unsigned long seconds = 0; + unsigned long milliseconds; + bool blinkt_test = false; + unsigned int target_args = 0; + uint32_t device_id = BACNET_MAX_INSTANCE; + int argi = 0; + char *filename = NULL; + + filename = filename_remove_path(argv[0]); + for (argi = 1; argi < argc; argi++) { + if (strcmp(argv[argi], "--help") == 0) { + print_usage(filename); + print_help(filename); + return 0; + } + if (strcmp(argv[argi], "--version") == 0) { + printf("%s %s\n", filename, BACNET_VERSION_TEXT); + printf("Copyright (C) 2023 by Steve Karg and others.\n" + "This is free software; see the source for copying " + "conditions.\n" + "There is NO warranty; not even for MERCHANTABILITY or\n" + "FITNESS FOR A PARTICULAR PURPOSE.\n"); + return 0; + } + if (strcmp(argv[argi], "--device") == 0) { + if (++argi < argc) { + device_id = strtol(argv[argi], NULL, 0); + } + } else if (strcmp(argv[argi], "--test") == 0) { + blinkt_test = true; + } else { + if (target_args == 0) { + device_id = strtol(argv[argi], NULL, 0); + target_args++; + } + } + } + if (device_id > BACNET_MAX_INSTANCE) { + fprintf(stderr, "device=%u - it must be less than %u\n", device_id, + BACNET_MAX_INSTANCE); + return 1; + } + Device_Set_Object_Instance_Number(device_id); + printf("BACnet Raspberry Pi Blinkt! Demo\n" + "BACnet Stack Version %s\n" + "BACnet Device ID: %u\n" + "Max APDU: %d\n", + BACnet_Version, Device_Object_Instance_Number(), MAX_APDU); + /* load any static address bindings to show up + in our device bindings list */ + address_init(); + Init_Service_Handlers(); + dlenv_init(); + atexit(datalink_cleanup); + blinkt_init(); + atexit(blinkt_cleanup); + bacnet_output_init(); + /* configure the timeout values */ + /* broadcast an I-Am on startup */ + Send_I_Am(&Handler_Transmit_Buffer[0]); + /* loop forever */ + for (;;) { + /* input */ + pdu_len = datalink_receive(&src, &Rx_Buf[0], MAX_MPDU, timeout_ms); + /* process */ + if (pdu_len) { + npdu_handler(&src, &Rx_Buf[0], pdu_len); + } + if (mstimer_expired(&BACnet_Task_Timer)) { + mstimer_reset(&BACnet_Task_Timer); + /* 1 second tasks */ + dcc_timer_seconds(1); + datalink_maintenance_timer(1); + dlenv_maintenance_timer(1); + handler_cov_timer_seconds(1); + } + if (mstimer_expired(&BACnet_TSM_Timer)) { + mstimer_reset(&BACnet_TSM_Timer); + tsm_timer_milliseconds(mstimer_interval(&BACnet_TSM_Timer)); + } + handler_cov_task(); + if (mstimer_expired(&BACnet_Address_Timer)) { + mstimer_reset(&BACnet_Address_Timer); + /* address cache */ + seconds = mstimer_interval(&BACnet_Address_Timer) / 1000; + address_cache_timer(seconds); + } + /* output/input */ + if (blinkt_test) { + blinkt_test_task(); + } else { + if (mstimer_expired(&BACnet_Object_Timer)) { + mstimer_reset(&BACnet_Object_Timer); + milliseconds = mstimer_interval(&BACnet_Object_Timer); + Device_Timer(milliseconds); + blinkt_show(); + } + } + } + + return 0; +} + +/* @} */ + +/* End group ServerDemo */ diff --git a/apps/piface/Makefile b/apps/piface/Makefile index 2403f937..c7ef0989 100644 --- a/apps/piface/Makefile +++ b/apps/piface/Makefile @@ -7,7 +7,7 @@ TARGET = bacpiface # BACnet objects that are used with this app BACNET_OBJECT_DIR = $(BACNET_SRC_DIR)/bacnet/basic/object SRC = main.c \ - $(BACNET_OBJECT_DIR)/device.c \ + device.c \ $(BACNET_OBJECT_DIR)/netport.c \ $(BACNET_OBJECT_DIR)/bi.c \ $(BACNET_OBJECT_DIR)/bo.c @@ -24,6 +24,8 @@ PIFACE_INCLUDE = libpifacedigital/src PIFACE_LIB = libpifacedigital MCP23S17_LIB = libmcp23s17 +CFLAGS += -I$(PIFACE_INCLUDE) + LDPIFACE = -Wl,-L$(PIFACE_LIB),-lpifacedigital,-L$(MCP23S17_LIB),-lmcp23s17 LFLAGS += $(LDPIFACE) diff --git a/apps/piface/device.c b/apps/piface/device.c index 07b3fd93..4ab7c0a4 100644 --- a/apps/piface/device.c +++ b/apps/piface/device.c @@ -1,6 +1,6 @@ /************************************************************************** * - * Copyright (C) 2014 Steve Karg + * Copyright (C) 2005,2006,2009 Steve Karg * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the @@ -22,15 +22,19 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * *********************************************************************/ + +/** @file device.c Base "class" for handling all BACnet objects belonging + * to a BACnet device, as well as Device-specific properties. */ + #include #include #include /* for memmove */ -#include /* for timezone, localtime */ #include "bacnet/bacdef.h" #include "bacnet/bacdcode.h" #include "bacnet/bacenum.h" #include "bacnet/bacapp.h" #include "bacnet/config.h" /* the custom stuff */ +#include "bacnet/datetime.h" #include "bacnet/apdu.h" #include "bacnet/wp.h" /* WriteProperty handling */ #include "bacnet/rp.h" /* ReadProperty handling */ @@ -41,8 +45,6 @@ #include "bacnet/datalink/datalink.h" #include "bacnet/basic/binding/address.h" /* include the OS specific */ -#include "bacnet/basic/sys/mstimer.h" -/* include the device object */ #include "bacnet/basic/object/device.h" #include "bacnet/basic/object/bi.h" #include "bacnet/basic/object/bo.h" @@ -67,14 +69,18 @@ static object_functions_t My_Object_Table[] = { Device_Read_Property_Local, Device_Write_Property_Local, Device_Property_Lists, DeviceGetRRInfo, NULL /* Iterator */, NULL /* Value_Lists */, NULL /* COV */, NULL /* COV Clear */, - NULL /* Intrinsic Reporting */ }, + NULL /* Intrinsic Reporting */, NULL /* Add_List_Element */, + NULL /* Remove_List_Element */, + NULL /* Create */, NULL /* Delete */, NULL /* Timer */ }, #if (BACNET_PROTOCOL_REVISION >= 17) { OBJECT_NETWORK_PORT, Network_Port_Init, Network_Port_Count, Network_Port_Index_To_Instance, Network_Port_Valid_Instance, Network_Port_Object_Name, Network_Port_Read_Property, Network_Port_Write_Property, Network_Port_Property_Lists, NULL /* ReadRangeInfo */, NULL /* Iterator */, NULL /* Value_Lists */, - NULL /* COV */, NULL /* COV Clear */, NULL /* Intrinsic Reporting */ }, + NULL /* COV */, NULL /* COV Clear */, NULL /* Intrinsic Reporting */, + NULL /* Add_List_Element */, NULL /* Remove_List_Element */, + NULL /* Create */, NULL /* Delete */, NULL /* Timer */ }, #endif { OBJECT_BINARY_INPUT, Binary_Input_Init, Binary_Input_Count, Binary_Input_Index_To_Instance, Binary_Input_Valid_Instance, @@ -82,20 +88,25 @@ static object_functions_t My_Object_Table[] = { Binary_Input_Write_Property, Binary_Input_Property_Lists, NULL /* ReadRangeInfo */, NULL /* Iterator */, Binary_Input_Encode_Value_List, Binary_Input_Change_Of_Value, - Binary_Input_Change_Of_Value_Clear, NULL /* Intrinsic Reporting */ }, + Binary_Input_Change_Of_Value_Clear, NULL /* Intrinsic Reporting */, + NULL /* Add_List_Element */, NULL /* Remove_List_Element */, + NULL /* Create */, NULL /* Delete */, NULL /* Timer */ }, { OBJECT_BINARY_OUTPUT, Binary_Output_Init, Binary_Output_Count, Binary_Output_Index_To_Instance, Binary_Output_Valid_Instance, Binary_Output_Object_Name, Binary_Output_Read_Property, Binary_Output_Write_Property, Binary_Output_Property_Lists, NULL /* ReadRangeInfo */, NULL /* Iterator */, NULL /* Value_Lists */, - NULL /* COV */, NULL /* COV Clear */, NULL /* Intrinsic Reporting */ }, + NULL /* COV */, NULL /* COV Clear */, NULL /* Intrinsic Reporting */, + NULL /* Add_List_Element */, NULL /* Remove_List_Element */, + Binary_Output_Create, Binary_Output_Delete, NULL /* Timer */}, { MAX_BACNET_OBJECT_TYPE, NULL /* Init */, NULL /* Count */, NULL /* Index_To_Instance */, NULL /* Valid_Instance */, NULL /* Object_Name */, NULL /* Read_Property */, NULL /* Write_Property */, NULL /* Property_Lists */, NULL /* ReadRangeInfo */, NULL /* Iterator */, NULL /* Value_Lists */, - NULL /* COV */, NULL /* COV Clear */, - NULL /* Intrinsic Reporting */ } + NULL /* COV */, NULL /* COV Clear */, NULL /* Intrinsic Reporting */, + NULL /* Add_List_Element */, NULL /* Remove_List_Element */, + NULL /* Create */, NULL /* Delete */ , NULL /* Timer */} }; /** Glue function to let the Device object, when called by a handler, @@ -208,6 +219,10 @@ static const int Device_Properties_Optional[] = { #endif PROP_DESCRIPTION, PROP_LOCAL_TIME, PROP_UTC_OFFSET, PROP_LOCAL_DATE, PROP_DAYLIGHT_SAVINGS_STATUS, PROP_LOCATION, PROP_ACTIVE_COV_SUBSCRIPTIONS, +#if defined(BACNET_TIME_MASTER) + PROP_TIME_SYNCHRONIZATION_RECIPIENTS, PROP_TIME_SYNCHRONIZATION_INTERVAL, + PROP_ALIGN_INTERVALS, PROP_INTERVAL_OFFSET, +#endif -1 }; @@ -259,10 +274,15 @@ static BACNET_DATE Local_Date; /* rely on OS, if there is one */ If your UTC offset is -5hours of GMT, then BACnet UTC offset is +5hours. BACnet UTC offset is expressed in minutes. */ -static int32_t UTC_Offset = 5 * 60; +static int16_t UTC_Offset = 5 * 60; static bool Daylight_Savings_Status = false; /* rely on OS */ -/* List_Of_Session_Keys */ +#if defined(BACNET_TIME_MASTER) +static bool Align_Intervals; +static uint32_t Interval_Minutes; +static uint32_t Interval_Offset_Minutes; /* Time_Synchronization_Recipients */ +#endif +/* List_Of_Session_Keys */ /* Max_Master - rely on MS/TP subsystem, if there is one */ /* Max_Info_Frames - rely on MS/TP subsystem, if there is one */ /* Device_Address_Binding - required, but relies on binding cache */ @@ -296,8 +316,18 @@ bool Device_Reinitialize(BACNET_REINITIALIZE_DEVICE_DATA *rd_data) { bool status = false; - /* Note: you could use a mix of state and password to multiple things */ - if (characterstring_ansi_same(&rd_data->password, Reinit_Password)) { + /* From 16.4.1.1.2 Password + This optional parameter shall be a CharacterString of up to + 20 characters. For those devices that require the password as a + protection, the service request shall be denied if the parameter + is absent or if the password is incorrect. For those devices that + do not require a password, this parameter shall be ignored.*/ + if (characterstring_length(&rd_data->password) > 20) { + rd_data->error_class = ERROR_CLASS_SERVICES; + rd_data->error_code = ERROR_CODE_PARAMETER_OUT_OF_RANGE; + } else if (characterstring_ansi_same(&rd_data->password, Reinit_Password)) { + /* Note: you could use a mix of state and password to + accomplish multiple things before restarting */ switch (rd_data->state) { case BACNET_REINIT_COLDSTART: case BACNET_REINIT_WARMSTART: @@ -347,7 +377,7 @@ unsigned Device_Count(void) uint32_t Device_Index_To_Instance(unsigned index) { - index = index; + (void)index; return Object_Instance_Number; } @@ -361,11 +391,7 @@ uint32_t Device_Index_To_Instance(unsigned index) */ uint32_t Device_Object_Instance_Number(void) { -#ifdef BAC_ROUTING - return Routed_Device_Object_Instance_Number(); -#else return Object_Instance_Number; -#endif } bool Device_Set_Object_Instance_Number(uint32_t object_id) @@ -413,6 +439,11 @@ bool Device_Set_Object_Name(BACNET_CHARACTER_STRING *object_name) return status; } +bool Device_Object_Name_ANSI_Init(const char *value) +{ + return characterstring_init_ansi(&My_Object_Name, value); +} + BACNET_DEVICE_STATUS Device_System_Status(void) { return System_Status; @@ -813,57 +844,8 @@ bool Device_Object_Name_Copy(BACNET_OBJECT_TYPE object_type, static void Update_Current_Time(void) { - struct tm *tblock = NULL; -#if defined(_MSC_VER) - time_t tTemp; -#else - struct timeval tv; -#endif -/* -struct tm - -int tm_sec Seconds [0,60]. -int tm_min Minutes [0,59]. -int tm_hour Hour [0,23]. -int tm_mday Day of month [1,31]. -int tm_mon Month of year [0,11]. -int tm_year Years since 1900. -int tm_wday Day of week [0,6] (Sunday =0). -int tm_yday Day of year [0,365]. -int tm_isdst Daylight Savings flag. -*/ -#if defined(_MSC_VER) - time(&tTemp); - tblock = (struct tm *)localtime(&tTemp); -#else - if (gettimeofday(&tv, NULL) == 0) { - tblock = (struct tm *)localtime((const time_t *)&tv.tv_sec); - } -#endif - - if (tblock) { - datetime_set_date(&Local_Date, (uint16_t)tblock->tm_year + 1900, - (uint8_t)tblock->tm_mon + 1, (uint8_t)tblock->tm_mday); -#if !defined(_MSC_VER) - datetime_set_time(&Local_Time, (uint8_t)tblock->tm_hour, - (uint8_t)tblock->tm_min, (uint8_t)tblock->tm_sec, - (uint8_t)(tv.tv_usec / 10000)); -#else - datetime_set_time(&Local_Time, (uint8_t)tblock->tm_hour, - (uint8_t)tblock->tm_min, (uint8_t)tblock->tm_sec, 0); -#endif - if (tblock->tm_isdst) { - Daylight_Savings_Status = true; - } else { - Daylight_Savings_Status = false; - } - /* note: timezone is declared in stdlib. */ - UTC_Offset = timezone / 60; - } else { - datetime_date_wildcard_set(&Local_Date); - datetime_time_wildcard_set(&Local_Time); - Daylight_Savings_Status = false; - } + datetime_local( + &Local_Date, &Local_Time, &UTC_Offset, &Daylight_Savings_Status); } void Device_getCurrentDateTime(BACNET_DATE_TIME *DateTime) @@ -881,11 +863,90 @@ int32_t Device_UTC_Offset(void) return UTC_Offset; } +void Device_UTC_Offset_Set(int16_t offset) +{ + UTC_Offset = offset; +} + bool Device_Daylight_Savings_Status(void) { return Daylight_Savings_Status; } +#if defined(BACNET_TIME_MASTER) +/** + * Sets the time sync interval in minutes + * + * @param flag + * This property, of type BOOLEAN, specifies whether (TRUE) + * or not (FALSE) clock-aligned periodic time synchronization is + * enabled. If periodic time synchronization is enabled and the + * time synchronization interval is a factor of (divides without + * remainder) an hour or day, then the beginning of the period + * specified for time synchronization shall be aligned to the hour or + * day, respectively. If this property is present, it shall be writable. + */ +bool Device_Align_Intervals_Set(bool flag) +{ + Align_Intervals = flag; + + return true; +} + +bool Device_Align_Intervals(void) +{ + return Align_Intervals; +} + +/** + * Sets the time sync interval in minutes + * + * @param minutes + * This property, of type Unsigned, specifies the periodic + * interval in minutes at which TimeSynchronization and + * UTCTimeSynchronization requests shall be sent. If this + * property has a value of zero, then periodic time synchronization is + * disabled. If this property is present, it shall be writable. + */ +bool Device_Time_Sync_Interval_Set(uint32_t minutes) +{ + Interval_Minutes = minutes; + + return true; +} + +uint32_t Device_Time_Sync_Interval(void) +{ + return Interval_Minutes; +} + +/** + * Sets the time sync interval offset value. + * + * @param minutes + * This property, of type Unsigned, specifies the offset in + * minutes from the beginning of the period specified for time + * synchronization until the actual time synchronization requests + * are sent. The offset used shall be the value of Interval_Offset + * modulo the value of Time_Synchronization_Interval; + * e.g., if Interval_Offset has the value 31 and + * Time_Synchronization_Interval is 30, the offset used shall be 1. + * Interval_Offset shall have no effect if Align_Intervals is + * FALSE. If this property is present, it shall be writable. + */ +bool Device_Interval_Offset_Set(uint32_t minutes) +{ + Interval_Offset_Minutes = minutes; + + return true; +} + +uint32_t Device_Interval_Offset(void) +{ + return Interval_Offset_Minutes; +} +#endif + /* return the length of the apdu encoded or BACNET_STATUS_ERROR for error or BACNET_STATUS_ABORT for abort message */ int Device_Read_Property_Local(BACNET_READ_PROPERTY_DATA *rpdata) @@ -1002,7 +1063,8 @@ int Device_Read_Property_Local(BACNET_READ_PROPERTY_DATA *rpdata) pObject = Object_Table; while (pObject->Object_Type < MAX_BACNET_OBJECT_TYPE) { if ((pObject->Object_Count) && (pObject->Object_Count() > 0)) { - bitstring_set_bit(&bit_string, pObject->Object_Type, true); + bitstring_set_bit( + &bit_string, (uint8_t)pObject->Object_Type, true); } pObject++; } @@ -1050,6 +1112,28 @@ int Device_Read_Property_Local(BACNET_READ_PROPERTY_DATA *rpdata) apdu_len = encode_application_unsigned(&apdu[0], dlmstp_max_master()); break; +#endif +#if defined(BACNET_TIME_MASTER) + case PROP_TIME_SYNCHRONIZATION_RECIPIENTS: + apdu_len = handler_timesync_encode_recipients(&apdu[0], MAX_APDU); + if (apdu_len < 0) { + rpdata->error_code = + ERROR_CODE_ABORT_SEGMENTATION_NOT_SUPPORTED; + apdu_len = BACNET_STATUS_ABORT; + } + break; + case PROP_TIME_SYNCHRONIZATION_INTERVAL: + apdu_len = encode_application_unsigned( + &apdu[0], Device_Time_Sync_Interval()); + break; + case PROP_ALIGN_INTERVALS: + apdu_len = + encode_application_boolean(&apdu[0], Device_Align_Intervals()); + break; + case PROP_INTERVAL_OFFSET: + apdu_len = + encode_application_unsigned(&apdu[0], Device_Interval_Offset()); + break; #endif case PROP_ACTIVE_COV_SUBSCRIPTIONS: apdu_len = handler_cov_encode_subscriptions(&apdu[0], apdu_max); @@ -1071,6 +1155,59 @@ int Device_Read_Property_Local(BACNET_READ_PROPERTY_DATA *rpdata) return apdu_len; } +/** Looks up the common Object and Property, and encodes its Value in an + * APDU. Sets the error class and code if request is not appropriate. + * @param pObject - object table + * @param rpdata [in,out] Structure with the requested Object & Property info + * on entry, and APDU message on return. + * @return The length of the APDU on success, else BACNET_STATUS_ERROR + */ +static int Read_Property_Common( + struct object_functions *pObject, BACNET_READ_PROPERTY_DATA *rpdata) +{ + int apdu_len = BACNET_STATUS_ERROR; + BACNET_CHARACTER_STRING char_string; + uint8_t *apdu = NULL; +#if (BACNET_PROTOCOL_REVISION >= 14) + struct special_property_list_t property_list; +#endif + + if ((rpdata->application_data == NULL) || + (rpdata->application_data_len == 0)) { + return 0; + } + apdu = rpdata->application_data; + if (property_list_common(rpdata->object_property)) { + apdu_len = property_list_common_encode(rpdata, Object_Instance_Number); + } else if (rpdata->object_property == PROP_OBJECT_NAME) { + /* only array properties can have array options */ + if (rpdata->array_index != BACNET_ARRAY_ALL) { + rpdata->error_class = ERROR_CLASS_PROPERTY; + rpdata->error_code = ERROR_CODE_PROPERTY_IS_NOT_AN_ARRAY; + apdu_len = BACNET_STATUS_ERROR; + } else { + characterstring_init_ansi(&char_string, ""); + if (pObject->Object_Name) { + (void)pObject->Object_Name( + rpdata->object_instance, &char_string); + } + apdu_len = + encode_application_character_string(&apdu[0], &char_string); + } +#if (BACNET_PROTOCOL_REVISION >= 14) + } else if (rpdata->object_property == PROP_PROPERTY_LIST) { + Device_Objects_Property_List( + rpdata->object_type, rpdata->object_instance, &property_list); + apdu_len = property_list_encode(rpdata, property_list.Required.pList, + property_list.Optional.pList, property_list.Proprietary.pList); +#endif + } else if (pObject->Object_Read_Property) { + apdu_len = pObject->Object_Read_Property(rpdata); + } + + return apdu_len; +} + /** Looks up the requested Object and Property, and encodes its Value in an * APDU. * @ingroup ObjIntf @@ -1092,10 +1229,14 @@ int Device_Read_Property(BACNET_READ_PROPERTY_DATA *rpdata) if (pObject != NULL) { if (pObject->Object_Valid_Instance && pObject->Object_Valid_Instance(rpdata->object_instance)) { - if (pObject->Object_Read_Property) { - apdu_len = pObject->Object_Read_Property(rpdata); + apdu_len = Read_Property_Common(pObject, rpdata); + } else { + rpdata->error_class = ERROR_CLASS_OBJECT; + rpdata->error_code = ERROR_CODE_UNKNOWN_OBJECT; } - } + } else { + rpdata->error_class = ERROR_CLASS_OBJECT; + rpdata->error_code = ERROR_CODE_UNKNOWN_OBJECT; } return apdu_len; @@ -1109,7 +1250,10 @@ bool Device_Write_Property_Local(BACNET_WRITE_PROPERTY_DATA *wp_data) BACNET_APPLICATION_DATA_VALUE value; BACNET_OBJECT_TYPE object_type = OBJECT_NONE; uint32_t object_instance = 0; - int temp; + int result = 0; +#if defined(BACNET_TIME_MASTER) + uint32_t minutes = 0; +#endif /* decode the some of the request */ len = bacapp_decode_application_data( @@ -1173,12 +1317,13 @@ bool Device_Write_Property_Local(BACNET_WRITE_PROPERTY_DATA *wp_data) status = write_property_type_valid( wp_data, &value, BACNET_APPLICATION_TAG_ENUMERATED); if (status) { - temp = Device_Set_System_Status( + result = Device_Set_System_Status( (BACNET_DEVICE_STATUS)value.type.Enumerated, false); - if (temp != 0) { + if (result != 0) { + /* result: - 0 = ok, -1 = bad value, -2 = not allowed */ status = false; wp_data->error_class = ERROR_CLASS_PROPERTY; - if (temp == -1) { + if (result == -1) { wp_data->error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; } else { wp_data->error_code = @@ -1236,9 +1381,67 @@ bool Device_Write_Property_Local(BACNET_WRITE_PROPERTY_DATA *wp_data) characterstring_length(&value.type.Character_String)); } break; - - case PROP_MAX_INFO_FRAMES: +#if defined(BACNET_TIME_MASTER) + case PROP_TIME_SYNCHRONIZATION_INTERVAL: + status = write_property_type_valid( + wp_data, &value, BACNET_APPLICATION_TAG_UNSIGNED_INT); + if (status) { + if (value.type.Unsigned_Int < 65535) { + minutes = value.type.Unsigned_Int; + Device_Time_Sync_Interval_Set(minutes); + status = true; + } else { + wp_data->error_class = ERROR_CLASS_PROPERTY; + wp_data->error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; + } + } + break; + case PROP_ALIGN_INTERVALS: + status = write_property_type_valid( + wp_data, &value, BACNET_APPLICATION_TAG_BOOLEAN); + if (status) { + Device_Align_Intervals_Set(value.type.Boolean); + status = true; + } + break; + case PROP_INTERVAL_OFFSET: + status = write_property_type_valid( + wp_data, &value, BACNET_APPLICATION_TAG_UNSIGNED_INT); + if (status) { + if (value.type.Unsigned_Int < 65535) { + minutes = value.type.Unsigned_Int; + Device_Interval_Offset_Set(minutes); + status = true; + } else { + wp_data->error_class = ERROR_CLASS_PROPERTY; + wp_data->error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; + } + } + break; +#else + case PROP_TIME_SYNCHRONIZATION_INTERVAL: + case PROP_ALIGN_INTERVALS: + case PROP_INTERVAL_OFFSET: + wp_data->error_class = ERROR_CLASS_PROPERTY; + wp_data->error_code = ERROR_CODE_UNKNOWN_PROPERTY; + break; +#endif + case PROP_UTC_OFFSET: + status = write_property_type_valid( + wp_data, &value, BACNET_APPLICATION_TAG_SIGNED_INT); + if (status) { + if ((value.type.Signed_Int < (12 * 60)) && + (value.type.Signed_Int > (-12 * 60))) { + Device_UTC_Offset_Set(value.type.Signed_Int); + status = true; + } else { + wp_data->error_class = ERROR_CLASS_PROPERTY; + wp_data->error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; + } + } + break; #if defined(BACDL_MSTP) + case PROP_MAX_INFO_FRAMES: status = write_property_type_valid( wp_data, &value, BACNET_APPLICATION_TAG_UNSIGNED_INT); if (status) { @@ -1252,9 +1455,7 @@ bool Device_Write_Property_Local(BACNET_WRITE_PROPERTY_DATA *wp_data) } } break; -#endif case PROP_MAX_MASTER: -#if defined(BACDL_MSTP) status = write_property_type_valid( wp_data, &value, BACNET_APPLICATION_TAG_UNSIGNED_INT); if (status) { @@ -1268,13 +1469,18 @@ bool Device_Write_Property_Local(BACNET_WRITE_PROPERTY_DATA *wp_data) } } break; +#else + case PROP_MAX_INFO_FRAMES: + case PROP_MAX_MASTER: + wp_data->error_class = ERROR_CLASS_PROPERTY; + wp_data->error_code = ERROR_CODE_UNKNOWN_PROPERTY; + break; #endif case PROP_OBJECT_TYPE: case PROP_VENDOR_NAME: case PROP_FIRMWARE_REVISION: case PROP_APPLICATION_SOFTWARE_VERSION: case PROP_LOCAL_TIME: - case PROP_UTC_OFFSET: case PROP_LOCAL_DATE: case PROP_DAYLIGHT_SAVINGS_STATUS: case PROP_PROTOCOL_VERSION: @@ -1287,6 +1493,9 @@ bool Device_Write_Property_Local(BACNET_WRITE_PROPERTY_DATA *wp_data) case PROP_DEVICE_ADDRESS_BINDING: case PROP_DATABASE_REVISION: case PROP_ACTIVE_COV_SUBSCRIPTIONS: +#if defined(BACNET_TIME_MASTER) + case PROP_TIME_SYNCHRONIZATION_RECIPIENTS: +#endif wp_data->error_class = ERROR_CLASS_PROPERTY; wp_data->error_code = ERROR_CODE_WRITE_ACCESS_DENIED; break; @@ -1321,7 +1530,15 @@ bool Device_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) if (pObject->Object_Valid_Instance && pObject->Object_Valid_Instance(wp_data->object_instance)) { if (pObject->Object_Write_Property) { +#if (BACNET_PROTOCOL_REVISION >= 14) + if (wp_data->object_property == PROP_PROPERTY_LIST) { + wp_data->error_class = ERROR_CLASS_PROPERTY; + wp_data->error_code = ERROR_CODE_WRITE_ACCESS_DENIED; + } else +#endif + { status = pObject->Object_Write_Property(wp_data); + } } else { wp_data->error_class = ERROR_CLASS_PROPERTY; wp_data->error_code = ERROR_CODE_WRITE_ACCESS_DENIED; @@ -1338,6 +1555,76 @@ bool Device_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) return (status); } +/** + * @brief AddListElement from an object list property + * @param list_element [in] Pointer to the BACnet_List_Element_Data structure, + * which is packed with the information from the request. + * @return The length of the apdu encoded or #BACNET_STATUS_ERROR or + * #BACNET_STATUS_ABORT or #BACNET_STATUS_REJECT. + */ +int Device_Add_List_Element( + BACNET_LIST_ELEMENT_DATA * list_element) +{ + int status = BACNET_STATUS_ERROR; + struct object_functions *pObject = NULL; + + pObject = Device_Objects_Find_Functions(list_element->object_type); + if (pObject != NULL) { + if (pObject->Object_Valid_Instance && + pObject->Object_Valid_Instance(list_element->object_instance)) { + if (pObject->Object_Add_List_Element) { + status = pObject->Object_Add_List_Element(list_element); + } else { + list_element->error_class = ERROR_CLASS_PROPERTY; + list_element->error_code = ERROR_CODE_WRITE_ACCESS_DENIED; + } + } else { + list_element->error_class = ERROR_CLASS_OBJECT; + list_element->error_code = ERROR_CODE_UNKNOWN_OBJECT; + } + } else { + list_element->error_class = ERROR_CLASS_OBJECT; + list_element->error_code = ERROR_CODE_UNKNOWN_OBJECT; + } + + return status; +} + +/** + * @brief RemoveListElement from an object list property + * @param list_element [in] Pointer to the BACnet_List_Element_Data structure, + * which is packed with the information from the request. + * @return The length of the apdu encoded or #BACNET_STATUS_ERROR or + * #BACNET_STATUS_ABORT or #BACNET_STATUS_REJECT. + */ +int Device_Remove_List_Element( + BACNET_LIST_ELEMENT_DATA * list_element) +{ + int status = BACNET_STATUS_ERROR; + struct object_functions *pObject = NULL; + + pObject = Device_Objects_Find_Functions(list_element->object_type); + if (pObject != NULL) { + if (pObject->Object_Valid_Instance && + pObject->Object_Valid_Instance(list_element->object_instance)) { + if (pObject->Object_Remove_List_Element) { + status = pObject->Object_Remove_List_Element(list_element); + } else { + list_element->error_class = ERROR_CLASS_PROPERTY; + list_element->error_code = ERROR_CODE_WRITE_ACCESS_DENIED; + } + } else { + list_element->error_class = ERROR_CLASS_OBJECT; + list_element->error_code = ERROR_CODE_UNKNOWN_OBJECT; + } + } else { + list_element->error_class = ERROR_CLASS_OBJECT; + list_element->error_code = ERROR_CODE_UNKNOWN_OBJECT; + } + + return status; +} + /** Looks up the requested Object, and fills the Property Value list. * If the Object or Property can't be found, returns false. * @ingroup ObjHelpers @@ -1411,33 +1698,110 @@ void Device_COV_Clear(BACNET_OBJECT_TYPE object_type, uint32_t object_instance) } } -#if defined(INTRINSIC_REPORTING) -void Device_local_reporting(void) +/** + * @brief Creates a child object, if supported + * @ingroup ObjHelpers + * @param data - CreateObject data, including error codes if failures + * @return true if object has been created + */ +bool Device_Create_Object( + BACNET_CREATE_OBJECT_DATA *data) { - struct object_functions *pObject; - uint32_t objects_count; + bool status = false; + struct object_functions *pObject = NULL; uint32_t object_instance; - BACNET_OBJECT_TYPE object_type; - uint32_t idx; - objects_count = Device_Object_List_Count(); - - /* loop for all objects */ - for (idx = 1; idx < objects_count; idx++) { - Device_Object_List_Identifier(idx, &object_type, &object_instance); - - pObject = Device_Objects_Find_Functions(object_type); - if (pObject != NULL) { - if (pObject->Object_Valid_Instance && - pObject->Object_Valid_Instance(object_instance)) { - if (pObject->Object_Intrinsic_Reporting) { - pObject->Object_Intrinsic_Reporting(object_instance); + pObject = Device_Objects_Find_Functions(data->object_type); + if (pObject != NULL) { + if (!pObject->Object_Create) { + /* The device supports the object type and may have + sufficient space, but does not support the creation of the + object for some other reason.*/ + data->error_class = ERROR_CLASS_OBJECT; + data->error_code = ERROR_CODE_DYNAMIC_CREATION_NOT_SUPPORTED; + } else if (pObject->Object_Valid_Instance && + pObject->Object_Valid_Instance(data->object_instance)) { + /* The object being created already exists */ + data->error_class = ERROR_CLASS_OBJECT; + data->error_code = ERROR_CODE_OBJECT_IDENTIFIER_ALREADY_EXISTS; + } else { + if (data->list_of_initial_values) { + /* FIXME: add support for writing to list of initial values */ + /* A property specified by the Property_Identifier in the + List of Initial Values does not support initialization + during the CreateObject service. */ + data->first_failed_element_number = 1; + data->error_class = ERROR_CLASS_PROPERTY; + data->error_code = ERROR_CODE_WRITE_ACCESS_DENIED; + /* and the object shall not be created */ + } else { + object_instance = pObject->Object_Create(data->object_instance); + if (object_instance == BACNET_MAX_INSTANCE) { + /* The device cannot allocate the space needed + for the new object.*/ + data->error_class = ERROR_CLASS_RESOURCES; + data->error_code = ERROR_CODE_NO_SPACE_FOR_OBJECT; + } else { + /* required by ACK */ + data->object_instance = object_instance; + Device_Inc_Database_Revision(); + status = true; } } } + } else { + /* The device does not support the specified object type. */ + data->error_class = ERROR_CLASS_OBJECT; + data->error_code = ERROR_CODE_UNSUPPORTED_OBJECT_TYPE; } + + return status; +} + +/** + * @brief Deletes a child object, if supported + * @ingroup ObjHelpers + * @param data - DeleteObject data, including error codes if failures + * @return true if object has been deleted + */ +bool Device_Delete_Object( + BACNET_DELETE_OBJECT_DATA *data) +{ + bool status = false; + struct object_functions *pObject = NULL; + + pObject = Device_Objects_Find_Functions(data->object_type); + if (pObject != NULL) { + if (!pObject->Object_Delete) { + /* The device supports the object type + but does not support the deletion of the + object for some reason.*/ + data->error_class = ERROR_CLASS_OBJECT; + data->error_code = ERROR_CODE_OBJECT_DELETION_NOT_PERMITTED; + } else if (pObject->Object_Valid_Instance && + pObject->Object_Valid_Instance(data->object_instance)) { + /* The object being deleted must already exist */ + status = pObject->Object_Delete(data->object_instance); + if (status) { + Device_Inc_Database_Revision(); + } else { + /* The object exists but cannot be deleted. */ + data->error_class = ERROR_CLASS_OBJECT; + data->error_code = ERROR_CODE_OBJECT_DELETION_NOT_PERMITTED; + } + } else { + /* The object to be deleted does not exist. */ + data->error_class = ERROR_CLASS_OBJECT; + data->error_code = ERROR_CODE_UNKNOWN_OBJECT; + } + } else { + /* The device does not support the specified object type. */ + data->error_class = ERROR_CLASS_OBJECT; + data->error_code = ERROR_CODE_UNSUPPORTED_OBJECT_TYPE; + } + + return status; } -#endif /** Looks up the requested Object to see if the functionality is supported. * @ingroup ObjHelpers @@ -1470,24 +1834,8 @@ bool Device_Value_List_Supported(BACNET_OBJECT_TYPE object_type) void Device_Init(object_functions_t *object_table) { struct object_functions *pObject = NULL; -#if defined(BAC_UCI) - const char *uciname; - struct uci_context *ctx; - fprintf(stderr, "Device_Init\n"); - ctx = ucix_init("bacnet_dev"); - if (!ctx) - fprintf(stderr, "Failed to load config file bacnet_dev\n"); - uciname = ucix_get_option(ctx, "bacnet_dev", "0", "Name"); - if (uciname != 0) { - characterstring_init_ansi(&My_Object_Name, uciname); - } else { -#endif /* defined(BAC_UCI) */ - characterstring_init_ansi(&My_Object_Name, "PiFace Digital Demo"); -#if defined(BAC_UCI) - } - ucix_cleanup(ctx); -#endif /* defined(BAC_UCI) */ - + characterstring_init_ansi(&My_Object_Name, "PiFace"); + datetime_init(); if (object_table) { Object_Table = object_table; } else { @@ -1540,37 +1888,3 @@ bool DeviceGetRRInfo(BACNET_READ_RANGE_DATA *pRequest, /* Info on the request */ return status; } - -#ifdef BAC_ROUTING -/**************************************************************************** - ************* BACnet Routing Functionality (Optional) ********************** - **************************************************************************** - * The supporting functions are located in gw_device.c, except for those - * that need access to local data in this file. - ****************************************************************************/ - -/** Initialize the first of our array of Devices with the main Device's - * information, and then swap out some of the Device object functions and - * replace with ones appropriate for routing. - * @ingroup ObjIntf - * @param first_object_instance Set the first (gateway) Device to this - instance number. - */ -void Routing_Device_Init(uint32_t first_object_instance) -{ - struct object_functions *pDevObject = NULL; - - /* Initialize with our preset strings */ - Add_Routed_Device(first_object_instance, &My_Object_Name, Description); - - /* Now substitute our routed versions of the main object functions. */ - pDevObject = Object_Table; - pDevObject->Object_Index_To_Instance = Routed_Device_Index_To_Instance; - pDevObject->Object_Valid_Instance = - Routed_Device_Valid_Object_Instance_Number; - pDevObject->Object_Name = Routed_Device_Name; - pDevObject->Object_Read_Property = Routed_Device_Read_Property_Local; - pDevObject->Object_Write_Property = Routed_Device_Write_Property_Local; -} - -#endif /* BAC_ROUTING */ diff --git a/apps/server/main.c b/apps/server/main.c index 682030ed..c17ae6bd 100644 --- a/apps/server/main.c +++ b/apps/server/main.c @@ -28,7 +28,6 @@ #include #include #include -#include #include "bacnet/config.h" #include "bacnet/bacdef.h" #include "bacnet/bacdcode.h" @@ -41,12 +40,21 @@ #include "bacnet/basic/services.h" #include "bacnet/datalink/dlenv.h" #include "bacnet/basic/sys/filename.h" +#include "bacnet/basic/sys/mstimer.h" #include "bacnet/basic/tsm/tsm.h" #include "bacnet/basic/tsm/tsm.h" #include "bacnet/datalink/datalink.h" #include "bacnet/basic/binding/address.h" /* include the device object */ #include "bacnet/basic/object/device.h" +/* objects that have tasks inside them */ +#if (BACNET_PROTOCOL_REVISION >= 14) +#include "bacnet/basic/object/lo.h" +#endif +#if (BACNET_PROTOCOL_REVISION >= 24) +#include "bacnet/basic/object/color_object.h" +#include "bacnet/basic/object/color_temperature.h" +#endif #include "bacnet/basic/object/lc.h" #include "bacnet/basic/object/trendlog.h" #if defined(INTRINSIC_REPORTING) @@ -68,7 +76,18 @@ /* current version of the BACnet stack */ static const char *BACnet_Version = BACNET_VERSION_TEXT; - +/* task timer for various BACnet timeouts */ +static struct mstimer BACnet_Task_Timer; +/* task timer for TSM timeouts */ +static struct mstimer BACnet_TSM_Timer; +/* task timer for address binding timeouts */ +static struct mstimer BACnet_Address_Timer; +#if defined(INTRINSIC_REPORTING) +/* task timer for notification recipient timeouts */ +static struct mstimer BACnet_Notification_Timer; +#endif +/* task timer for objects */ +static struct mstimer BACnet_Object_Timer; /** Buffer used for receiving */ static uint8_t Rx_Buf[MAX_MPDU] = { 0 }; @@ -148,6 +167,14 @@ static void Init_Service_Handlers(void) SERVICE_CONFIRMED_CREATE_OBJECT, handler_create_object); apdu_set_confirmed_handler( SERVICE_CONFIRMED_DELETE_OBJECT, handler_delete_object); + /* configure the cyclic timers */ + mstimer_set(&BACnet_Task_Timer, 1000UL); + mstimer_set(&BACnet_TSM_Timer, 50UL); + mstimer_set(&BACnet_Address_Timer, 60UL*1000UL); + mstimer_set(&BACnet_Object_Timer, 100UL); +#if defined(INTRINSIC_REPORTING) + mstimer_set(&BACnet_Notification_Timer, NC_RESCAN_RECIPIENTS_SECS*1000UL); +#endif } static void print_usage(const char *filename) @@ -190,15 +217,9 @@ int main(int argc, char *argv[]) BACNET_ADDRESS src = { 0 }; /* address where message came from */ uint16_t pdu_len = 0; unsigned timeout = 1; /* milliseconds */ - time_t last_seconds = 0; - time_t current_seconds = 0; - uint32_t elapsed_seconds = 0; uint32_t elapsed_milliseconds = 0; - uint32_t address_binding_tmr = 0; + uint32_t elapsed_seconds = 0; BACNET_CHARACTER_STRING DeviceName; -#if defined(INTRINSIC_REPORTING) - uint32_t recipient_scan_tmr = 0; -#endif #if defined(BACNET_TIME_MASTER) BACNET_DATE_TIME bdatetime; #endif @@ -277,33 +298,27 @@ int main(int argc, char *argv[]) dlenv_init(); atexit(datalink_cleanup); - /* configure the timeout values */ - last_seconds = time(NULL); /* broadcast an I-Am on startup */ Send_I_Am(&Handler_Transmit_Buffer[0]); /* loop forever */ for (;;) { /* input */ - current_seconds = time(NULL); - - /* returns 0 bytes on timeout */ pdu_len = datalink_receive(&src, &Rx_Buf[0], MAX_MPDU, timeout); /* process */ if (pdu_len) { npdu_handler(&src, &Rx_Buf[0], pdu_len); } - /* at least one second has passed */ - elapsed_seconds = (uint32_t)(current_seconds - last_seconds); - if (elapsed_seconds) { - last_seconds = current_seconds; + if (mstimer_expired(&BACnet_Task_Timer)) { + mstimer_reset(&BACnet_Task_Timer); + elapsed_milliseconds = mstimer_interval(&BACnet_Task_Timer); + elapsed_seconds = elapsed_milliseconds/1000; + /* 1 second tasks */ dcc_timer_seconds(elapsed_seconds); datalink_maintenance_timer(elapsed_seconds); dlenv_maintenance_timer(elapsed_seconds); - Load_Control_State_Machine_Handler(); - elapsed_milliseconds = elapsed_seconds * 1000; handler_cov_timer_seconds(elapsed_seconds); - tsm_timer_milliseconds(elapsed_milliseconds); + Load_Control_State_Machine_Handler(); trend_log_timer(elapsed_seconds); #if defined(INTRINSIC_REPORTING) Device_local_reporting(); @@ -313,24 +328,30 @@ int main(int argc, char *argv[]) handler_timesync_task(&bdatetime); #endif } - handler_cov_task(); - /* scan cache address */ - address_binding_tmr += elapsed_seconds; - if (address_binding_tmr >= 60) { - address_cache_timer(address_binding_tmr); - address_binding_tmr = 0; + if (mstimer_expired(&BACnet_TSM_Timer)) { + mstimer_reset(&BACnet_TSM_Timer); + elapsed_milliseconds = mstimer_interval(&BACnet_TSM_Timer); + tsm_timer_milliseconds(elapsed_milliseconds); } + if (mstimer_expired(&BACnet_Address_Timer)) { + mstimer_reset(&BACnet_Address_Timer); + elapsed_milliseconds = mstimer_interval(&BACnet_Address_Timer); + elapsed_seconds = elapsed_milliseconds/1000; + address_cache_timer(elapsed_seconds); + } + handler_cov_task(); #if defined(INTRINSIC_REPORTING) - /* try to find addresses of recipients */ - recipient_scan_tmr += elapsed_seconds; - if (recipient_scan_tmr >= NC_RESCAN_RECIPIENTS_SECS) { + if (mstimer_expired(&BACnet_Notification_Timer)) { + mstimer_reset(&BACnet_Notification_Timer); Notification_Class_find_recipient(); - recipient_scan_tmr = 0; } #endif /* output */ - - /* blink LEDs, Turn on or off outputs, etc */ + if (mstimer_expired(&BACnet_Object_Timer)) { + mstimer_reset(&BACnet_Object_Timer); + elapsed_milliseconds = mstimer_interval(&BACnet_Object_Timer); + Device_Timer(elapsed_milliseconds); + } } return 0; diff --git a/apps/writeprop/Makefile b/apps/writeprop/Makefile index bb0d8eed..79ee5472 100644 --- a/apps/writeprop/Makefile +++ b/apps/writeprop/Makefile @@ -2,6 +2,9 @@ # Executable file name TARGET = bacwp + +CFLAGS += -DBACAPP_COLOR_RGB_CONVERSION_ENABLED=1 + # BACnet objects that are used with this app BACNET_OBJECT_DIR = $(BACNET_SRC_DIR)/bacnet/basic/object SRC = main.c \ diff --git a/apps/writepropm/Makefile b/apps/writepropm/Makefile index b458de02..2f4a092c 100644 --- a/apps/writepropm/Makefile +++ b/apps/writepropm/Makefile @@ -2,6 +2,9 @@ # Executable file name TARGET = bacwpm + +CFLAGS += -DBACAPP_COLOR_RGB_CONVERSION_ENABLED=1 + # BACnet objects that are used with this app BACNET_OBJECT_DIR = $(BACNET_SRC_DIR)/bacnet/basic/object SRC = main.c \ diff --git a/src/bacnet/bacapp.c b/src/bacnet/bacapp.c index a97aba58..64635b9b 100644 --- a/src/bacnet/bacapp.c +++ b/src/bacnet/bacapp.c @@ -901,7 +901,7 @@ BACNET_APPLICATION_TAG bacapp_context_tag_type( time [0] Time, -- deprecated in version 1 revision 21 sequence-number [1] Unsigned (0..65535), datetime [2] BACnetDateTime - } + } */ switch (tag_number) { case TIME_STAMP_TIME: @@ -2155,6 +2155,16 @@ int bacapp_snprintf_value( ret_val = snprintf(str, str_len, "%s", bactext_node_type_name(value->type.Enumerated)); break; + case PROP_TRANSITION: + ret_val = snprintf(str, str_len, "%s", + bactext_lighting_transition( + value->type.Enumerated)); + break; + case PROP_IN_PROGRESS: + ret_val = snprintf(str, str_len, "%s", + bactext_lighting_in_progress( + value->type.Enumerated)); + break; default: ret_val = snprintf(str, str_len, "%lu", (unsigned long)value->type.Enumerated); @@ -2267,9 +2277,8 @@ int bacapp_snprintf_value( break; case BACNET_APPLICATION_TAG_XY_COLOR: /* BACnetxyColor */ - ret_val = snprintf(str, str_len, "(%f,%f)", - value->type.XY_Color.x_coordinate, - value->type.XY_Color.x_coordinate); + ret_val = xy_color_to_ascii(&value->type.XY_Color, str, + str_len); break; case BACNET_APPLICATION_TAG_COLOR_COMMAND: /* BACnetColorCommand */ @@ -2641,7 +2650,6 @@ bool bacapp_parse_application_data(BACNET_APPLICATION_TAG tag_number, int count = 0; #if defined(BACAPP_TYPES_EXTRA) unsigned a[4] = { 0 }, p = 0; - float x, y; #endif if (value && (tag_number != MAX_BACNET_APPLICATION_TAG)) { @@ -2793,13 +2801,7 @@ bool bacapp_parse_application_data(BACNET_APPLICATION_TAG tag_number, break; case BACNET_APPLICATION_TAG_XY_COLOR: /* BACnetxyColor */ - count = sscanf(argv, "%f,%f", &x, &y); - if (count == 2) { - value->type.XY_Color.x_coordinate = x; - value->type.XY_Color.y_coordinate = y; - } else { - status = false; - } + status = xy_color_from_ascii(&value->type.XY_Color, argv); break; case BACNET_APPLICATION_TAG_COLOR_COMMAND: /* FIXME: add parsing for BACnetColorCommand */ diff --git a/src/bacnet/bacdevobjpropref.c b/src/bacnet/bacdevobjpropref.c index 097ba22f..f3c3467a 100644 --- a/src/bacnet/bacdevobjpropref.c +++ b/src/bacnet/bacdevobjpropref.c @@ -100,29 +100,42 @@ int bacapp_encode_device_obj_property_ref( int len; int apdu_len = 0; + if (!value) { + return apdu_len; + } /* object-identifier [0] BACnetObjectIdentifier */ - len = encode_context_object_id(&apdu[apdu_len], 0, + len = encode_context_object_id(apdu, 0, value->objectIdentifier.type, value->objectIdentifier.instance); apdu_len += len; + if (apdu) { + apdu += len; + } /* property-identifier [1] BACnetPropertyIdentifier */ len = encode_context_enumerated( - &apdu[apdu_len], 1, value->propertyIdentifier); + apdu, 1, value->propertyIdentifier); apdu_len += len; + if (apdu) { + apdu += len; + } /* property-array-index [2] Unsigned OPTIONAL */ /* Check if needed before inserting */ if (value->arrayIndex != BACNET_ARRAY_ALL) { - len = encode_context_unsigned(&apdu[apdu_len], 2, value->arrayIndex); + len = encode_context_unsigned(apdu, 2, value->arrayIndex); apdu_len += len; + if (apdu) { + apdu += len; + } } /* device-identifier [3] BACnetObjectIdentifier OPTIONAL */ /* Likewise, device id is optional so see if needed * (set type to BACNET_NO_DEV_TYPE or something other than OBJECT_DEVICE to * omit */ if (value->deviceIdentifier.type == OBJECT_DEVICE) { - len = encode_context_object_id(&apdu[apdu_len], 3, + len = encode_context_object_id(apdu, 3, value->deviceIdentifier.type, value->deviceIdentifier.instance); apdu_len += len; } + return apdu_len; } diff --git a/src/bacnet/bacenum.h b/src/bacnet/bacenum.h index b93acd8c..e3d56a48 100644 --- a/src/bacnet/bacenum.h +++ b/src/bacnet/bacenum.h @@ -2049,10 +2049,9 @@ typedef enum BACnetLightingInProgress { } BACNET_LIGHTING_IN_PROGRESS; typedef enum BACnetLightingTransition { - BACNET_LIGHTING_TRANSITION_IDLE = 0, + BACNET_LIGHTING_TRANSITION_NONE = 0, BACNET_LIGHTING_TRANSITION_FADE = 1, BACNET_LIGHTING_TRANSITION_RAMP = 2, - MAX_BACNET_LIGHTING_TRANSITION = 3, /* Enumerated values 0-63 are reserved for definition by ASHRAE. Enumerated values 64-255 may be used by others subject to the procedures and constraints described in Clause 23. */ diff --git a/src/bacnet/bactext.c b/src/bacnet/bactext.c index 5c07ebee..3c27c5ea 100644 --- a/src/bacnet/bactext.c +++ b/src/bacnet/bactext.c @@ -1689,14 +1689,14 @@ const char *bactext_life_safety_state_name(unsigned index) return indtext_by_index_default( life_safety_state_names, index, ASHRAE_Reserved_String); } else { - return "Invalid Safety State Message"; + return "Invalid BACnetLifeSafetyState"; } } INDTEXT_DATA lighting_in_progress[] = { { BACNET_LIGHTING_IDLE, "idle" }, { BACNET_LIGHTING_FADE_ACTIVE, "fade" }, { BACNET_LIGHTING_RAMP_ACTIVE, "ramp" }, - { BACNET_LIGHTING_NOT_CONTROLLED, "not" }, + { BACNET_LIGHTING_NOT_CONTROLLED, "not-controlled" }, { BACNET_LIGHTING_OTHER, "other" }, { BACNET_LIGHTING_TRIM_ACTIVE, "trim-active" }, { 0, NULL } }; @@ -1706,22 +1706,24 @@ const char *bactext_lighting_in_progress(unsigned index) return indtext_by_index_default( lighting_in_progress, index, ASHRAE_Reserved_String); } else { - return "Invalid Lighting In Progress Message"; + return "Invalid BACnetLightingInProgress"; } } -INDTEXT_DATA lighting_transition[] = { { BACNET_LIGHTING_TRANSITION_IDLE, - "idle" }, +INDTEXT_DATA lighting_transition[] = { + { BACNET_LIGHTING_TRANSITION_NONE, "none" }, { BACNET_LIGHTING_TRANSITION_FADE, "fade" }, { BACNET_LIGHTING_TRANSITION_RAMP, "ramp" }, { 0, NULL } }; const char *bactext_lighting_transition(unsigned index) { - if (index < MAX_BACNET_LIGHTING_TRANSITION) { + if (index < BACNET_LIGHTING_TRANSITION_PROPRIETARY_FIRST) { return indtext_by_index_default( lighting_transition, index, ASHRAE_Reserved_String); + } else if (index <= BACNET_LIGHTING_TRANSITION_PROPRIETARY_LAST) { + return Vendor_Proprietary_String; } else { - return "Invalid Lighting Transition Message"; + return "Invalid BACnetLightingTransition"; } } diff --git a/src/bacnet/basic/object/ao.c b/src/bacnet/basic/object/ao.c index 90d40677..2db64b87 100644 --- a/src/bacnet/basic/object/ao.c +++ b/src/bacnet/basic/object/ao.c @@ -1208,9 +1208,9 @@ uint32_t Analog_Output_Create(uint32_t object_instance) return BACNET_MAX_INSTANCE; } else if (object_instance == BACNET_MAX_INSTANCE) { /* wildcard instance */ - /* the Object_Identifier property of the newly created object - shall be initialized to a value that is unique within the - responding BACnet-user device. The method used to generate + /* the Object_Identifier property of the newly created object + shall be initialized to a value that is unique within the + responding BACnet-user device. The method used to generate the object identifier is a local matter.*/ object_instance = Keylist_Next_Empty_Key(Object_List, 1); } @@ -1235,9 +1235,7 @@ uint32_t Analog_Output_Create(uint32_t object_instance) pObject->Max_Pres_Value = 100; /* add to list */ index = Keylist_Data_Add(Object_List, object_instance, pObject); - if (index >= 0) { - Device_Inc_Database_Revision(); - } else { + if (index < 0) { free(pObject); return BACNET_MAX_INSTANCE; } @@ -1263,7 +1261,6 @@ bool Analog_Output_Delete(uint32_t object_instance) if (pObject) { free(pObject); status = true; - Device_Inc_Database_Revision(); } return status; @@ -1281,7 +1278,6 @@ void Analog_Output_Cleanup(void) pObject = Keylist_Data_Pop(Object_List); if (pObject) { free(pObject); - Device_Inc_Database_Revision(); } } while (pObject); Keylist_Delete(Object_List); diff --git a/src/bacnet/basic/object/bacfile.c b/src/bacnet/basic/object/bacfile.c index 51c2b939..6945dbfb 100644 --- a/src/bacnet/basic/object/bacfile.c +++ b/src/bacnet/basic/object/bacfile.c @@ -990,9 +990,9 @@ uint32_t bacfile_create(uint32_t object_instance) return BACNET_MAX_INSTANCE; } else if (object_instance == BACNET_MAX_INSTANCE) { /* wildcard instance */ - /* the Object_Identifier property of the newly created object - shall be initialized to a value that is unique within the - responding BACnet-user device. The method used to generate + /* the Object_Identifier property of the newly created object + shall be initialized to a value that is unique within the + responding BACnet-user device. The method used to generate the object identifier is a local matter.*/ object_instance = Keylist_Next_Empty_Key(Object_List, 1); } @@ -1010,9 +1010,7 @@ uint32_t bacfile_create(uint32_t object_instance) pObject->File_Access_Stream = true; /* add to list */ index = Keylist_Data_Add(Object_List, object_instance, pObject); - if (index >= 0) { - Device_Inc_Database_Revision(); - } else { + if (index < 0) { free(pObject); return BACNET_MAX_INSTANCE; } @@ -1038,7 +1036,6 @@ bool bacfile_delete(uint32_t object_instance) if (pObject) { free(pObject); status = true; - Device_Inc_Database_Revision(); } return status; @@ -1056,7 +1053,6 @@ void bacfile_cleanup(void) pObject = Keylist_Data_Pop(Object_List); if (pObject) { free(pObject); - Device_Inc_Database_Revision(); } } while (pObject); Keylist_Delete(Object_List); diff --git a/src/bacnet/basic/object/bo.c b/src/bacnet/basic/object/bo.c index cd6b0d54..f5ae225b 100644 --- a/src/bacnet/basic/object/bo.c +++ b/src/bacnet/basic/object/bo.c @@ -1183,9 +1183,9 @@ uint32_t Binary_Output_Create(uint32_t object_instance) return BACNET_MAX_INSTANCE; } else if (object_instance == BACNET_MAX_INSTANCE) { /* wildcard instance */ - /* the Object_Identifier property of the newly created object - shall be initialized to a value that is unique within the - responding BACnet-user device. The method used to generate + /* the Object_Identifier property of the newly created object + shall be initialized to a value that is unique within the + responding BACnet-user device. The method used to generate the object identifier is a local matter.*/ object_instance = Keylist_Next_Empty_Key(Object_List, 1); } @@ -1202,9 +1202,7 @@ uint32_t Binary_Output_Create(uint32_t object_instance) pObject->Changed = false; /* add to list */ index = Keylist_Data_Add(Object_List, object_instance, pObject); - if (index >= 0) { - Device_Inc_Database_Revision(); - } else { + if (index < 0) { free(pObject); return BACNET_MAX_INSTANCE; } @@ -1228,7 +1226,6 @@ void Binary_Output_Cleanup(void) pObject = Keylist_Data_Pop(Object_List); if (pObject) { free(pObject); - Device_Inc_Database_Revision(); } } while (pObject); Keylist_Delete(Object_List); @@ -1248,7 +1245,6 @@ bool Binary_Output_Delete(uint32_t object_instance) if (pObject) { free(pObject); status = true; - Device_Inc_Database_Revision(); } return status; diff --git a/src/bacnet/basic/object/channel.c b/src/bacnet/basic/object/channel.c index 173197bc..37c52760 100644 --- a/src/bacnet/basic/object/channel.c +++ b/src/bacnet/basic/object/channel.c @@ -31,10 +31,11 @@ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +#include +#include +#include #include #include -#include -#include #include "bacnet/bacdef.h" #include "bacnet/bacdcode.h" #include "bacnet/bacenum.h" @@ -43,18 +44,16 @@ #include "bacnet/wp.h" #include "bacnet/basic/services.h" #include "bacnet/proplist.h" -#include "bacnet/lighting.h" -#include "bacnet/basic/object/device.h" +#include "bacnet/basic/sys/keylist.h" #if defined(CHANNEL_LIGHTING_COMMAND) #include "bacnet/basic/object/lo.h" #endif +#if defined(CHANNEL_LIGHTING_COMMAND) || defined(CHANNEL_COLOR_COMMAND) +#include "bacnet/lighting.h" +#endif /* me! */ #include "bacnet/basic/object/channel.h" -#ifndef BACNET_CHANNELS_MAX -#define BACNET_CHANNELS_MAX 1 -#endif - #ifndef CONTROL_GROUPS_MAX #define CONTROL_GROUPS_MAX 8 #endif @@ -63,7 +62,7 @@ #define CHANNEL_MEMBERS_MAX 8 #endif -struct bacnet_channel_object { +struct object_data { bool Out_Of_Service : 1; BACNET_CHANNEL_VALUE Present_Value; unsigned Last_Priority; @@ -71,9 +70,14 @@ struct bacnet_channel_object { BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE Members[CHANNEL_MEMBERS_MAX]; uint16_t Number; uint32_t Control_Groups[CONTROL_GROUPS_MAX]; + const char *Object_Name; + const char *Description; }; -static struct bacnet_channel_object Channel[BACNET_CHANNELS_MAX]; +/* Key List for storing the object data sorted by instance number */ +static OS_Keylist Object_List; + +static write_property_function Write_Property_Internal_Callback; /* These arrays are used by the ReadPropertyMultiple handler property-list property (as of protocol-revision 14) */ @@ -123,10 +127,10 @@ void Channel_Property_Lists( */ bool Channel_Valid_Instance(uint32_t object_instance) { - unsigned int index; + struct object_data *pObject; - index = Channel_Instance_To_Index(object_instance); - if (index < BACNET_CHANNELS_MAX) { + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { return true; } @@ -140,7 +144,7 @@ bool Channel_Valid_Instance(uint32_t object_instance) */ unsigned Channel_Count(void) { - return BACNET_CHANNELS_MAX; + return Keylist_Count(Object_List); } /** @@ -153,11 +157,7 @@ unsigned Channel_Count(void) */ uint32_t Channel_Index_To_Instance(unsigned index) { - uint32_t instance = 1; - - instance += index; - - return instance; + return Keylist_Key(Object_List, index); } /** @@ -171,16 +171,7 @@ uint32_t Channel_Index_To_Instance(unsigned index) */ unsigned Channel_Instance_To_Index(uint32_t object_instance) { - unsigned index = BACNET_CHANNELS_MAX; - - if (object_instance) { - index = object_instance - 1; - if (index > BACNET_CHANNELS_MAX) { - index = BACNET_CHANNELS_MAX; - } - } - - return index; + return Keylist_Index(Object_List, object_instance); } /** @@ -191,12 +182,12 @@ unsigned Channel_Instance_To_Index(uint32_t object_instance) */ BACNET_CHANNEL_VALUE *Channel_Present_Value(uint32_t object_instance) { - unsigned index = 0; BACNET_CHANNEL_VALUE *cvalue = NULL; + struct object_data *pObject; - index = Channel_Instance_To_Index(object_instance); - if (index < BACNET_CHANNELS_MAX) { - cvalue = &Channel[index].Present_Value; + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + cvalue = &pObject->Present_Value; } return cvalue; @@ -211,12 +202,12 @@ BACNET_CHANNEL_VALUE *Channel_Present_Value(uint32_t object_instance) */ unsigned Channel_Last_Priority(uint32_t object_instance) { - unsigned index = 0; unsigned priority = 0; + struct object_data *pObject; - index = Channel_Instance_To_Index(object_instance); - if (index < BACNET_CHANNELS_MAX) { - priority = Channel[index].Last_Priority; + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + priority = pObject->Last_Priority; } return priority; @@ -231,12 +222,12 @@ unsigned Channel_Last_Priority(uint32_t object_instance) */ BACNET_WRITE_STATUS Channel_Write_Status(uint32_t object_instance) { - unsigned index = 0; BACNET_WRITE_STATUS write_status = BACNET_WRITE_STATUS_IDLE; + struct object_data *pObject; - index = Channel_Instance_To_Index(object_instance); - if (index < BACNET_CHANNELS_MAX) { - write_status = Channel[index].Write_Status; + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + write_status = pObject->Write_Status; } return write_status; @@ -251,12 +242,12 @@ BACNET_WRITE_STATUS Channel_Write_Status(uint32_t object_instance) */ uint16_t Channel_Number(uint32_t object_instance) { - unsigned index = 0; uint16_t value = 0; + struct object_data *pObject; - index = Channel_Instance_To_Index(object_instance); - if (index < BACNET_CHANNELS_MAX) { - value = Channel[index].Number; + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + value = pObject->Number; } return value; @@ -274,17 +265,44 @@ uint16_t Channel_Number(uint32_t object_instance) bool Channel_Number_Set(uint32_t object_instance, uint16_t value) { bool status = false; - unsigned index = 0; + struct object_data *pObject; - index = Channel_Instance_To_Index(object_instance); - if (index < BACNET_CHANNELS_MAX) { - Channel[index].Number = value; + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + pObject->Number = value; status = true; } return status; } +/** + * @brief Encode a BACnetARRAY property element + * @param object_instance [in] BACnet object instance number + * @param array_index [in] array index requested: + * 0 to N for individual array members + * @param apdu [out] Buffer in which the APDU contents are built, or NULL to + * return the length of buffer if it had been built + * @return The length of the apdu encoded or + * BACNET_STATUS_ERROR for ERROR_CODE_INVALID_ARRAY_INDEX + */ +static int Channel_Reference_List_Member_Element_Encode( + uint32_t object_instance, BACNET_ARRAY_INDEX array_index, uint8_t *apdu) +{ + int apdu_len = BACNET_STATUS_ERROR; + BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE *value; + unsigned count = 0; + + count = Channel_Reference_List_Member_Count(object_instance); + if (array_index < count) { + value = Channel_Reference_List_Member_Element( + object_instance, array_index + 1); + apdu_len = bacapp_encode_device_obj_property_ref(apdu, value); + } + + return apdu_len; +} + /** * For a given object instance-number, determines the member count * @@ -315,22 +333,8 @@ static bool Channel_Reference_List_Member_Valid( */ unsigned Channel_Reference_List_Member_Count(uint32_t object_instance) { - BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE *pMember = NULL; - unsigned count = 0; - unsigned m = 0; - unsigned index = 0; - - index = Channel_Instance_To_Index(object_instance); - if (index < BACNET_CHANNELS_MAX) { - for (m = 0; m < CHANNEL_MEMBERS_MAX; m++) { - pMember = &Channel[index].Members[m]; - if (Channel_Reference_List_Member_Valid(pMember)) { - count++; - } - } - } - - return count; + (void)object_instance; + return CHANNEL_MEMBERS_MAX; } /** @@ -345,24 +349,17 @@ BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE *Channel_Reference_List_Member_Element( uint32_t object_instance, unsigned array_index) { BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE *pMember = NULL; - unsigned count = 0; - unsigned m = 0; - unsigned index = 0; + struct object_data *pObject; - index = Channel_Instance_To_Index(object_instance); - if (index < BACNET_CHANNELS_MAX) { - for (m = 0; m < CHANNEL_MEMBERS_MAX; m++) { - pMember = &Channel[index].Members[m]; - if (Channel_Reference_List_Member_Valid(pMember)) { - count++; - if (count == array_index) { - return pMember; - } - } + pObject = Keylist_Data(Object_List, object_instance); + if (pObject && (array_index > 0)) { + array_index--; + if (array_index < CHANNEL_MEMBERS_MAX) { + pMember = &pObject->Members[array_index]; } } - return NULL; + return pMember; } /** @@ -378,24 +375,17 @@ bool Channel_Reference_List_Member_Element_Set(uint32_t object_instance, BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE *pMemberSrc) { BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE *pMember = NULL; - unsigned count = 0; - unsigned m = 0; - unsigned index = 0; bool status = false; + struct object_data *pObject; - index = Channel_Instance_To_Index(object_instance); - if (index < BACNET_CHANNELS_MAX) { - for (m = 0; m < CHANNEL_MEMBERS_MAX; m++) { - pMember = &Channel[index].Members[m]; - if (Channel_Reference_List_Member_Valid(pMember)) { - count++; - if (count == array_index) { - memcpy(pMember, pMemberSrc, - sizeof(BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE)); - status = true; - break; - } - } + pObject = Keylist_Data(Object_List, object_instance); + if (pObject && (array_index > 0)) { + array_index--; + if (array_index < CHANNEL_MEMBERS_MAX) { + pMember = &pObject->Members[array_index]; + memcpy(pMember, pMemberSrc, + sizeof(BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE)); + status = true; } } @@ -415,19 +405,17 @@ unsigned Channel_Reference_List_Member_Element_Add(uint32_t object_instance, BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE *pMemberSrc) { BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE *pMember = NULL; - unsigned count = 0; + unsigned array_index = 0; unsigned m = 0; - unsigned index = 0; + struct object_data *pObject; - index = Channel_Instance_To_Index(object_instance); - if (index < BACNET_CHANNELS_MAX) { + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { for (m = 0; m < CHANNEL_MEMBERS_MAX; m++) { - pMember = &Channel[index].Members[m]; - if (Channel_Reference_List_Member_Valid(pMember)) { - count++; - } else { + pMember = &pObject->Members[m]; + if (!Channel_Reference_List_Member_Valid(pMember)) { /* first empty slot */ - count++; + array_index = 1 + m; memcpy(pMember, pMemberSrc, sizeof(BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE)); break; @@ -435,37 +423,7 @@ unsigned Channel_Reference_List_Member_Element_Add(uint32_t object_instance, } } - return count; -} - -/** - * For a given object instance-number, adds a member element - * - * @param object_instance - object-instance number of the object - * @param type - object type - * @param instance - object instance number - * @param propertyIdentifier - property identifier BACNET_PROPERTY_ID - * @param array_index - 1-based array index of object property - * - * @return array_index - 1-based array index value for added element, or - * zero if not added - */ -unsigned Channel_Reference_List_Member_Local_Add(uint32_t object_instance, - BACNET_OBJECT_TYPE type, - uint32_t instance, - BACNET_PROPERTY_ID propertyIdentifier, - uint32_t arrayIndex) -{ - BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE member = { 0 }; - - member.objectIdentifier.type = type; - member.objectIdentifier.instance = instance; - member.propertyIdentifier = propertyIdentifier; - member.arrayIndex = arrayIndex; - member.deviceIdentifier.type = OBJECT_DEVICE; - member.deviceIdentifier.instance = Device_Object_Instance_Number(); - - return Channel_Reference_List_Member_Element_Add(object_instance, &member); + return array_index; } /** @@ -474,19 +432,20 @@ unsigned Channel_Reference_List_Member_Local_Add(uint32_t object_instance, * @param object_instance - object-instance number of the object * @param array_index - 1-based array index * - * @return group number in the array + * @return group number in the array, or 0 if invalid */ uint16_t Channel_Control_Groups_Element( uint32_t object_instance, int32_t array_index) { - unsigned index = 0; uint16_t value = 0; + struct object_data *pObject; - index = Channel_Instance_To_Index(object_instance); - if ((index < BACNET_CHANNELS_MAX) && (array_index > 0) && - (array_index <= CONTROL_GROUPS_MAX)) { - array_index--; - value = Channel[index].Control_Groups[array_index]; + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + if ((array_index > 0) && (array_index <= CONTROL_GROUPS_MAX)) { + array_index--; + value = pObject->Control_Groups[array_index]; + } } return value; @@ -505,19 +464,47 @@ bool Channel_Control_Groups_Element_Set( uint32_t object_instance, int32_t array_index, uint16_t value) { bool status = false; - unsigned index = 0; + struct object_data *pObject; - index = Channel_Instance_To_Index(object_instance); - if ((index < BACNET_CHANNELS_MAX) && (array_index > 0) && - (array_index <= CONTROL_GROUPS_MAX)) { - array_index--; - Channel[index].Control_Groups[array_index] = value; - status = true; + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + if ((array_index > 0) && (array_index <= CONTROL_GROUPS_MAX)) { + array_index--; + pObject->Control_Groups[array_index] = value; + status = true; + } } return status; } +/** + * @brief Encode a BACnetARRAY property element + * @param object_instance [in] BACnet network port object instance number + * @param array_index [in] array index requested: + * 0 to N for individual array members + * @param apdu [out] Buffer in which the APDU contents are built, or NULL to + * return the length of buffer if it had been built + * @return The length of the apdu encoded or + * BACNET_STATUS_ERROR for ERROR_CODE_INVALID_ARRAY_INDEX + */ +static int Channel_Control_Groups_Element_Encode( + uint32_t object_instance, BACNET_ARRAY_INDEX array_index, uint8_t *apdu) +{ + int apdu_len = BACNET_STATUS_ERROR; + uint16_t value = 1; + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject && (array_index < CONTROL_GROUPS_MAX)) { + value = + Channel_Control_Groups_Element(object_instance, array_index + 1); + apdu_len = encode_application_unsigned(apdu, value); + } + + return apdu_len; +} + /** * For a given application value, copy to the channel value * @@ -610,7 +597,7 @@ bool Channel_Value_Copy( case BACNET_APPLICATION_TAG_DATE: cvalue->tag = value->tag; datetime_date_copy(&cvalue->type.Date, &value->type.Date); - apdu_len = encode_application_date(&apdu[0], &value->type.Date); + apdu_len = encode_application_date(apdu, &value->type.Date); status = true; break; #endif @@ -635,6 +622,21 @@ bool Channel_Value_Copy( &cvalue->type.Lighting_Command, &value->type.Lighting_Command); status = true; break; +#endif +#if defined(BACAPP_TYPES_EXTRA) && defined(CHANNEL_COLOR_COMMAND) + case BACNET_APPLICATION_TAG_COLOR_COMMAND: + cvalue->tag = value->tag; + color_command_copy( + &cvalue->type.Color_Command, &value->type.Color_Command); + status = true; + break; +#endif +#if defined(BACAPP_TYPES_EXTRA) && defined(CHANNEL_XY_COLOR) + case BACNET_APPLICATION_TAG_XY_COLOR: + cvalue->tag = value->tag; + xy_color_copy(&cvalue->type.XY_Color, &value->type.XY_Color); + status = true; + break; #endif default: break; @@ -646,7 +648,7 @@ bool Channel_Value_Copy( /** * For a given application value, copy to the channel value * - * @param apdu - APDU buffer for storing the encoded data + * @param apdu - APDU buffer for storing the encoded data, or NULL for length * @param apdu_max - size of APDU buffer available for storing data * @param value - BACNET_CHANNEL_VALUE value * @@ -658,86 +660,94 @@ int Channel_Value_Encode( int apdu_len = BACNET_STATUS_ERROR; (void)apdu_max; - if (!apdu || !value) { + if (!value) { return BACNET_STATUS_ERROR; } switch (value->tag) { case BACNET_APPLICATION_TAG_NULL: - apdu_len = encode_application_null(&apdu[0]); + apdu_len = encode_application_null(apdu); break; #if defined(CHANNEL_BOOLEAN) case BACNET_APPLICATION_TAG_BOOLEAN: - apdu_len = - encode_application_boolean(&apdu[0], value->type.Boolean); + apdu_len = encode_application_boolean(apdu, value->type.Boolean); break; #endif #if defined(CHANNEL_UNSIGNED) case BACNET_APPLICATION_TAG_UNSIGNED_INT: apdu_len = - encode_application_unsigned(&apdu[0], value->type.Unsigned_Int); + encode_application_unsigned(apdu, value->type.Unsigned_Int); break; #endif #if defined(CHANNEL_SIGNED) case BACNET_APPLICATION_TAG_SIGNED_INT: - apdu_len = - encode_application_signed(&apdu[0], value->type.Signed_Int); + apdu_len = encode_application_signed(apdu, value->type.Signed_Int); break; #endif #if defined(CHANNEL_REAL) case BACNET_APPLICATION_TAG_REAL: - apdu_len = encode_application_real(&apdu[0], value->type.Real); + apdu_len = encode_application_real(apdu, value->type.Real); break; #endif #if defined(CHANNEL_DOUBLE) case BACNET_APPLICATION_TAG_DOUBLE: - apdu_len = encode_application_double(&apdu[0], value->type.Double); + apdu_len = encode_application_double(apdu, value->type.Double); break; #endif #if defined(CHANNEL_OCTET_STRING) case BACNET_APPLICATION_TAG_OCTET_STRING: apdu_len = encode_application_octet_string( - &apdu[0], &value->type.Octet_String); + apdu, &value->type.Octet_String); break; #endif #if defined(CHANNEL_CHARACTER_STRING) case BACNET_APPLICATION_TAG_CHARACTER_STRING: apdu_len = encode_application_character_string( - &apdu[0], &value->type.Character_String); + apdu, &value->type.Character_String); break; #endif #if defined(CHANNEL_BIT_STRING) case BACNET_APPLICATION_TAG_BIT_STRING: apdu_len = - encode_application_bitstring(&apdu[0], &value->type.Bit_String); + encode_application_bitstring(apdu, &value->type.Bit_String); break; #endif #if defined(CHANNEL_ENUMERATED) case BACNET_APPLICATION_TAG_ENUMERATED: apdu_len = - encode_application_enumerated(&apdu[0], value->type.Enumerated); + encode_application_enumerated(apdu, value->type.Enumerated); break; #endif #if defined(CHANNEL_DATE) case BACNET_APPLICATION_TAG_DATE: - apdu_len = encode_application_date(&apdu[0], &value->type.Date); + apdu_len = encode_application_date(apdu, &value->type.Date); break; #endif #if defined(CHANNEL_TIME) case BACNET_APPLICATION_TAG_TIME: - apdu_len = encode_application_time(&apdu[0], &value->type.Time); + apdu_len = encode_application_time(apdu, &value->type.Time); break; #endif #if defined(CHANNEL_OBJECT_ID) case BACNET_APPLICATION_TAG_OBJECT_ID: - apdu_len = encode_application_object_id(&apdu[0], + apdu_len = encode_application_object_id(apdu, (int)value->type.Object_Id.type, value->type.Object_Id.instance); break; #endif #if defined(CHANNEL_LIGHTING_COMMAND) case BACNET_APPLICATION_TAG_LIGHTING_COMMAND: - apdu_len = lighting_command_encode( - &apdu[0], &value->type.Lighting_Command); + apdu_len = + lighting_command_encode(apdu, &value->type.Lighting_Command); + break; +#endif +#if defined(CHANNEL_COLOR_COMMAND) + case BACNET_APPLICATION_TAG_COLOR_COMMAND: + apdu_len = color_command_encode(apdu, &value->type.Color_Command); + break; +#endif +#if defined(CHANNEL_XY_COLOR) + case BACNET_APPLICATION_TAG_XY_COLOR: + apdu_len = xy_color_encode(apdu, &value->type.XY_Color); break; #endif default: @@ -750,15 +760,13 @@ int Channel_Value_Encode( /** * For a given application value, coerce the encoding, if necessary * - * @param apdu - buffer to hold the encoding - * @param apdu_max - max size of the buffer to hold the encoding + * @param apdu - buffer to hold the encoding, or NULL for length * @param value - BACNET_APPLICATION_DATA_VALUE value * @param tag - application tag to be coerced, if possible * * @return number of bytes in the APDU, or BACNET_STATUS_ERROR if error. */ -int Channel_Coerce_Data_Encode(uint8_t *apdu, - unsigned max_apdu, +static int Coerce_Data_Encode(uint8_t *apdu, BACNET_APPLICATION_DATA_VALUE *value, BACNET_APPLICATION_TAG tag) { @@ -769,16 +777,18 @@ int Channel_Coerce_Data_Encode(uint8_t *apdu, int32_t signed_value = 0; bool boolean_value = false; - (void)max_apdu; - if (apdu && value) { + if (value) { switch (value->tag) { #if defined(BACAPP_NULL) case BACNET_APPLICATION_TAG_NULL: - if (tag == BACNET_APPLICATION_TAG_LIGHTING_COMMAND) { + if ((tag == BACNET_APPLICATION_TAG_LIGHTING_COMMAND) || + (tag == BACNET_APPLICATION_TAG_COLOR_COMMAND)) { apdu_len = BACNET_STATUS_ERROR; } else { /* no coercion */ - apdu[0] = value->tag; + if (apdu) { + *apdu = value->tag; + } apdu_len++; } break; @@ -786,37 +796,35 @@ int Channel_Coerce_Data_Encode(uint8_t *apdu, #if defined(BACAPP_BOOLEAN) case BACNET_APPLICATION_TAG_BOOLEAN: if (tag == BACNET_APPLICATION_TAG_BOOLEAN) { - apdu_len = encode_application_boolean( - &apdu[0], value->type.Boolean); + apdu_len = + encode_application_boolean(apdu, value->type.Boolean); } else if (tag == BACNET_APPLICATION_TAG_UNSIGNED_INT) { if (value->type.Boolean) { unsigned_value = 1; } apdu_len = - encode_application_unsigned(&apdu[0], unsigned_value); + encode_application_unsigned(apdu, unsigned_value); } else if (tag == BACNET_APPLICATION_TAG_SIGNED_INT) { if (value->type.Boolean) { signed_value = 1; } - apdu_len = - encode_application_signed(&apdu[0], signed_value); + apdu_len = encode_application_signed(apdu, signed_value); } else if (tag == BACNET_APPLICATION_TAG_REAL) { if (value->type.Boolean) { float_value = 1; } - apdu_len = encode_application_real(&apdu[0], float_value); + apdu_len = encode_application_real(apdu, float_value); } else if (tag == BACNET_APPLICATION_TAG_DOUBLE) { if (value->type.Boolean) { double_value = 1; } - apdu_len = - encode_application_double(&apdu[0], double_value); + apdu_len = encode_application_double(apdu, double_value); } else if (tag == BACNET_APPLICATION_TAG_ENUMERATED) { if (value->type.Boolean) { unsigned_value = 1; } apdu_len = - encode_application_enumerated(&apdu[0], unsigned_value); + encode_application_enumerated(apdu, unsigned_value); } else { apdu_len = BACNET_STATUS_ERROR; } @@ -828,36 +836,33 @@ int Channel_Coerce_Data_Encode(uint8_t *apdu, if (value->type.Unsigned_Int) { boolean_value = true; } - apdu_len = - encode_application_boolean(&apdu[0], boolean_value); + apdu_len = encode_application_boolean(apdu, boolean_value); } else if (tag == BACNET_APPLICATION_TAG_UNSIGNED_INT) { unsigned_value = value->type.Unsigned_Int; apdu_len = - encode_application_unsigned(&apdu[0], unsigned_value); + encode_application_unsigned(apdu, unsigned_value); } else if (tag == BACNET_APPLICATION_TAG_SIGNED_INT) { if (value->type.Unsigned_Int <= 2147483647) { signed_value = value->type.Unsigned_Int; apdu_len = - encode_application_signed(&apdu[0], signed_value); + encode_application_signed(apdu, signed_value); } else { apdu_len = BACNET_STATUS_ERROR; } } else if (tag == BACNET_APPLICATION_TAG_REAL) { if (value->type.Unsigned_Int <= 9999999) { float_value = (float)value->type.Unsigned_Int; - apdu_len = - encode_application_real(&apdu[0], float_value); + apdu_len = encode_application_real(apdu, float_value); } else { apdu_len = BACNET_STATUS_ERROR; } } else if (tag == BACNET_APPLICATION_TAG_DOUBLE) { double_value = (double)value->type.Unsigned_Int; - apdu_len = - encode_application_double(&apdu[0], double_value); + apdu_len = encode_application_double(apdu, double_value); } else if (tag == BACNET_APPLICATION_TAG_ENUMERATED) { unsigned_value = value->type.Unsigned_Int; apdu_len = - encode_application_enumerated(&apdu[0], unsigned_value); + encode_application_enumerated(apdu, unsigned_value); } else { apdu_len = BACNET_STATUS_ERROR; } @@ -869,37 +874,33 @@ int Channel_Coerce_Data_Encode(uint8_t *apdu, if (value->type.Signed_Int) { boolean_value = true; } - apdu_len = - encode_application_boolean(&apdu[0], boolean_value); + apdu_len = encode_application_boolean(apdu, boolean_value); } else if (tag == BACNET_APPLICATION_TAG_UNSIGNED_INT) { if ((value->type.Signed_Int >= 0) && (value->type.Signed_Int <= 2147483647)) { unsigned_value = value->type.Signed_Int; - apdu_len = encode_application_unsigned( - &apdu[0], unsigned_value); + apdu_len = + encode_application_unsigned(apdu, unsigned_value); } else { apdu_len = BACNET_STATUS_ERROR; } } else if (tag == BACNET_APPLICATION_TAG_SIGNED_INT) { signed_value = value->type.Signed_Int; - apdu_len = - encode_application_signed(&apdu[0], signed_value); + apdu_len = encode_application_signed(apdu, signed_value); } else if (tag == BACNET_APPLICATION_TAG_REAL) { if (value->type.Signed_Int <= 9999999) { float_value = (float)value->type.Signed_Int; - apdu_len = - encode_application_real(&apdu[0], float_value); + apdu_len = encode_application_real(apdu, float_value); } else { apdu_len = BACNET_STATUS_ERROR; } } else if (tag == BACNET_APPLICATION_TAG_DOUBLE) { double_value = (double)value->type.Signed_Int; - apdu_len = - encode_application_double(&apdu[0], double_value); + apdu_len = encode_application_double(apdu, double_value); } else if (tag == BACNET_APPLICATION_TAG_ENUMERATED) { unsigned_value = value->type.Signed_Int; apdu_len = - encode_application_enumerated(&apdu[0], unsigned_value); + encode_application_enumerated(apdu, unsigned_value); } else { apdu_len = BACNET_STATUS_ERROR; } @@ -911,14 +912,13 @@ int Channel_Coerce_Data_Encode(uint8_t *apdu, if (islessgreater(value->type.Real, 0.0F)) { boolean_value = true; } - apdu_len = - encode_application_boolean(&apdu[0], boolean_value); + apdu_len = encode_application_boolean(apdu, boolean_value); } else if (tag == BACNET_APPLICATION_TAG_UNSIGNED_INT) { if ((value->type.Real >= 0.0F) && (value->type.Real <= 2147483000.0F)) { unsigned_value = (uint32_t)value->type.Real; - apdu_len = encode_application_unsigned( - &apdu[0], unsigned_value); + apdu_len = + encode_application_unsigned(apdu, unsigned_value); } else { apdu_len = BACNET_STATUS_ERROR; } @@ -927,23 +927,22 @@ int Channel_Coerce_Data_Encode(uint8_t *apdu, (value->type.Real <= 214783000.0F)) { signed_value = (int32_t)value->type.Real; apdu_len = - encode_application_signed(&apdu[0], signed_value); + encode_application_signed(apdu, signed_value); } else { apdu_len = BACNET_STATUS_ERROR; } } else if (tag == BACNET_APPLICATION_TAG_REAL) { float_value = value->type.Real; - apdu_len = encode_application_real(&apdu[0], float_value); + apdu_len = encode_application_real(apdu, float_value); } else if (tag == BACNET_APPLICATION_TAG_DOUBLE) { double_value = value->type.Real; - apdu_len = - encode_application_double(&apdu[0], double_value); + apdu_len = encode_application_double(apdu, double_value); } else if (tag == BACNET_APPLICATION_TAG_ENUMERATED) { if ((value->type.Real >= 0.0F) && (value->type.Real <= 2147483000.0F)) { unsigned_value = (uint32_t)value->type.Real; - apdu_len = encode_application_enumerated( - &apdu[0], unsigned_value); + apdu_len = + encode_application_enumerated(apdu, unsigned_value); } else { apdu_len = BACNET_STATUS_ERROR; } @@ -958,14 +957,13 @@ int Channel_Coerce_Data_Encode(uint8_t *apdu, if (islessgreater(value->type.Double, 0.0)) { boolean_value = true; } - apdu_len = - encode_application_boolean(&apdu[0], boolean_value); + apdu_len = encode_application_boolean(apdu, boolean_value); } else if (tag == BACNET_APPLICATION_TAG_UNSIGNED_INT) { if ((value->type.Double >= 0.0) && (value->type.Double <= 2147483000.0)) { unsigned_value = (uint32_t)value->type.Double; - apdu_len = encode_application_unsigned( - &apdu[0], unsigned_value); + apdu_len = + encode_application_unsigned(apdu, unsigned_value); } else { apdu_len = BACNET_STATUS_ERROR; } @@ -974,7 +972,7 @@ int Channel_Coerce_Data_Encode(uint8_t *apdu, (value->type.Double <= 214783000.0)) { signed_value = (int32_t)value->type.Double; apdu_len = - encode_application_signed(&apdu[0], signed_value); + encode_application_signed(apdu, signed_value); } else { apdu_len = BACNET_STATUS_ERROR; } @@ -982,21 +980,19 @@ int Channel_Coerce_Data_Encode(uint8_t *apdu, if ((value->type.Double >= 3.4E-38) && (value->type.Double <= 3.4E+38)) { float_value = (float)value->type.Double; - apdu_len = - encode_application_real(&apdu[0], float_value); + apdu_len = encode_application_real(apdu, float_value); } else { apdu_len = BACNET_STATUS_ERROR; } } else if (tag == BACNET_APPLICATION_TAG_DOUBLE) { double_value = value->type.Double; - apdu_len = - encode_application_double(&apdu[0], double_value); + apdu_len = encode_application_double(apdu, double_value); } else if (tag == BACNET_APPLICATION_TAG_ENUMERATED) { if ((value->type.Double >= 0.0) && (value->type.Double <= 2147483000.0)) { unsigned_value = (uint32_t)value->type.Double; - apdu_len = encode_application_enumerated( - &apdu[0], unsigned_value); + apdu_len = + encode_application_enumerated(apdu, unsigned_value); } else { apdu_len = BACNET_STATUS_ERROR; } @@ -1011,36 +1007,33 @@ int Channel_Coerce_Data_Encode(uint8_t *apdu, if (value->type.Enumerated) { boolean_value = true; } - apdu_len = - encode_application_boolean(&apdu[0], boolean_value); + apdu_len = encode_application_boolean(apdu, boolean_value); } else if (tag == BACNET_APPLICATION_TAG_UNSIGNED_INT) { unsigned_value = value->type.Enumerated; apdu_len = - encode_application_unsigned(&apdu[0], unsigned_value); + encode_application_unsigned(apdu, unsigned_value); } else if (tag == BACNET_APPLICATION_TAG_SIGNED_INT) { if (value->type.Enumerated <= 2147483647) { signed_value = value->type.Enumerated; apdu_len = - encode_application_signed(&apdu[0], signed_value); + encode_application_signed(apdu, signed_value); } else { apdu_len = BACNET_STATUS_ERROR; } } else if (tag == BACNET_APPLICATION_TAG_REAL) { if (value->type.Enumerated <= 9999999) { float_value = (float)value->type.Enumerated; - apdu_len = - encode_application_real(&apdu[0], float_value); + apdu_len = encode_application_real(apdu, float_value); } else { apdu_len = BACNET_STATUS_ERROR; } } else if (tag == BACNET_APPLICATION_TAG_DOUBLE) { double_value = (double)value->type.Enumerated; - apdu_len = - encode_application_double(&apdu[0], double_value); + apdu_len = encode_application_double(apdu, double_value); } else if (tag == BACNET_APPLICATION_TAG_ENUMERATED) { unsigned_value = value->type.Enumerated; apdu_len = - encode_application_enumerated(&apdu[0], unsigned_value); + encode_application_enumerated(apdu, unsigned_value); } else { apdu_len = BACNET_STATUS_ERROR; } @@ -1050,7 +1043,22 @@ int Channel_Coerce_Data_Encode(uint8_t *apdu, case BACNET_APPLICATION_TAG_LIGHTING_COMMAND: if (tag == BACNET_APPLICATION_TAG_LIGHTING_COMMAND) { apdu_len = lighting_command_encode( - &apdu[0], &value->type.Lighting_Command); + apdu, &value->type.Lighting_Command); + } else { + apdu_len = BACNET_STATUS_ERROR; + } + break; + case BACNET_APPLICATION_TAG_COLOR_COMMAND: + if (tag == BACNET_APPLICATION_TAG_COLOR_COMMAND) { + apdu_len = + color_command_encode(apdu, &value->type.Color_Command); + } else { + apdu_len = BACNET_STATUS_ERROR; + } + break; + case BACNET_APPLICATION_TAG_XY_COLOR: + if (tag == BACNET_APPLICATION_TAG_XY_COLOR) { + apdu_len = xy_color_encode(apdu, &value->type.XY_Color); } else { apdu_len = BACNET_STATUS_ERROR; } @@ -1065,6 +1073,32 @@ int Channel_Coerce_Data_Encode(uint8_t *apdu, return apdu_len; } +/** + * For a given application value, coerce the encoding, if necessary + * + * @param apdu - buffer to hold the encoding, or null for length + * @param value - BACNET_APPLICATION_DATA_VALUE value + * @param tag - application tag to be coerced, if possible + * + * @return number of bytes in the APDU, or BACNET_STATUS_ERROR if error. + */ +int Channel_Coerce_Data_Encode(uint8_t *apdu, + size_t apdu_size, + BACNET_APPLICATION_DATA_VALUE *value, + BACNET_APPLICATION_TAG tag) +{ + int len; + + len = Coerce_Data_Encode(NULL, value, tag); + if ((len > 0) && (len <= apdu_size)) { + len = Coerce_Data_Encode(apdu, value, tag); + } else { + len = BACNET_STATUS_ERROR; + } + + return len; +} + /** * For a given object instance-number, sets the present-value at a given * priority 1..16. @@ -1136,6 +1170,34 @@ bool Channel_Write_Member_Value( status = true; } } + } else if (wp_data->object_type == OBJECT_COLOR) { + if ((wp_data->object_property == PROP_PRESENT_VALUE) && + (wp_data->array_index == BACNET_ARRAY_ALL)) { + apdu_len = Channel_Coerce_Data_Encode(wp_data->application_data, + wp_data->application_data_len, value, + BACNET_APPLICATION_TAG_XY_COLOR); + if (apdu_len != BACNET_STATUS_ERROR) { + wp_data->application_data_len = apdu_len; + status = true; + } + } else if ((wp_data->object_property == PROP_COLOR_COMMAND) && + (wp_data->array_index == BACNET_ARRAY_ALL)) { + apdu_len = Channel_Coerce_Data_Encode(wp_data->application_data, + wp_data->application_data_len, value, + BACNET_APPLICATION_TAG_COLOR_COMMAND); + if (apdu_len != BACNET_STATUS_ERROR) { + wp_data->application_data_len = apdu_len; + status = true; + } + } + } else if (wp_data->object_type == OBJECT_COLOR_TEMPERATURE) { + apdu_len = Channel_Coerce_Data_Encode(wp_data->application_data, + wp_data->application_data_len, value, + BACNET_APPLICATION_TAG_UNSIGNED_INT); + if (apdu_len != BACNET_STATUS_ERROR) { + wp_data->application_data_len = apdu_len; + status = true; + } } } @@ -1146,11 +1208,13 @@ bool Channel_Write_Member_Value( * For a given object instance-number, sets the present-value at a given * priority 1..16. * - * @param wp_data - all of the WriteProperty data structure + * @param pObject - object instance data + * @param value - application value + * @param priority - BACnet priority 0=none,1..16 * * @return true if values are within range and present-value is sent. */ -static bool Channel_Write_Members(struct bacnet_channel_object *pChannel, +static bool Channel_Write_Members(struct object_data *pObject, BACNET_APPLICATION_DATA_VALUE *value, uint8_t priority) { @@ -1159,10 +1223,10 @@ static bool Channel_Write_Members(struct bacnet_channel_object *pChannel, unsigned m = 0; BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE *pMember = NULL; - if (pChannel && value) { - pChannel->Write_Status = BACNET_WRITE_STATUS_IN_PROGRESS; + if (pObject && value) { + pObject->Write_Status = BACNET_WRITE_STATUS_IN_PROGRESS; for (m = 0; m < CHANNEL_MEMBERS_MAX; m++) { - pMember = &pChannel->Members[m]; + pMember = &pObject->Members[m]; /* NOTE: our implementation is for internal objects only */ /* NOTE: we could check to match our Device ID, but then we would need to update all channels when our device ID @@ -1179,14 +1243,16 @@ static bool Channel_Write_Members(struct bacnet_channel_object *pChannel, wp_data.application_data_len = sizeof(wp_data.application_data); status = Channel_Write_Member_Value(&wp_data, value); if (status) { - status = Device_Write_Property(&wp_data); + if (Write_Property_Internal_Callback) { + status = Write_Property_Internal_Callback(&wp_data); + } } else { - pChannel->Write_Status = BACNET_WRITE_STATUS_FAILED; + pObject->Write_Status = BACNET_WRITE_STATUS_FAILED; } } } - if (pChannel->Write_Status == BACNET_WRITE_STATUS_IN_PROGRESS) { - pChannel->Write_Status = BACNET_WRITE_STATUS_SUCCESSFUL; + if (pObject->Write_Status == BACNET_WRITE_STATUS_IN_PROGRESS) { + pObject->Write_Status = BACNET_WRITE_STATUS_SUCCESSFUL; } } @@ -1197,26 +1263,25 @@ static bool Channel_Write_Members(struct bacnet_channel_object *pChannel, * For a given object instance-number, sets the present-value at a given * priority 1..16. * - * @param wp_data - all of the WriteProperty data structure - * - * @return true if values are within range and present-value is sent. + * @param wp_data - all of the WriteProperty data structure + * @param value - application value + * @return true if values are within range and present-value is sent. */ bool Channel_Present_Value_Set( BACNET_WRITE_PROPERTY_DATA *wp_data, BACNET_APPLICATION_DATA_VALUE *value) { - unsigned index = 0; bool status = false; + struct object_data *pObject; - index = Channel_Instance_To_Index(wp_data->object_instance); - if (index < BACNET_CHANNELS_MAX) { + pObject = Keylist_Data(Object_List, wp_data->object_instance); + if (pObject) { if ((wp_data->priority > 0) && (wp_data->priority <= BACNET_MAX_PRIORITY)) { if (wp_data->priority != 6 /* reserved */) { - status = - Channel_Value_Copy(&Channel[index].Present_Value, value); + status = Channel_Value_Copy(&pObject->Present_Value, value); (void)status; - status = Channel_Write_Members( - &Channel[index], value, wp_data->priority); + status = + Channel_Write_Members(pObject, value, wp_data->priority); (void)status; status = true; } else { @@ -1248,14 +1313,42 @@ bool Channel_Present_Value_Set( bool Channel_Object_Name( uint32_t object_instance, BACNET_CHARACTER_STRING *object_name) { - char text_string[32] = ""; bool status = false; - unsigned index = 0; + char name_text[24] = "CHANNEL-4194303"; + struct object_data *pObject; - index = Channel_Instance_To_Index(object_instance); - if (index < BACNET_CHANNELS_MAX) { - sprintf(text_string, "CHANNEL %lu", (unsigned long)object_instance); - status = characterstring_init_ansi(object_name, text_string); + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + if (pObject->Object_Name) { + status = + characterstring_init_ansi(object_name, pObject->Object_Name); + } else { + snprintf(name_text, sizeof(name_text), "CHANNEL-%lu", + (unsigned long)object_instance); + status = characterstring_init_ansi(object_name, name_text); + } + } + + return status; +} + +/** + * For a given object instance-number, sets the object-name + * + * @param object_instance - object-instance number of the object + * @param new_name - holds the object-name to be set + * + * @return true if object-name was set + */ +bool Channel_Name_Set(uint32_t object_instance, char *new_name) +{ + bool status = false; /* return value */ + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject && new_name) { + status = true; + pObject->Object_Name = new_name; } return status; @@ -1269,14 +1362,14 @@ bool Channel_Object_Name( * * @return out-of-service property value */ -bool Channel_Out_Of_Service(uint32_t instance) +bool Channel_Out_Of_Service(uint32_t object_instance) { - unsigned int index = 0; bool value = false; + struct object_data *pObject; - index = Channel_Instance_To_Index(instance); - if (index < BACNET_CHANNELS_MAX) { - value = Channel[index].Out_Of_Service; + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + value = pObject->Out_Of_Service; } return value; @@ -1290,13 +1383,13 @@ bool Channel_Out_Of_Service(uint32_t instance) * * @return true if the out-of-service property value was set */ -void Channel_Out_Of_Service_Set(uint32_t instance, bool value) +void Channel_Out_Of_Service_Set(uint32_t object_instance, bool value) { - unsigned int index = 0; + struct object_data *pObject; - index = Channel_Instance_To_Index(instance); - if (index < BACNET_CHANNELS_MAX) { - Channel[index].Out_Of_Service = value; + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + pObject->Out_Of_Service = value; } } @@ -1312,16 +1405,14 @@ void Channel_Out_Of_Service_Set(uint32_t instance, bool value) */ int Channel_Read_Property(BACNET_READ_PROPERTY_DATA *rpdata) { - int len = 0; int apdu_len = 0; /* return value */ BACNET_BIT_STRING bit_string; BACNET_CHARACTER_STRING char_string; BACNET_CHANNEL_VALUE *cvalue = NULL; uint32_t unsigned_value = 0; - unsigned i = 0; unsigned count = 0; bool state = false; - BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE *pMember = NULL; + int apdu_size = 0; uint8_t *apdu = NULL; if ((rpdata == NULL) || (rpdata->application_data == NULL) || @@ -1329,34 +1420,34 @@ int Channel_Read_Property(BACNET_READ_PROPERTY_DATA *rpdata) return 0; } apdu = rpdata->application_data; + apdu_size = rpdata->application_data_len; switch (rpdata->object_property) { case PROP_OBJECT_IDENTIFIER: apdu_len = encode_application_object_id( - &apdu[0], OBJECT_CHANNEL, rpdata->object_instance); + apdu, OBJECT_CHANNEL, rpdata->object_instance); break; case PROP_OBJECT_NAME: Channel_Object_Name(rpdata->object_instance, &char_string); - apdu_len = - encode_application_character_string(&apdu[0], &char_string); + apdu_len = encode_application_character_string(apdu, &char_string); break; case PROP_OBJECT_TYPE: - apdu_len = encode_application_enumerated(&apdu[0], OBJECT_CHANNEL); + apdu_len = encode_application_enumerated(apdu, OBJECT_CHANNEL); break; case PROP_PRESENT_VALUE: cvalue = Channel_Present_Value(rpdata->object_instance); - apdu_len = Channel_Value_Encode(&apdu[0], MAX_APDU, cvalue); + apdu_len = Channel_Value_Encode(apdu, MAX_APDU, cvalue); if (apdu_len == BACNET_STATUS_ERROR) { - apdu_len = encode_application_null(&apdu[0]); + apdu_len = encode_application_null(apdu); } break; case PROP_LAST_PRIORITY: unsigned_value = Channel_Last_Priority(rpdata->object_instance); - apdu_len = encode_application_unsigned(&apdu[0], unsigned_value); + apdu_len = encode_application_unsigned(apdu, unsigned_value); break; case PROP_WRITE_STATUS: unsigned_value = (BACNET_WRITE_STATUS)Channel_Write_Status( rpdata->object_instance); - apdu_len = encode_application_enumerated(&apdu[0], unsigned_value); + apdu_len = encode_application_enumerated(apdu, unsigned_value); break; case PROP_STATUS_FLAGS: bitstring_init(&bit_string); @@ -1365,95 +1456,41 @@ int Channel_Read_Property(BACNET_READ_PROPERTY_DATA *rpdata) bitstring_set_bit(&bit_string, STATUS_FLAG_OVERRIDDEN, false); state = Channel_Out_Of_Service(rpdata->object_instance); bitstring_set_bit(&bit_string, STATUS_FLAG_OUT_OF_SERVICE, state); - apdu_len = encode_application_bitstring(&apdu[0], &bit_string); + apdu_len = encode_application_bitstring(apdu, &bit_string); break; case PROP_OUT_OF_SERVICE: state = Channel_Out_Of_Service(rpdata->object_instance); - apdu_len = encode_application_boolean(&apdu[0], state); + apdu_len = encode_application_boolean(apdu, state); break; case PROP_LIST_OF_OBJECT_PROPERTY_REFERENCES: - if (rpdata->array_index == 0) { - /* Array element zero is the number of elements in the array */ - count = Channel_Reference_List_Member_Count( - rpdata->object_instance); - apdu_len = encode_application_unsigned(&apdu[0], count); - } else if (rpdata->array_index == BACNET_ARRAY_ALL) { - /* if no index was specified, then try to encode the entire list - */ - /* into one packet. */ - count = Channel_Reference_List_Member_Count( - rpdata->object_instance); - for (i = 1; i <= count; i++) { - pMember = Channel_Reference_List_Member_Element( - rpdata->object_instance, i); - len = bacapp_encode_device_obj_property_ref( - &apdu[apdu_len], pMember); - /* add it if we have room */ - if ((apdu_len + len) < MAX_APDU) { - apdu_len += len; - } else { - rpdata->error_code = - ERROR_CODE_ABORT_SEGMENTATION_NOT_SUPPORTED; - apdu_len = BACNET_STATUS_ABORT; - break; - } - } - } else { - /* a specific element was requested */ - count = Channel_Reference_List_Member_Count( - rpdata->object_instance); - if (rpdata->array_index <= count) { - pMember = Channel_Reference_List_Member_Element( - rpdata->object_instance, rpdata->array_index); - apdu_len += bacapp_encode_device_obj_property_ref( - &apdu[0], pMember); - } else { - rpdata->error_class = ERROR_CLASS_PROPERTY; - rpdata->error_code = ERROR_CODE_INVALID_ARRAY_INDEX; - apdu_len = BACNET_STATUS_ERROR; - } + count = + Channel_Reference_List_Member_Count(rpdata->object_instance); + apdu_len = bacnet_array_encode(rpdata->object_instance, + rpdata->array_index, + Channel_Reference_List_Member_Element_Encode, count, apdu, + apdu_size); + if (apdu_len == BACNET_STATUS_ABORT) { + rpdata->error_code = + ERROR_CODE_ABORT_SEGMENTATION_NOT_SUPPORTED; + } else if (apdu_len == BACNET_STATUS_ERROR) { + rpdata->error_class = ERROR_CLASS_PROPERTY; + rpdata->error_code = ERROR_CODE_INVALID_ARRAY_INDEX; } break; case PROP_CHANNEL_NUMBER: unsigned_value = Channel_Number(rpdata->object_instance); - apdu_len = encode_application_unsigned(&apdu[0], unsigned_value); + apdu_len = encode_application_unsigned(apdu, unsigned_value); break; case PROP_CONTROL_GROUPS: - if (rpdata->array_index == 0) { - /* Array element zero is the number of elements in the array */ - apdu_len = - encode_application_unsigned(&apdu[0], CONTROL_GROUPS_MAX); - } else if (rpdata->array_index == BACNET_ARRAY_ALL) { - /* if no index was specified, then try to encode the entire list - */ - /* into one packet. */ - for (i = 1; i <= CONTROL_GROUPS_MAX; i++) { - unsigned_value = Channel_Control_Groups_Element( - rpdata->object_instance, i); - len = encode_application_unsigned( - &apdu[apdu_len], unsigned_value); - /* add it if we have room */ - if ((apdu_len + len) < MAX_APDU) { - apdu_len += len; - } else { - rpdata->error_code = - ERROR_CODE_ABORT_SEGMENTATION_NOT_SUPPORTED; - apdu_len = BACNET_STATUS_ABORT; - break; - } - } - } else { - /* a specific element was requested */ - if (rpdata->array_index <= CONTROL_GROUPS_MAX) { - unsigned_value = Channel_Control_Groups_Element( - rpdata->object_instance, rpdata->array_index); - apdu_len = encode_application_unsigned( - &apdu[apdu_len], unsigned_value); - } else { - rpdata->error_class = ERROR_CLASS_PROPERTY; - rpdata->error_code = ERROR_CODE_INVALID_ARRAY_INDEX; - apdu_len = BACNET_STATUS_ERROR; - } + apdu_len = bacnet_array_encode(rpdata->object_instance, + rpdata->array_index, Channel_Control_Groups_Element_Encode, + CONTROL_GROUPS_MAX, apdu, apdu_size); + if (apdu_len == BACNET_STATUS_ABORT) { + rpdata->error_code = + ERROR_CODE_ABORT_SEGMENTATION_NOT_SUPPORTED; + } else if (apdu_len == BACNET_STATUS_ERROR) { + rpdata->error_class = ERROR_CLASS_PROPERTY; + rpdata->error_code = ERROR_CODE_INVALID_ARRAY_INDEX; } break; default: @@ -1463,7 +1500,7 @@ int Channel_Read_Property(BACNET_READ_PROPERTY_DATA *rpdata) break; } /* only array properties can have array options */ - if ((apdu_len >= 0) && (rpdata->object_property != PROP_PRIORITY_ARRAY) && + if ((apdu_len >= 0) && (rpdata->object_property != PROP_CONTROL_GROUPS) && (rpdata->object_property != PROP_LIST_OF_OBJECT_PROPERTY_REFERENCES) && (rpdata->array_index != BACNET_ARRAY_ALL)) { rpdata->error_class = ERROR_CLASS_PROPERTY; @@ -1502,7 +1539,8 @@ bool Channel_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) wp_data->error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; return false; } - if ((wp_data->object_property != PROP_PRIORITY_ARRAY) && + if ((wp_data->object_property != PROP_CONTROL_GROUPS) && + (wp_data->object_property != PROP_LIST_OF_OBJECT_PROPERTY_REFERENCES) && (wp_data->array_index != BACNET_ARRAY_ALL)) { /* only array properties can have array options */ wp_data->error_class = ERROR_CLASS_PROPERTY; @@ -1619,31 +1657,116 @@ bool Channel_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) } /** - * Initializes the Channel object data + * @brief Sets a callback used when present-value is written from BACnet + * @param cb - callback used to provide indications */ -void Channel_Init(void) +void Channel_Write_Property_Internal_Callback_Set(write_property_function cb) { - unsigned i, m, g; + Write_Property_Internal_Callback = cb; +} - for (i = 0; i < BACNET_CHANNELS_MAX; i++) { - Channel[i].Present_Value.tag = BACNET_APPLICATION_TAG_EMPTYLIST; - Channel[i].Out_Of_Service = false; - Channel[i].Last_Priority = BACNET_NO_PRIORITY; - Channel[i].Write_Status = BACNET_WRITE_STATUS_IDLE; - for (m = 0; m < CHANNEL_MEMBERS_MAX; m++) { - Channel[i].Members[m].objectIdentifier.type = - OBJECT_LIGHTING_OUTPUT; - Channel[i].Members[m].objectIdentifier.instance = i + 1; - Channel[i].Members[m].propertyIdentifier = PROP_LIGHTING_COMMAND; - Channel[i].Members[m].arrayIndex = BACNET_ARRAY_ALL; - Channel[i].Members[m].deviceIdentifier.type = OBJECT_DEVICE; - Channel[i].Members[m].deviceIdentifier.instance = 0; - } - Channel[i].Number = 0; - for (g = 0; g < CONTROL_GROUPS_MAX; g++) { - Channel[i].Control_Groups[g] = 0; +/** + * @brief Creates a new object + * @param object_instance - object-instance number of the object + * @return the object-instance that was created, or BACNET_MAX_INSTANCE + */ +uint32_t Channel_Create(uint32_t object_instance) +{ + struct object_data *pObject = NULL; + int index = 0; + unsigned m, g; + + if (object_instance > BACNET_MAX_INSTANCE) { + return BACNET_MAX_INSTANCE; + } else if (object_instance == BACNET_MAX_INSTANCE) { + /* wildcard instance */ + /* the Object_Identifier property of the newly created object + shall be initialized to a value that is unique within the + responding BACnet-user device. The method used to generate + the object identifier is a local matter.*/ + object_instance = Keylist_Next_Empty_Key(Object_List, 1); + } + pObject = Keylist_Data(Object_List, object_instance); + if (!pObject) { + pObject = calloc(1, sizeof(struct object_data)); + if (pObject) { + /* channel defaults */ + pObject->Object_Name = NULL; + pObject->Present_Value.tag = BACNET_APPLICATION_TAG_EMPTYLIST; + pObject->Out_Of_Service = false; + pObject->Last_Priority = BACNET_NO_PRIORITY; + pObject->Write_Status = BACNET_WRITE_STATUS_IDLE; + for (m = 0; m < CHANNEL_MEMBERS_MAX; m++) { + pObject->Members[m].objectIdentifier.type = + OBJECT_LIGHTING_OUTPUT; + pObject->Members[m].objectIdentifier.instance = + BACNET_MAX_INSTANCE; + pObject->Members[m].propertyIdentifier = PROP_PRESENT_VALUE; + pObject->Members[m].arrayIndex = BACNET_ARRAY_ALL; + pObject->Members[m].deviceIdentifier.type = OBJECT_DEVICE; + pObject->Members[m].deviceIdentifier.instance = + BACNET_MAX_INSTANCE; + } + pObject->Number = 0; + for (g = 0; g < CONTROL_GROUPS_MAX; g++) { + pObject->Control_Groups[g] = 0; + } + /* add to list */ + index = Keylist_Data_Add(Object_List, object_instance, pObject); + if (index < 0) { + free(pObject); + return BACNET_MAX_INSTANCE; + } + } else { + return BACNET_MAX_INSTANCE; } } - return; + return object_instance; +} + +/** + * Deletes a dynamically created object + * @param object_instance - object-instance number of the object + * @return true if the object is deleted + */ +bool Channel_Delete(uint32_t object_instance) +{ + bool status = false; + struct object_data *pObject = NULL; + + pObject = Keylist_Data_Delete(Object_List, object_instance); + if (pObject) { + free(pObject); + status = true; + } + + return status; +} + +/** + * Deletes all the dynamic objects and their data + */ +void Channel_Cleanup(void) +{ + struct object_data *pObject; + + if (Object_List) { + do { + pObject = Keylist_Data_Pop(Object_List); + if (pObject) { + free(pObject); + } + } while (pObject); + Keylist_Delete(Object_List); + Object_List = NULL; + } +} + +/** + * Initializes the object data + */ +void Channel_Init(void) +{ + Object_List = Keylist_Create(); } diff --git a/src/bacnet/basic/object/channel.h b/src/bacnet/basic/object/channel.h index fad67f30..c15aa572 100644 --- a/src/bacnet/basic/object/channel.h +++ b/src/bacnet/basic/object/channel.h @@ -7,8 +7,8 @@ * @section DESCRIPTION * * The Channel object is a command object without a priority array, and the - * present-value property uses a priority array and a single precision floating point - * data type. + * present-value property uses a priority array and a single precision floating + * point data type. * * @section LICENSE * @@ -42,31 +42,21 @@ #include "bacnet/wp.h" #include "bacnet/basic/object/lo.h" -#ifdef __cplusplus -extern "C" { -#endif /* __cplusplus */ - /* BACNET_CHANNEL_VALUE decodes WriteProperty service requests Choose the datatypes that your application supports */ -#if !(defined(CHANNEL_NUMERIC) || \ - defined(CHANNEL_NULL) || \ - defined(CHANNEL_BOOLEAN) || \ - defined(CHANNEL_UNSIGNED) || \ - defined(CHANNEL_SIGNED) || \ - defined(CHANNEL_REAL) || \ - defined(CHANNEL_DOUBLE) || \ - defined(CHANNEL_OCTET_STRING) || \ - defined(CHANNEL_CHARACTER_STRING) || \ - defined(CHANNEL_BIT_STRING) || \ - defined(CHANNEL_ENUMERATED) || \ - defined(CHANNEL_DATE) || \ - defined(CHANNEL_TIME) || \ - defined(CHANNEL_OBJECT_ID) || \ - defined(CHANNEL_LIGHTING_COMMAND)) +#if !(defined(CHANNEL_NUMERIC) || defined(CHANNEL_NULL) || \ + defined(CHANNEL_BOOLEAN) || defined(CHANNEL_UNSIGNED) || \ + defined(CHANNEL_SIGNED) || defined(CHANNEL_REAL) || \ + defined(CHANNEL_DOUBLE) || defined(CHANNEL_OCTET_STRING) || \ + defined(CHANNEL_CHARACTER_STRING) || defined(CHANNEL_BIT_STRING) || \ + defined(CHANNEL_ENUMERATED) || defined(CHANNEL_DATE) || \ + defined(CHANNEL_TIME) || defined(CHANNEL_OBJECT_ID) || \ + defined(CHANNEL_LIGHTING_COMMAND) || defined(CHANNEL_XY_COLOR) || \ + defined(CHANNEL_COLOR_COMMAND)) #define CHANNEL_NUMERIC #endif -#if defined (CHANNEL_NUMERIC) +#if defined(CHANNEL_NUMERIC) #define CHANNEL_NULL #define CHANNEL_BOOLEAN #define CHANNEL_UNSIGNED @@ -75,152 +65,156 @@ extern "C" { #define CHANNEL_DOUBLE #define CHANNEL_ENUMERATED #define CHANNEL_LIGHTING_COMMAND +#define CHANNEL_COLOR_COMMAND +#define CHANNEL_XY_COLOR #endif - typedef struct BACnet_Channel_Value_t { - uint8_t tag; - union { - /* NULL - not needed as it is encoded in the tag alone */ -#if defined (CHANNEL_BOOLEAN) - bool Boolean; +typedef struct BACnet_Channel_Value_t { + uint8_t tag; + union { + /* NULL - not needed as it is encoded in the tag alone */ +#if defined(CHANNEL_BOOLEAN) + bool Boolean; #endif -#if defined (CHANNEL_UNSIGNED) - uint32_t Unsigned_Int; +#if defined(CHANNEL_UNSIGNED) + uint32_t Unsigned_Int; #endif -#if defined (CHANNEL_SIGNED) - int32_t Signed_Int; +#if defined(CHANNEL_SIGNED) + int32_t Signed_Int; #endif -#if defined (CHANNEL_REAL) - float Real; +#if defined(CHANNEL_REAL) + float Real; #endif -#if defined (CHANNEL_DOUBLE) - double Double; +#if defined(CHANNEL_DOUBLE) + double Double; #endif -#if defined (CHANNEL_OCTET_STRING) - BACNET_OCTET_STRING Octet_String; +#if defined(CHANNEL_OCTET_STRING) + BACNET_OCTET_STRING Octet_String; #endif -#if defined (CHANNEL_CHARACTER_STRING) - BACNET_CHARACTER_STRING Character_String; +#if defined(CHANNEL_CHARACTER_STRING) + BACNET_CHARACTER_STRING Character_String; #endif -#if defined (CHANNEL_BIT_STRING) - BACNET_BIT_STRING Bit_String; +#if defined(CHANNEL_BIT_STRING) + BACNET_BIT_STRING Bit_String; #endif -#if defined (CHANNEL_ENUMERATED) - uint32_t Enumerated; +#if defined(CHANNEL_ENUMERATED) + uint32_t Enumerated; #endif -#if defined (CHANNEL_DATE) - BACNET_DATE Date; +#if defined(CHANNEL_DATE) + BACNET_DATE Date; #endif -#if defined (CHANNEL_TIME) - BACNET_TIME Time; +#if defined(CHANNEL_TIME) + BACNET_TIME Time; #endif -#if defined (CHANNEL_OBJECT_ID) - BACNET_OBJECT_ID Object_Id; +#if defined(CHANNEL_OBJECT_ID) + BACNET_OBJECT_ID Object_Id; #endif -#if defined (CHANNEL_LIGHTING_COMMAND) - BACNET_LIGHTING_COMMAND Lighting_Command; +#if defined(CHANNEL_LIGHTING_COMMAND) + BACNET_LIGHTING_COMMAND Lighting_Command; #endif - } type; - /* simple linked list if needed */ - struct BACnet_Channel_Value_t *next; - } BACNET_CHANNEL_VALUE; +#if defined(CHANNEL_COLOR_COMMAND) + BACNET_COLOR_COMMAND Color_Command; +#endif +#if defined(CHANNEL_XY_COLOR) + BACNET_XY_COLOR XY_Color; +#endif + } type; + /* simple linked list if needed */ + struct BACnet_Channel_Value_t *next; +} BACNET_CHANNEL_VALUE; - BACNET_STACK_EXPORT - void Channel_Property_Lists(const int **pRequired, - const int **pOptional, - const int **pProprietary); - BACNET_STACK_EXPORT - bool Channel_Valid_Instance(uint32_t object_instance); - BACNET_STACK_EXPORT - unsigned Channel_Count(void); - BACNET_STACK_EXPORT - uint32_t Channel_Index_To_Instance(unsigned index); - BACNET_STACK_EXPORT - unsigned Channel_Instance_To_Index(uint32_t instance); - BACNET_STACK_EXPORT - bool Channel_Object_Instance_Add(uint32_t instance); +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ +BACNET_STACK_EXPORT +void Channel_Property_Lists( + const int **pRequired, const int **pOptional, const int **pProprietary); +BACNET_STACK_EXPORT +bool Channel_Valid_Instance(uint32_t object_instance); +BACNET_STACK_EXPORT +unsigned Channel_Count(void); +BACNET_STACK_EXPORT +uint32_t Channel_Index_To_Instance(unsigned index); +BACNET_STACK_EXPORT +unsigned Channel_Instance_To_Index(uint32_t instance); +BACNET_STACK_EXPORT +bool Channel_Object_Instance_Add(uint32_t instance); - BACNET_STACK_EXPORT - bool Channel_Object_Name(uint32_t object_instance, - BACNET_CHARACTER_STRING * object_name); - BACNET_STACK_EXPORT - bool Channel_Name_Set(uint32_t object_instance, - char *new_name); +BACNET_STACK_EXPORT +bool Channel_Object_Name( + uint32_t object_instance, BACNET_CHARACTER_STRING *object_name); +BACNET_STACK_EXPORT +bool Channel_Name_Set(uint32_t object_instance, char *new_name); - BACNET_STACK_EXPORT - int Channel_Read_Property(BACNET_READ_PROPERTY_DATA * rpdata); - BACNET_STACK_EXPORT - bool Channel_Write_Property(BACNET_WRITE_PROPERTY_DATA * wp_data); +BACNET_STACK_EXPORT +int Channel_Read_Property(BACNET_READ_PROPERTY_DATA *rpdata); +BACNET_STACK_EXPORT +bool Channel_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data); - BACNET_STACK_EXPORT - BACNET_CHANNEL_VALUE * Channel_Present_Value(uint32_t object_instance); - BACNET_STACK_EXPORT - bool Channel_Present_Value_Set( - BACNET_WRITE_PROPERTY_DATA * wp_data, - BACNET_APPLICATION_DATA_VALUE * value); +BACNET_STACK_EXPORT +BACNET_CHANNEL_VALUE *Channel_Present_Value(uint32_t object_instance); +BACNET_STACK_EXPORT +bool Channel_Present_Value_Set( + BACNET_WRITE_PROPERTY_DATA *wp_data, BACNET_APPLICATION_DATA_VALUE *value); - BACNET_STACK_EXPORT - bool Channel_Out_Of_Service(uint32_t object_instance); - BACNET_STACK_EXPORT - void Channel_Out_Of_Service_Set(uint32_t object_instance, - bool oos_flag); +BACNET_STACK_EXPORT +bool Channel_Out_Of_Service(uint32_t object_instance); +BACNET_STACK_EXPORT +void Channel_Out_Of_Service_Set(uint32_t object_instance, bool oos_flag); - BACNET_STACK_EXPORT - unsigned Channel_Last_Priority(uint32_t object_instance); - BACNET_STACK_EXPORT - BACNET_WRITE_STATUS Channel_Write_Status(uint32_t object_instance); - uint16_t Channel_Number(uint32_t object_instance); - BACNET_STACK_EXPORT - bool Channel_Number_Set(uint32_t object_instance, uint16_t value); +BACNET_STACK_EXPORT +unsigned Channel_Last_Priority(uint32_t object_instance); +BACNET_STACK_EXPORT +BACNET_WRITE_STATUS Channel_Write_Status(uint32_t object_instance); +uint16_t Channel_Number(uint32_t object_instance); +BACNET_STACK_EXPORT +bool Channel_Number_Set(uint32_t object_instance, uint16_t value); - BACNET_STACK_EXPORT - unsigned Channel_Reference_List_Member_Count(uint32_t object_instance); - BACNET_STACK_EXPORT - BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE * - Channel_Reference_List_Member_Element(uint32_t object_instance, - unsigned element); - BACNET_STACK_EXPORT - bool Channel_Reference_List_Member_Element_Set(uint32_t object_instance, - unsigned array_index, - BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE *pMemberSrc); - BACNET_STACK_EXPORT - unsigned Channel_Reference_List_Member_Element_Add(uint32_t object_instance, - BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE *pMemberSrc); - BACNET_STACK_EXPORT - unsigned Channel_Reference_List_Member_Local_Add( - uint32_t object_instance, - BACNET_OBJECT_TYPE type, - uint32_t instance, - BACNET_PROPERTY_ID propertyIdentifier, - uint32_t arrayIndex); - BACNET_STACK_EXPORT - uint16_t Channel_Control_Groups_Element( - uint32_t object_instance, - int32_t array_index); - BACNET_STACK_EXPORT - bool Channel_Control_Groups_Element_Set( - uint32_t object_instance, - int32_t array_index, - uint16_t value); - BACNET_STACK_EXPORT - bool Channel_Value_Copy(BACNET_CHANNEL_VALUE * cvalue, - BACNET_APPLICATION_DATA_VALUE * value); - BACNET_STACK_EXPORT - int Channel_Value_Encode(uint8_t *apdu, int apdu_max, - BACNET_CHANNEL_VALUE * value); - BACNET_STACK_EXPORT - int Channel_Coerce_Data_Encode( - uint8_t * apdu, - unsigned max_apdu, - BACNET_APPLICATION_DATA_VALUE * value, - BACNET_APPLICATION_TAG tag); - BACNET_STACK_EXPORT - bool Channel_Write_Member_Value( - BACNET_WRITE_PROPERTY_DATA * wp_data, - BACNET_APPLICATION_DATA_VALUE * value); +BACNET_STACK_EXPORT +unsigned Channel_Reference_List_Member_Count(uint32_t object_instance); +BACNET_STACK_EXPORT +BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE *Channel_Reference_List_Member_Element( + uint32_t object_instance, unsigned element); +BACNET_STACK_EXPORT +bool Channel_Reference_List_Member_Element_Set(uint32_t object_instance, + unsigned array_index, + BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE *pMemberSrc); +BACNET_STACK_EXPORT +unsigned Channel_Reference_List_Member_Element_Add(uint32_t object_instance, + BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE *pMemberSrc); +BACNET_STACK_EXPORT +uint16_t Channel_Control_Groups_Element( + uint32_t object_instance, int32_t array_index); +BACNET_STACK_EXPORT +bool Channel_Control_Groups_Element_Set( + uint32_t object_instance, int32_t array_index, uint16_t value); +BACNET_STACK_EXPORT +bool Channel_Value_Copy( + BACNET_CHANNEL_VALUE *cvalue, BACNET_APPLICATION_DATA_VALUE *value); +BACNET_STACK_EXPORT +int Channel_Value_Encode( + uint8_t *apdu, int apdu_max, BACNET_CHANNEL_VALUE *value); +BACNET_STACK_EXPORT +int Channel_Coerce_Data_Encode(uint8_t *apdu, + size_t apdu_size, + BACNET_APPLICATION_DATA_VALUE *value, + BACNET_APPLICATION_TAG tag); +BACNET_STACK_EXPORT +bool Channel_Write_Member_Value( + BACNET_WRITE_PROPERTY_DATA *wp_data, BACNET_APPLICATION_DATA_VALUE *value); - BACNET_STACK_EXPORT - void Channel_Init(void); +BACNET_STACK_EXPORT +void Channel_Write_Property_Internal_Callback_Set( + write_property_function cb); + +BACNET_STACK_EXPORT +uint32_t Channel_Create(uint32_t object_instance); +BACNET_STACK_EXPORT +bool Channel_Delete(uint32_t object_instance); +BACNET_STACK_EXPORT +void Channel_Cleanup(void); +BACNET_STACK_EXPORT +void Channel_Init(void); #ifdef __cplusplus } diff --git a/src/bacnet/basic/object/client/device-client.c b/src/bacnet/basic/object/client/device-client.c index bb74d75a..e3497b8c 100644 --- a/src/bacnet/basic/object/client/device-client.c +++ b/src/bacnet/basic/object/client/device-client.c @@ -118,7 +118,7 @@ static object_functions_t Object_Table[] = { NULL /* Value_Lists */, NULL /* COV */, NULL /* COV Clear */, NULL /* Intrinsic Reporting */, NULL /* Add_List_Element */, NULL /* Remove_List_Element */, - NULL /* Create */, NULL /* Delete */ }, + NULL /* Create */, NULL /* Delete */, NULL /* Timer */ }, #if (BACNET_PROTOCOL_REVISION >= 17) { OBJECT_NETWORK_PORT, Network_Port_Init, Network_Port_Count, Network_Port_Index_To_Instance, Network_Port_Valid_Instance, @@ -127,7 +127,7 @@ static object_functions_t Object_Table[] = { NULL /* ReadRangeInfo */, NULL /* Iterator */, NULL /* Value_Lists */, NULL /* COV */, NULL /* COV Clear */, NULL /* Intrinsic Reporting */, NULL /* Add_List_Element */, NULL /* Remove_List_Element */, - NULL /* Create */, NULL /* Delete */ }, + NULL /* Create */, NULL /* Delete */, NULL /* Timer */ }, #endif { MAX_BACNET_OBJECT_TYPE, NULL /* Init */, NULL /* Count */, NULL /* Index_To_Instance */, NULL /* Valid_Instance */, @@ -137,7 +137,7 @@ static object_functions_t Object_Table[] = { NULL /* COV */, NULL /* COV Clear */, NULL /* Intrinsic Reporting */, NULL /* Add_List_Element */, NULL /* Remove_List_Element */, - NULL /* Create */, NULL /* Delete */ }, + NULL /* Create */, NULL /* Delete */, NULL /* Timer */ }, }; /** Glue function to let the Device object, when called by a handler, @@ -1042,6 +1042,34 @@ int Device_Read_Property(BACNET_READ_PROPERTY_DATA *rpdata) return apdu_len; } +/** + * @brief Updates all the object timers with elapsed milliseconds + * @param milliseconds - number of milliseconds elapsed + */ +void Device_Timer( + uint16_t milliseconds) +{ + struct object_functions *pObject; + unsigned count = 0; + uint32_t instance; + + pObject = Object_Table; + while (pObject->Object_Type < MAX_BACNET_OBJECT_TYPE) { + if (pObject->Object_Count) { + count = pObject->Object_Count(); + } + while (count) { + count--; + if ((pObject->Object_Timer) && + (pObject->Object_Index_To_Instance)) { + instance = pObject->Object_Index_To_Instance(count); + pObject->Object_Timer(instance, milliseconds); + } + } + pObject++; + } +} + /** Initialize the Device Object. Initialize the group of object helper functions for any supported Object. Initialize each of the Device Object child Object instances. diff --git a/src/bacnet/basic/object/color_object.c b/src/bacnet/basic/object/color_object.c index 2f275217..e576c1fe 100644 --- a/src/bacnet/basic/object/color_object.c +++ b/src/bacnet/basic/object/color_object.c @@ -39,18 +39,29 @@ #include "bacnet/basic/object/device.h" #include "bacnet/basic/services.h" #include "bacnet/basic/sys/keylist.h" +#include "bacnet/basic/sys/linear.h" /* me! */ -#include "color_object.h" +#include "bacnet/basic/object/color_object.h" struct object_data { bool Changed : 1; bool Write_Enabled : 1; + /* indicate the target color value for the color output */ BACNET_XY_COLOR Present_Value; + /* indicates the components of the object's actual color output */ BACNET_XY_COLOR Tracking_Value; + /* used to request specific behaviors */ BACNET_COLOR_COMMAND Color_Command; + /* indicates that there may be processes in the color object that may + cause the Tracking_Value and Present_Value to differ temporarily. */ BACNET_COLOR_OPERATION_IN_PROGRESS In_Progress; + /* the color to be used for the color output when the device is restarted + until such time as Present_Value or Color_Command are written */ BACNET_XY_COLOR Default_Color; + /* indicates the amount of time in milliseconds over which changes + to the color output reflected in the Tracking_Value property */ uint32_t Default_Fade_Time; + /* The transition may be NONE or FADE. */ BACNET_COLOR_TRANSITION Transition; const char *Object_Name; const char *Description; @@ -198,10 +209,10 @@ bool Color_Present_Value_Set(uint32_t object_instance, BACNET_XY_COLOR *value) } /** - * For a given object instance-number, sets the present-value + * For a given object instance-number, writes to the present-value * * @param object_instance - object-instance number of the object - * @param value - floating point Color + * @param value - property value to be written * @param priority - priority-array index value 1..16 * @param error_class - the BACnet error class * @param error_code - BACnet Error code @@ -216,23 +227,21 @@ static bool Color_Present_Value_Write(uint32_t object_instance, { bool status = false; struct object_data *pObject; - BACNET_XY_COLOR old_value = { 0.0, 0.0 }; pObject = Keylist_Data(Object_List, object_instance); if (pObject) { (void)priority; - if (pObject->Write_Enabled) { - xy_color_copy(&old_value, &pObject->Present_Value); - xy_color_copy(&pObject->Present_Value, value); - if (Color_Write_Present_Value_Callback) { - Color_Write_Present_Value_Callback( - object_instance, &old_value, value); - } - status = true; + xy_color_copy(&pObject->Present_Value, value); + /* configure the color-command to perform the transition */ + if (pObject->Transition == BACNET_COLOR_TRANSITION_FADE) { + pObject->Color_Command.transit.fade_time = + pObject->Default_Fade_Time; } else { - *error_class = ERROR_CLASS_PROPERTY; - *error_code = ERROR_CODE_WRITE_ACCESS_DENIED; + pObject->Color_Command.transit.fade_time = 0; } + pObject->Color_Command.operation = BACNET_COLOR_OPERATION_FADE_TO_COLOR; + xy_color_copy(&pObject->Color_Command.target.color, value); + status = true; } else { *error_class = ERROR_CLASS_OBJECT; *error_code = ERROR_CODE_UNKNOWN_OBJECT; @@ -326,6 +335,44 @@ bool Color_Command_Set(uint32_t object_instance, BACNET_COLOR_COMMAND *value) return status; } +/** + * For a given object instance-number, writes to the present-value + * + * @param object_instance - object-instance number of the object + * @param value - property value to be written + * @param priority - priority-array index value 1..16 + * @param error_class - the BACnet error class + * @param error_code - BACnet Error code + * + * @return true if values are within range and present-value is set. + */ +static bool Color_Command_Write(uint32_t object_instance, + BACNET_COLOR_COMMAND *value, + uint8_t priority, + BACNET_ERROR_CLASS *error_class, + BACNET_ERROR_CODE *error_code) +{ + bool status = false; + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + (void)priority; + if (pObject->Write_Enabled) { + color_command_copy(&pObject->Color_Command, value); + status = true; + } else { + *error_class = ERROR_CLASS_PROPERTY; + *error_code = ERROR_CODE_WRITE_ACCESS_DENIED; + } + } else { + *error_class = ERROR_CLASS_OBJECT; + *error_code = ERROR_CODE_UNKNOWN_OBJECT; + } + + return status; +} + /** * For a given object instance-number, gets the property value * @@ -413,11 +460,50 @@ bool Color_Default_Color_Set(uint32_t object_instance, BACNET_XY_COLOR *value) return status; } +/** + * Handle a WriteProperty to a specific property. + * + * @param object_instance - object-instance number of the object + * @param value - property value to be written + * @param priority - priority-array index value 1..16 + * @param error_class - the BACnet error class + * @param error_code - BACnet Error code + * + * @return true if values are within range and present-value is set. + */ +static bool Color_Default_Color_Write(uint32_t object_instance, + BACNET_XY_COLOR *value, + uint8_t priority, + BACNET_ERROR_CLASS *error_class, + BACNET_ERROR_CODE *error_code) +{ + bool status = false; + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + (void)priority; + if (pObject->Write_Enabled) { + xy_color_copy(&pObject->Default_Color, value); + status = true; + } else { + *error_class = ERROR_CLASS_PROPERTY; + *error_code = ERROR_CODE_WRITE_ACCESS_DENIED; + } + } else { + *error_class = ERROR_CLASS_OBJECT; + *error_code = ERROR_CODE_UNKNOWN_OBJECT; + } + + return status; +} + /** * For a given object instance-number, gets the property value * * @param object_instance - object-instance number of the object - * @return property value + * @return the amount of time in milliseconds over which changes + * to the Color reflected in the Tracking_Value property */ uint32_t Color_Default_Fade_Time(uint32_t object_instance) { @@ -436,7 +522,8 @@ uint32_t Color_Default_Fade_Time(uint32_t object_instance) * For a given object instance-number, sets the property value * * @param object_instance - object-instance number of the object - * @param value - BACNET_COLOR_OPERATION_IN_PROGRESS + * @param value - the amount of time in milliseconds over which changes + * to the Color reflected in the Tracking_Value property * @return true if values are within range and value is set. */ bool Color_Default_Fade_Time_Set(uint32_t object_instance, uint32_t value) @@ -457,6 +544,51 @@ bool Color_Default_Fade_Time_Set(uint32_t object_instance, uint32_t value) return status; } +/** + * Handle a WriteProperty to a specific property. + * + * @param object_instance - object-instance number of the object + * @param value - property value to be written + * @param priority - priority-array index value 1..16 + * @param error_class - the BACnet error class + * @param error_code - BACnet Error code + * + * @return true if values are within range and present-value is set. + */ +static bool Color_Default_Fade_Time_Write(uint32_t object_instance, + BACNET_UNSIGNED_INTEGER value, + uint8_t priority, + BACNET_ERROR_CLASS *error_class, + BACNET_ERROR_CODE *error_code) +{ + bool status = false; + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + (void)priority; + if (pObject->Write_Enabled) { + if ((value == 0) || + ((value >= BACNET_COLOR_FADE_TIME_MIN) && + (value <= BACNET_COLOR_FADE_TIME_MAX))) { + pObject->Default_Fade_Time = value; + status = true; + } else { + *error_class = ERROR_CLASS_PROPERTY; + *error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; + } + } else { + *error_class = ERROR_CLASS_PROPERTY; + *error_code = ERROR_CODE_WRITE_ACCESS_DENIED; + } + } else { + *error_class = ERROR_CLASS_OBJECT; + *error_code = ERROR_CODE_UNKNOWN_OBJECT; + } + + return status; +} + /** * For a given object instance-number, gets the property value * @@ -500,6 +632,49 @@ bool Color_Transition_Set( return status; } +/** + * Handle a WriteProperty to a specific property. + * + * @param object_instance - object-instance number of the object + * @param value - property value to be written + * @param priority - priority-array index value 1..16 + * @param error_class - the BACnet error class + * @param error_code - BACnet Error code + * + * @return true if values are within range and present-value is set. + */ +static bool Color_Transition_Write(uint32_t object_instance, + uint32_t value, + uint8_t priority, + BACNET_ERROR_CLASS *error_class, + BACNET_ERROR_CODE *error_code) +{ + bool status = false; + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + (void)priority; + if (pObject->Write_Enabled) { + if (value < BACNET_COLOR_TRANSITION_MAX) { + pObject->Transition = value; + status = true; + } else { + *error_class = ERROR_CLASS_PROPERTY; + *error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; + } + } else { + *error_class = ERROR_CLASS_PROPERTY; + *error_code = ERROR_CODE_WRITE_ACCESS_DENIED; + } + } else { + *error_class = ERROR_CLASS_OBJECT; + *error_code = ERROR_CODE_UNKNOWN_OBJECT; + } + + return status; +} + /** * For a given object instance-number, loads the object-name into * a characterstring. Note that the object name must be unique @@ -618,6 +793,95 @@ bool Color_Description_Set(uint32_t object_instance, char *new_name) return status; } +/** + * Updates the color object tracking value while fading + * + * Transitioning from one color to another is supported by writing a + * FADE_TO_COLOR command to the property Color_Command. + * The current color is always indicated in the + * Tracking_Value property. If a color command is + * currently in progress and the Present_Value is written, + * the color command shall be halted. + * + * @param object_instance - object-instance number of the object + * @param milliseconds - number of milliseconds elapsed + */ +static void Color_Fade_To_Color_Handler( + uint32_t object_instance, uint16_t milliseconds) +{ + BACNET_XY_COLOR old_value; + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (!pObject) { + return; + } + xy_color_copy(&old_value, &pObject->Tracking_Value); + if (milliseconds >= pObject->Color_Command.transit.fade_time) { + /* stop fading */ + xy_color_copy( + &pObject->Tracking_Value, &pObject->Color_Command.target.color); + pObject->In_Progress = BACNET_COLOR_OPERATION_IN_PROGRESS_IDLE; + pObject->Color_Command.operation = BACNET_COLOR_OPERATION_STOP; + pObject->Color_Command.transit.fade_time = 0; + } else { + if (xy_color_same(&old_value, &pObject->Color_Command.target.color)) { + /* stop fading */ + xy_color_copy( + &pObject->Tracking_Value, &pObject->Color_Command.target.color); + pObject->In_Progress = BACNET_COLOR_OPERATION_IN_PROGRESS_IDLE; + pObject->Color_Command.operation = BACNET_COLOR_OPERATION_STOP; + pObject->Color_Command.transit.fade_time = 0; + } else { + /* fading */ + pObject->Tracking_Value.x_coordinate = linear_interpolate(0, + milliseconds, pObject->Color_Command.transit.fade_time, + old_value.x_coordinate, + pObject->Color_Command.target.color.x_coordinate); + pObject->Tracking_Value.y_coordinate = linear_interpolate(0, + milliseconds, pObject->Color_Command.transit.fade_time, + old_value.y_coordinate, + pObject->Color_Command.target.color.y_coordinate); + pObject->Color_Command.transit.fade_time -= milliseconds; + pObject->In_Progress = + BACNET_COLOR_OPERATION_IN_PROGRESS_FADE_ACTIVE; + } + } + if (Color_Write_Present_Value_Callback) { + Color_Write_Present_Value_Callback( + object_instance, &old_value, &pObject->Tracking_Value); + } +} + +/** + * Updates the color object tracking value per ramp or fade + * + * @param object_instance - object-instance number of the object + * @param milliseconds - number of milliseconds elapsed + */ +void Color_Timer(uint32_t object_instance, uint16_t milliseconds) +{ + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + switch (pObject->Color_Command.operation) { + case BACNET_COLOR_OPERATION_NONE: + pObject->In_Progress = BACNET_COLOR_OPERATION_IN_PROGRESS_IDLE; + break; + case BACNET_COLOR_OPERATION_FADE_TO_COLOR: + Color_Fade_To_Color_Handler( + object_instance, milliseconds); + break; + case BACNET_COLOR_OPERATION_STOP: + pObject->In_Progress = BACNET_COLOR_OPERATION_IN_PROGRESS_IDLE; + break; + default: + break; + } + } +} + /** * ReadProperty handler for this object. For the given ReadProperty * data, the application_data is loaded or the error flags are set. @@ -725,10 +989,14 @@ bool Color_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) bool status = false; /* return value */ int len = 0; BACNET_APPLICATION_DATA_VALUE value; + int apdu_size = 0; + uint8_t *apdu = NULL; /* decode the some of the request */ - len = bacapp_decode_application_data( - wp_data->application_data, wp_data->application_data_len, &value); + apdu = wp_data->application_data; + apdu_size = wp_data->application_data_len; + len = bacapp_decode_known_property(apdu, apdu_size, &value, + wp_data->object_type, wp_data->object_property); /* FIXME: len < application_data_len: more data? */ if (len < 0) { /* error while decoding - a value larger than we can handle */ @@ -747,17 +1015,55 @@ bool Color_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) switch (wp_data->object_property) { case PROP_PRESENT_VALUE: status = write_property_type_valid( - wp_data, &value, BACNET_APPLICATION_TAG_REAL); + wp_data, &value, BACNET_APPLICATION_TAG_XY_COLOR); if (status) { status = Color_Present_Value_Write(wp_data->object_instance, &value.type.XY_Color, wp_data->priority, &wp_data->error_class, &wp_data->error_code); } break; + case PROP_COLOR_COMMAND: + status = write_property_type_valid( + wp_data, &value, BACNET_APPLICATION_TAG_COLOR_COMMAND); + if (status) { + status = Color_Command_Write(wp_data->object_instance, + &value.type.Color_Command, wp_data->priority, + &wp_data->error_class, &wp_data->error_code); + } + break; + case PROP_DEFAULT_COLOR: + status = write_property_type_valid( + wp_data, &value, BACNET_APPLICATION_TAG_XY_COLOR); + if (status) { + status = Color_Default_Color_Write(wp_data->object_instance, + &value.type.XY_Color, wp_data->priority, + &wp_data->error_class, &wp_data->error_code); + } + break; + case PROP_DEFAULT_FADE_TIME: + status = write_property_type_valid( + wp_data, &value, BACNET_APPLICATION_TAG_UNSIGNED_INT); + if (status) { + status = Color_Default_Fade_Time_Write(wp_data->object_instance, + value.type.Unsigned_Int, wp_data->priority, + &wp_data->error_class, &wp_data->error_code); + } + break; + case PROP_TRANSITION: + status = write_property_type_valid( + wp_data, &value, BACNET_APPLICATION_TAG_ENUMERATED); + if (status) { + status = Color_Transition_Write(wp_data->object_instance, + value.type.Enumerated, wp_data->priority, + &wp_data->error_class, &wp_data->error_code); + } + break; case PROP_OBJECT_IDENTIFIER: case PROP_OBJECT_TYPE: case PROP_OBJECT_NAME: case PROP_DESCRIPTION: + case PROP_TRACKING_VALUE: + case PROP_IN_PROGRESS: wp_data->error_class = ERROR_CLASS_PROPERTY; wp_data->error_code = ERROR_CODE_WRITE_ACCESS_DENIED; break; @@ -840,9 +1146,9 @@ uint32_t Color_Create(uint32_t object_instance) return BACNET_MAX_INSTANCE; } else if (object_instance == BACNET_MAX_INSTANCE) { /* wildcard instance */ - /* the Object_Identifier property of the newly created object - shall be initialized to a value that is unique within the - responding BACnet-user device. The method used to generate + /* the Object_Identifier property of the newly created object + shall be initialized to a value that is unique within the + responding BACnet-user device. The method used to generate the object identifier is a local matter.*/ object_instance = Keylist_Next_Empty_Key(Object_List, 1); } @@ -851,23 +1157,26 @@ uint32_t Color_Create(uint32_t object_instance) pObject = calloc(1, sizeof(struct object_data)); if (pObject) { pObject->Object_Name = NULL; - pObject->Present_Value.x_coordinate = 0.0; - pObject->Present_Value.y_coordinate = 0.0; - pObject->Tracking_Value.x_coordinate = 0.0; - pObject->Tracking_Value.y_coordinate = 0.0; - pObject->Color_Command.operation = BACNET_COLOR_OPERATION_NONE; + /* color defaults */ + xy_color_set(&pObject->Present_Value, 0.0, 0.0); + xy_color_set(&pObject->Tracking_Value, 0.0, 0.0); + xy_color_set(&pObject->Default_Color, 1.0, 1.0); + pObject->Default_Fade_Time = BACNET_COLOR_FADE_TIME_MIN; + /* at powerup - fade to default color */ + xy_color_copy( + &pObject->Color_Command.target.color, &pObject->Default_Color); + pObject->Color_Command.operation = + BACNET_COLOR_OPERATION_FADE_TO_COLOR; + pObject->Color_Command.transit.fade_time = + pObject->Default_Fade_Time; + /* initialize all the status */ pObject->In_Progress = BACNET_COLOR_OPERATION_IN_PROGRESS_IDLE; - pObject->Default_Color.x_coordinate = 1.0; - pObject->Default_Color.y_coordinate = 1.0; - pObject->Default_Fade_Time = 0; - pObject->Transition = BACNET_COLOR_TRANSITION_NONE; + pObject->Transition = BACNET_COLOR_TRANSITION_FADE; pObject->Changed = false; pObject->Write_Enabled = false; /* add to list */ index = Keylist_Data_Add(Object_List, object_instance, pObject); - if (index >= 0) { - Device_Inc_Database_Revision(); - } else { + if (index < 0) { free(pObject); return BACNET_MAX_INSTANCE; } @@ -893,7 +1202,6 @@ bool Color_Delete(uint32_t object_instance) if (pObject) { free(pObject); status = true; - Device_Inc_Database_Revision(); } return status; @@ -911,7 +1219,6 @@ void Color_Cleanup(void) pObject = Keylist_Data_Pop(Object_List); if (pObject) { free(pObject); - Device_Inc_Database_Revision(); } } while (pObject); Keylist_Delete(Object_List); @@ -925,7 +1232,4 @@ void Color_Cleanup(void) void Color_Init(void) { Object_List = Keylist_Create(); - if (Object_List) { - atexit(Color_Cleanup); - } } diff --git a/src/bacnet/basic/object/color_object.h b/src/bacnet/basic/object/color_object.h index 6c035a5d..47169865 100644 --- a/src/bacnet/basic/object/color_object.h +++ b/src/bacnet/basic/object/color_object.h @@ -29,7 +29,7 @@ #include "bacnet/wp.h" /** - * @brief Callback for gateway write present value request + * @brief Callback for tracking value * @param object_instance - object-instance number of the object * @param old_value - BACnetXYColor value prior to write * @param value - BACnetXYColor value of the write @@ -118,6 +118,9 @@ void Color_Write_Enable(uint32_t instance); BACNET_STACK_EXPORT void Color_Write_Disable(uint32_t instance); +BACNET_STACK_EXPORT +void Color_Timer(uint32_t object_instance, uint16_t milliseconds); + BACNET_STACK_EXPORT uint32_t Color_Create(uint32_t object_instance); BACNET_STACK_EXPORT diff --git a/src/bacnet/basic/object/color_temperature.c b/src/bacnet/basic/object/color_temperature.c index 6530e1d3..8e7b7fa3 100644 --- a/src/bacnet/basic/object/color_temperature.c +++ b/src/bacnet/basic/object/color_temperature.c @@ -39,6 +39,7 @@ #include "bacnet/basic/object/device.h" #include "bacnet/basic/services.h" #include "bacnet/basic/sys/keylist.h" +#include "bacnet/basic/sys/linear.h" /* me! */ #include "color_temperature.h" @@ -207,7 +208,7 @@ bool Color_Temperature_Present_Value_Set( * For a given object instance-number, sets the present-value * * @param object_instance - object-instance number of the object - * @param value - floating point Color + * @param value - property value to write * @param priority - priority-array index value 1..16 * @param error_class - the BACnet error class * @param error_code - BACnet Error code @@ -222,22 +223,34 @@ static bool Color_Temperature_Present_Value_Write(uint32_t object_instance, { bool status = false; struct object_data *pObject; - uint32_t old_value = 0; pObject = Keylist_Data(Object_List, object_instance); if (pObject) { (void)priority; - if (pObject->Write_Enabled) { - old_value = pObject->Present_Value; + if ((value >= BACNET_COLOR_TEMPERATURE_MIN) && + (value <= BACNET_COLOR_TEMPERATURE_MAX)) { pObject->Present_Value = value; - if (Color_Temperature_Write_Present_Value_Callback) { - Color_Temperature_Write_Present_Value_Callback( - object_instance, old_value, value); + /* configure the color-command to perform the transition */ + if (pObject->Transition == BACNET_COLOR_TRANSITION_FADE) { + pObject->Color_Command.transit.fade_time = + pObject->Default_Fade_Time; + pObject->Color_Command.operation = + BACNET_COLOR_OPERATION_FADE_TO_CCT; + } else if (pObject->Transition == BACNET_COLOR_TRANSITION_RAMP) { + pObject->Color_Command.transit.ramp_rate = + pObject->Default_Ramp_Rate; + pObject->Color_Command.operation = + BACNET_COLOR_OPERATION_RAMP_TO_CCT; + } else { + pObject->Color_Command.transit.fade_time = 0; + pObject->Color_Command.operation = + BACNET_COLOR_OPERATION_FADE_TO_CCT; } + pObject->Color_Command.target.color_temperature = value; status = true; } else { *error_class = ERROR_CLASS_PROPERTY; - *error_code = ERROR_CODE_WRITE_ACCESS_DENIED; + *error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; } } else { *error_class = ERROR_CLASS_OBJECT; @@ -506,6 +519,50 @@ bool Color_Temperature_Default_Color_Temperature_Set( return status; } +/** + * Handle a WriteProperty to a specific property. + * + * @param object_instance - object-instance number of the object + * @param value - property value to be written + * @param priority - priority-array index value 1..16 + * @param error_class - the BACnet error class + * @param error_code - BACnet Error code + * + * @return true if values are within range and present-value is set. + */ +static bool Color_Temperature_Default_Write(uint32_t object_instance, + BACNET_UNSIGNED_INTEGER value, + uint8_t priority, + BACNET_ERROR_CLASS *error_class, + BACNET_ERROR_CODE *error_code) +{ + bool status = false; + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + (void)priority; + if (pObject->Write_Enabled) { + if ((value >= BACNET_COLOR_TEMPERATURE_MIN) && + (value <= BACNET_COLOR_TEMPERATURE_MAX)) { + pObject->Default_Color_Temperature = value; + status = true; + } else { + *error_class = ERROR_CLASS_PROPERTY; + *error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; + } + } else { + *error_class = ERROR_CLASS_PROPERTY; + *error_code = ERROR_CODE_WRITE_ACCESS_DENIED; + } + } else { + *error_class = ERROR_CLASS_OBJECT; + *error_code = ERROR_CODE_UNKNOWN_OBJECT; + } + + return status; +} + /** * For a given object instance-number, gets the property value * @@ -551,6 +608,51 @@ bool Color_Temperature_Default_Fade_Time_Set( return status; } +/** + * Handle a WriteProperty to a specific property. + * + * @param object_instance - object-instance number of the object + * @param value - property value to be written + * @param priority - priority-array index value 1..16 + * @param error_class - the BACnet error class + * @param error_code - BACnet Error code + * + * @return true if values are within range and present-value is set. + */ +static bool Color_Temperature_Default_Fade_Time_Write(uint32_t object_instance, + BACNET_UNSIGNED_INTEGER value, + uint8_t priority, + BACNET_ERROR_CLASS *error_class, + BACNET_ERROR_CODE *error_code) +{ + bool status = false; + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + (void)priority; + if (pObject->Write_Enabled) { + if ((value == 0) || + ((value >= BACNET_COLOR_FADE_TIME_MIN) && + (value <= BACNET_COLOR_FADE_TIME_MAX))) { + pObject->Default_Fade_Time = value; + status = true; + } else { + *error_class = ERROR_CLASS_PROPERTY; + *error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; + } + } else { + *error_class = ERROR_CLASS_PROPERTY; + *error_code = ERROR_CODE_WRITE_ACCESS_DENIED; + } + } else { + *error_class = ERROR_CLASS_OBJECT; + *error_code = ERROR_CODE_UNKNOWN_OBJECT; + } + + return status; +} + /** * For a given object instance-number, gets the property value * @@ -592,6 +694,51 @@ bool Color_Temperature_Default_Ramp_Rate_Set( return status; } +/** + * Handle a WriteProperty to a specific property. + * + * @param object_instance - object-instance number of the object + * @param value - property value to be written + * @param priority - priority-array index value 1..16 + * @param error_class - the BACnet error class + * @param error_code - BACnet Error code + * + * @return true if values are within range and present-value is set. + */ +static bool Color_Temperature_Default_Ramp_Rate_Write(uint32_t object_instance, + BACNET_UNSIGNED_INTEGER value, + uint8_t priority, + BACNET_ERROR_CLASS *error_class, + BACNET_ERROR_CODE *error_code) +{ + bool status = false; + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + (void)priority; + if (pObject->Write_Enabled) { + if ((value == 0) || + ((value >= BACNET_COLOR_RAMP_RATE_MIN) && + (value <= BACNET_COLOR_RAMP_RATE_MAX))) { + pObject->Default_Fade_Time = value; + status = true; + } else { + *error_class = ERROR_CLASS_PROPERTY; + *error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; + } + } else { + *error_class = ERROR_CLASS_PROPERTY; + *error_code = ERROR_CODE_WRITE_ACCESS_DENIED; + } + } else { + *error_class = ERROR_CLASS_OBJECT; + *error_code = ERROR_CODE_UNKNOWN_OBJECT; + } + + return status; +} + /** * For a given object instance-number, gets the property value * @@ -633,6 +780,52 @@ bool Color_Temperature_Default_Step_Increment_Set( return status; } +/** + * Handle a WriteProperty to a specific property. + * + * @param object_instance - object-instance number of the object + * @param value - property value to be written + * @param priority - priority-array index value 1..16 + * @param error_class - the BACnet error class + * @param error_code - BACnet Error code + * + * @return true if values are within range and present-value is set. + */ +static bool Color_Temperature_Default_Step_Increment_Write( + uint32_t object_instance, + BACNET_UNSIGNED_INTEGER value, + uint8_t priority, + BACNET_ERROR_CLASS *error_class, + BACNET_ERROR_CODE *error_code) +{ + bool status = false; + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + (void)priority; + if (pObject->Write_Enabled) { + if ((value == 0) || + ((value >= BACNET_COLOR_STEP_INCREMENT_MIN) && + (value <= BACNET_COLOR_STEP_INCREMENT_MAX))) { + pObject->Default_Fade_Time = value; + status = true; + } else { + *error_class = ERROR_CLASS_PROPERTY; + *error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; + } + } else { + *error_class = ERROR_CLASS_PROPERTY; + *error_code = ERROR_CODE_WRITE_ACCESS_DENIED; + } + } else { + *error_class = ERROR_CLASS_OBJECT; + *error_code = ERROR_CODE_UNKNOWN_OBJECT; + } + + return status; +} + /** * For a given object instance-number, gets the property value * @@ -676,6 +869,49 @@ bool Color_Temperature_Transition_Set( return status; } +/** + * Handle a WriteProperty to a specific property. + * + * @param object_instance - object-instance number of the object + * @param value - property value to be written + * @param priority - priority-array index value 1..16 + * @param error_class - the BACnet error class + * @param error_code - BACnet Error code + * + * @return true if values are within range and present-value is set. + */ +static bool Color_Transition_Write(uint32_t object_instance, + uint32_t value, + uint8_t priority, + BACNET_ERROR_CLASS *error_class, + BACNET_ERROR_CODE *error_code) +{ + bool status = false; + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + (void)priority; + if (pObject->Write_Enabled) { + if (value < BACNET_COLOR_TRANSITION_MAX) { + pObject->Transition = value; + status = true; + } else { + *error_class = ERROR_CLASS_PROPERTY; + *error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; + } + } else { + *error_class = ERROR_CLASS_PROPERTY; + *error_code = ERROR_CODE_WRITE_ACCESS_DENIED; + } + } else { + *error_class = ERROR_CLASS_OBJECT; + *error_code = ERROR_CODE_UNKNOWN_OBJECT; + } + + return status; +} + /** * For a given object instance-number, loads the object-name into * a characterstring. Note that the object name must be unique @@ -795,6 +1031,265 @@ bool Color_Temperature_Description_Set(uint32_t object_instance, char *new_name) return status; } +/** + * Updates the color object tracking value while fading + * + * The fade operation changes the output color temperature + * from its current value to target-color-temperature, over + * a period of time defined by fade-time. While the fade + * operation is executing, In_Progress shall be set to FADE_ACTIVE, + * and Tracking_Value shall be updated to reflect the current + * progress of the fade. shall be clamped + * to Min_Pres_Value and Max_Pres_Value + * + * @param object_instance - object-instance number of the object + * @param milliseconds - number of milliseconds elapsed + */ +static void Color_Temperature_Fade_To_CCT_Handler( + uint32_t object_instance, uint16_t milliseconds) +{ + uint32_t old_value, target_value, min_value, max_value; + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (!pObject) { + return; + } + old_value = pObject->Tracking_Value; + min_value = pObject->Present_Value_Minimum; + max_value = pObject->Present_Value_Maximum; + target_value = pObject->Color_Command.target.color_temperature; + /* clamp target within min/max, if needed */ + if (target_value > max_value) { + target_value = max_value; + } + if (target_value < min_value) { + target_value = min_value; + } + if (milliseconds >= pObject->Color_Command.transit.fade_time) { + /* done fading */ + pObject->Tracking_Value = target_value; + pObject->In_Progress = BACNET_COLOR_OPERATION_IN_PROGRESS_IDLE; + pObject->Color_Command.operation = BACNET_COLOR_OPERATION_STOP; + pObject->Color_Command.transit.fade_time = 0; + } else { + if (old_value == target_value) { + /* stop fading */ + pObject->Tracking_Value = target_value; + pObject->In_Progress = BACNET_COLOR_OPERATION_IN_PROGRESS_IDLE; + pObject->Color_Command.operation = BACNET_COLOR_OPERATION_STOP; + pObject->Color_Command.transit.fade_time = 0; + } else { + /* fading */ + pObject->Tracking_Value = linear_interpolate_int(0, milliseconds, + pObject->Color_Command.transit.fade_time, old_value, + target_value); + pObject->Color_Command.transit.fade_time -= milliseconds; + pObject->In_Progress = + BACNET_COLOR_OPERATION_IN_PROGRESS_FADE_ACTIVE; + } + } + if (Color_Temperature_Write_Present_Value_Callback) { + Color_Temperature_Write_Present_Value_Callback( + object_instance, old_value, pObject->Tracking_Value); + } +} + +/** + * Updates the color object tracking value while ramping + * + * Commands Present_Value to ramp from the current Tracking_Value to the + * target-color-temperature specified in the command. The ramp operation + * changes the output color temperature from its current value to + * target-color-temperature, at a particular Kelvin per second defined by + * ramp-rate. While the ramp operation is executing, In_Progress shall be set + * to RAMP_ACTIVE, and Tracking_Value shall be updated to reflect the current + * progress of the fade. shall be clamped to + * Min_Pres_Value and Max_Pres_Value + * + * @param object_instance - object-instance number of the object + * @param milliseconds - number of milliseconds elapsed + */ +static void Color_Temperature_Ramp_To_CCT_Handler( + uint32_t object_instance, uint16_t milliseconds) +{ + uint16_t old_value, target_value, min_value, max_value, step_value, steps; + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (!pObject) { + return; + } + old_value = pObject->Tracking_Value; + min_value = pObject->Present_Value_Minimum; + max_value = pObject->Present_Value_Maximum; + target_value = pObject->Color_Command.target.color_temperature; + /* clamp target within min/max, if needed */ + if (target_value > max_value) { + target_value = max_value; + } + if (target_value < min_value) { + target_value = min_value; + } + /* determine the number of K steps */ + if (milliseconds <= 1000) { + /* K per second */ + steps = linear_interpolate_int( + 0, milliseconds, 1000, 0, pObject->Color_Command.transit.ramp_rate); + } else { + steps = + (milliseconds * pObject->Color_Command.transit.ramp_rate) / 1000; + } + if (old_value == target_value) { + pObject->Tracking_Value = target_value; + pObject->In_Progress = BACNET_COLOR_OPERATION_IN_PROGRESS_IDLE; + pObject->Color_Command.operation = BACNET_COLOR_OPERATION_STOP; + } else { + if (old_value < target_value) { + step_value = old_value + steps; + } else if (old_value > target_value) { + if (steps > old_value) { + step_value = old_value - steps; + } else { + step_value = target_value; + } + } else { + step_value = target_value; + } + /* clamp target within min/max, if needed */ + if (step_value > max_value) { + step_value = max_value; + } + if (step_value < min_value) { + step_value = min_value; + } + pObject->Tracking_Value = step_value; + pObject->In_Progress = BACNET_COLOR_OPERATION_IN_PROGRESS_FADE_ACTIVE; + } + if (Color_Temperature_Write_Present_Value_Callback) { + Color_Temperature_Write_Present_Value_Callback( + object_instance, old_value, pObject->Tracking_Value); + } +} + +/** + * Updates the color object tracking value while stepping + * + * Commands Present_Value to a value equal to the Tracking_Value + * plus the step-increment. The resulting sum shall be clamped to + * Min_Pres_Value and Max_Pres_Value + * + * @param object_instance - object-instance number of the object + */ +static void Color_Temperature_Step_Up_CCT_Handler(uint32_t object_instance) +{ + uint16_t old_value, target_value, min_value, max_value, step_value; + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (!pObject) { + return; + } + old_value = target_value = pObject->Tracking_Value; + min_value = pObject->Present_Value_Minimum; + max_value = pObject->Present_Value_Maximum; + step_value = pObject->Color_Command.transit.step_increment; + target_value += step_value; + /* clamp target within min/max, if needed */ + if (target_value > max_value) { + target_value = max_value; + } + if (target_value < min_value) { + target_value = min_value; + } + pObject->Present_Value = target_value; + pObject->Tracking_Value = target_value; + pObject->In_Progress = BACNET_COLOR_OPERATION_IN_PROGRESS_IDLE; + if (Color_Temperature_Write_Present_Value_Callback) { + Color_Temperature_Write_Present_Value_Callback( + object_instance, old_value, pObject->Tracking_Value); + } +} + +/** + * Updates the color object tracking value while stepping + * + * Commands Present_Value to a value equal to the Tracking_Value + * plus the step-increment. The resulting sum shall be clamped to + * Min_Pres_Value and Max_Pres_Value + * + * @param object_instance - object-instance number of the object + */ +static void Color_Temperature_Step_Down_CCT_Handler(uint32_t object_instance) +{ + uint16_t old_value, target_value, min_value, max_value, step_value; + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (!pObject) { + return; + } + old_value = target_value = pObject->Tracking_Value; + min_value = pObject->Present_Value_Minimum; + max_value = pObject->Present_Value_Maximum; + step_value = pObject->Color_Command.transit.step_increment; + if (target_value >= step_value) { + target_value -= step_value; + } else { + target_value = 0; + } + /* clamp target within min/max, if needed */ + if (target_value > max_value) { + target_value = max_value; + } + if (target_value < min_value) { + target_value = min_value; + } + pObject->Present_Value = target_value; + pObject->Tracking_Value = target_value; + pObject->In_Progress = BACNET_COLOR_OPERATION_IN_PROGRESS_IDLE; + if (Color_Temperature_Write_Present_Value_Callback) { + Color_Temperature_Write_Present_Value_Callback( + object_instance, old_value, pObject->Tracking_Value); + } +} + +/** + * Updates the color temperature tracking value per ramp or fade + * + * @param object_instance - object-instance number of the object + * @param milliseconds - number of milliseconds elapsed + */ +void Color_Temperature_Timer(uint32_t object_instance, uint16_t milliseconds) +{ + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + switch (pObject->Color_Command.operation) { + case BACNET_COLOR_OPERATION_FADE_TO_CCT: + Color_Temperature_Fade_To_CCT_Handler( + object_instance, milliseconds); + break; + case BACNET_COLOR_OPERATION_RAMP_TO_CCT: + Color_Temperature_Ramp_To_CCT_Handler( + object_instance, milliseconds); + break; + case BACNET_COLOR_OPERATION_STEP_UP_CCT: + Color_Temperature_Step_Up_CCT_Handler(object_instance); + break; + case BACNET_COLOR_OPERATION_STEP_DOWN_CCT: + Color_Temperature_Step_Down_CCT_Handler(object_instance); + break; + case BACNET_COLOR_OPERATION_NONE: + case BACNET_COLOR_OPERATION_STOP: + default: + pObject->In_Progress = BACNET_COLOR_OPERATION_IN_PROGRESS_IDLE; + break; + } + } +} + /** * ReadProperty handler for this object. For the given ReadProperty * data, the application_data is loaded or the error flags are set. @@ -918,11 +1413,14 @@ bool Color_Temperature_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) bool status = false; /* return value */ int len = 0; BACNET_APPLICATION_DATA_VALUE value; + int apdu_size = 0; + uint8_t *apdu = NULL; /* decode the some of the request */ - len = bacapp_decode_application_data( - wp_data->application_data, wp_data->application_data_len, &value); - /* FIXME: len < application_data_len: more data? */ + apdu = wp_data->application_data; + apdu_size = wp_data->application_data_len; + len = bacapp_decode_known_property(apdu, apdu_size, &value, + wp_data->object_type, wp_data->object_property); if (len < 0) { /* error while decoding - a value larger than we can handle */ wp_data->error_class = ERROR_CLASS_PROPERTY; @@ -948,10 +1446,64 @@ bool Color_Temperature_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) &wp_data->error_code); } break; + case PROP_DEFAULT_COLOR_TEMPERATURE: + status = write_property_type_valid( + wp_data, &value, BACNET_APPLICATION_TAG_UNSIGNED_INT); + if (status) { + status = + Color_Temperature_Default_Write(wp_data->object_instance, + value.type.Unsigned_Int, wp_data->priority, + &wp_data->error_class, &wp_data->error_code); + } + break; + case PROP_DEFAULT_FADE_TIME: + status = write_property_type_valid( + wp_data, &value, BACNET_APPLICATION_TAG_UNSIGNED_INT); + if (status) { + status = Color_Temperature_Default_Fade_Time_Write( + wp_data->object_instance, value.type.Unsigned_Int, + wp_data->priority, &wp_data->error_class, + &wp_data->error_code); + } + break; + case PROP_TRANSITION: + status = write_property_type_valid( + wp_data, &value, BACNET_APPLICATION_TAG_ENUMERATED); + if (status) { + status = Color_Transition_Write(wp_data->object_instance, + value.type.Enumerated, wp_data->priority, + &wp_data->error_class, &wp_data->error_code); + } + break; + case PROP_DEFAULT_RAMP_RATE: + status = write_property_type_valid( + wp_data, &value, BACNET_APPLICATION_TAG_UNSIGNED_INT); + if (status) { + status = Color_Temperature_Default_Ramp_Rate_Write( + wp_data->object_instance, value.type.Unsigned_Int, + wp_data->priority, &wp_data->error_class, + &wp_data->error_code); + } + break; + case PROP_DEFAULT_STEP_INCREMENT: + status = write_property_type_valid( + wp_data, &value, BACNET_APPLICATION_TAG_UNSIGNED_INT); + if (status) { + status = Color_Temperature_Default_Step_Increment_Write( + wp_data->object_instance, value.type.Unsigned_Int, + wp_data->priority, &wp_data->error_class, + &wp_data->error_code); + } + break; case PROP_OBJECT_IDENTIFIER: case PROP_OBJECT_TYPE: case PROP_OBJECT_NAME: case PROP_DESCRIPTION: + case PROP_TRACKING_VALUE: + case PROP_COLOR_COMMAND: + case PROP_IN_PROGRESS: + case PROP_MAX_PRES_VALUE: + case PROP_MIN_PRES_VALUE: wp_data->error_class = ERROR_CLASS_PROPERTY; wp_data->error_code = ERROR_CODE_WRITE_ACCESS_DENIED; break; @@ -1034,9 +1586,9 @@ uint32_t Color_Temperature_Create(uint32_t object_instance) return BACNET_MAX_INSTANCE; } else if (object_instance == BACNET_MAX_INSTANCE) { /* wildcard instance */ - /* the Object_Identifier property of the newly created object - shall be initialized to a value that is unique within the - responding BACnet-user device. The method used to generate + /* the Object_Identifier property of the newly created object + shall be initialized to a value that is unique within the + responding BACnet-user device. The method used to generate the object identifier is a local matter.*/ object_instance = Keylist_Next_Empty_Key(Object_List, 1); } @@ -1047,22 +1599,26 @@ uint32_t Color_Temperature_Create(uint32_t object_instance) pObject->Object_Name = NULL; pObject->Present_Value = 0; pObject->Tracking_Value = 0; - pObject->Color_Command.operation = BACNET_COLOR_OPERATION_NONE; pObject->In_Progress = BACNET_COLOR_OPERATION_IN_PROGRESS_IDLE; pObject->Default_Color_Temperature = 5000; - pObject->Default_Fade_Time = 0; - pObject->Default_Ramp_Rate = 0; - pObject->Default_Step_Increment = 0; - pObject->Transition = BACNET_COLOR_TRANSITION_NONE; - pObject->Present_Value_Minimum = 0; - pObject->Present_Value_Maximum = 0; + pObject->Default_Fade_Time = BACNET_COLOR_FADE_TIME_MIN; + pObject->Default_Ramp_Rate = BACNET_COLOR_RAMP_RATE_MIN; + pObject->Default_Step_Increment = BACNET_COLOR_STEP_INCREMENT_MIN; + pObject->Transition = BACNET_COLOR_TRANSITION_FADE; + pObject->Present_Value_Minimum = BACNET_COLOR_TEMPERATURE_MIN; + pObject->Present_Value_Maximum = BACNET_COLOR_TEMPERATURE_MAX; + /* configure to transition from power up values */ + pObject->Color_Command.operation = + BACNET_COLOR_OPERATION_FADE_TO_CCT; + pObject->Color_Command.transit.fade_time = + pObject->Default_Fade_Time; + pObject->Color_Command.target.color_temperature = + pObject->Default_Color_Temperature; pObject->Changed = false; pObject->Write_Enabled = false; /* add to list */ index = Keylist_Data_Add(Object_List, object_instance, pObject); - if (index >= 0) { - Device_Inc_Database_Revision(); - } else { + if (index < 0) { free(pObject); return BACNET_MAX_INSTANCE; } @@ -1088,7 +1644,6 @@ bool Color_Temperature_Delete(uint32_t object_instance) if (pObject) { free(pObject); status = true; - Device_Inc_Database_Revision(); } return status; @@ -1106,7 +1661,6 @@ void Color_Temperature_Cleanup(void) pObject = Keylist_Data_Pop(Object_List); if (pObject) { free(pObject); - Device_Inc_Database_Revision(); } } while (pObject); Keylist_Delete(Object_List); @@ -1120,7 +1674,4 @@ void Color_Temperature_Cleanup(void) void Color_Temperature_Init(void) { Object_List = Keylist_Create(); - if (Object_List) { - atexit(Color_Temperature_Cleanup); - } } diff --git a/src/bacnet/basic/object/color_temperature.h b/src/bacnet/basic/object/color_temperature.h index accc519e..0ceb6ad6 100644 --- a/src/bacnet/basic/object/color_temperature.h +++ b/src/bacnet/basic/object/color_temperature.h @@ -29,7 +29,7 @@ #include "bacnet/wp.h" /** - * @brief Callback for gateway write present value request + * @brief Callback for write present value request * @param object_instance - object-instance number of the object * @param old_value - 32-bit value prior to write * @param value - 32-bit value of the write @@ -150,6 +150,9 @@ void Color_Temperature_Write_Enable(uint32_t instance); BACNET_STACK_EXPORT void Color_Temperature_Write_Disable(uint32_t instance); +BACNET_STACK_EXPORT +void Color_Temperature_Timer(uint32_t object_instance, uint16_t milliseconds); + BACNET_STACK_EXPORT uint32_t Color_Temperature_Create(uint32_t object_instance); BACNET_STACK_EXPORT diff --git a/src/bacnet/basic/object/device.c b/src/bacnet/basic/object/device.c index 1e4143ad..1e125069 100644 --- a/src/bacnet/basic/object/device.c +++ b/src/bacnet/basic/object/device.c @@ -99,7 +99,7 @@ static object_functions_t My_Object_Table[] = { NULL /* Value_Lists */, NULL /* COV */, NULL /* COV Clear */, NULL /* Intrinsic Reporting */, NULL /* Add_List_Element */, NULL /* Remove_List_Element */, - NULL /* Create */, NULL /* Delete */ }, + NULL /* Create */, NULL /* Delete */, NULL /* Timer */ }, #if (BACNET_PROTOCOL_REVISION >= 17) { OBJECT_NETWORK_PORT, Network_Port_Init, Network_Port_Count, Network_Port_Index_To_Instance, Network_Port_Valid_Instance, @@ -108,7 +108,7 @@ static object_functions_t My_Object_Table[] = { NULL /* ReadRangeInfo */, NULL /* Iterator */, NULL /* Value_Lists */, NULL /* COV */, NULL /* COV Clear */, NULL /* Intrinsic Reporting */, NULL /* Add_List_Element */, NULL /* Remove_List_Element */, - NULL /* Create */, NULL /* Delete */ }, + NULL /* Create */, NULL /* Delete */ , NULL /* Timer */}, #endif { OBJECT_ANALOG_INPUT, Analog_Input_Init, Analog_Input_Count, Analog_Input_Index_To_Instance, Analog_Input_Valid_Instance, @@ -118,7 +118,7 @@ static object_functions_t My_Object_Table[] = { Analog_Input_Encode_Value_List, Analog_Input_Change_Of_Value, Analog_Input_Change_Of_Value_Clear, Analog_Input_Intrinsic_Reporting, NULL /* Add_List_Element */, NULL /* Remove_List_Element */, - NULL /* Create */, NULL /* Delete */ }, + NULL /* Create */, NULL /* Delete */ , NULL /* Timer */}, { OBJECT_ANALOG_OUTPUT, Analog_Output_Init, Analog_Output_Count, Analog_Output_Index_To_Instance, Analog_Output_Valid_Instance, Analog_Output_Object_Name, Analog_Output_Read_Property, @@ -126,7 +126,7 @@ static object_functions_t My_Object_Table[] = { NULL /* ReadRangeInfo */, NULL /* Iterator */, NULL /* Value_Lists */, NULL /* COV */, NULL /* COV Clear */, NULL /* Intrinsic Reporting */, NULL /* Add_List_Element */, NULL /* Remove_List_Element */, - Analog_Output_Create, Analog_Output_Delete}, + Analog_Output_Create, Analog_Output_Delete, NULL /* Timer */}, { OBJECT_ANALOG_VALUE, Analog_Value_Init, Analog_Value_Count, Analog_Value_Index_To_Instance, Analog_Value_Valid_Instance, Analog_Value_Object_Name, Analog_Value_Read_Property, @@ -135,7 +135,7 @@ static object_functions_t My_Object_Table[] = { Analog_Value_Encode_Value_List, Analog_Value_Change_Of_Value, Analog_Value_Change_Of_Value_Clear, Analog_Value_Intrinsic_Reporting, NULL /* Add_List_Element */, NULL /* Remove_List_Element */, - NULL /* Create */, NULL /* Delete */ }, + NULL /* Create */, NULL /* Delete */, NULL /* Timer */ }, { OBJECT_BINARY_INPUT, Binary_Input_Init, Binary_Input_Count, Binary_Input_Index_To_Instance, Binary_Input_Valid_Instance, Binary_Input_Object_Name, Binary_Input_Read_Property, @@ -144,7 +144,7 @@ static object_functions_t My_Object_Table[] = { Binary_Input_Encode_Value_List, Binary_Input_Change_Of_Value, Binary_Input_Change_Of_Value_Clear, NULL /* Intrinsic Reporting */, NULL /* Add_List_Element */, NULL /* Remove_List_Element */, - NULL /* Create */, NULL /* Delete */ }, + NULL /* Create */, NULL /* Delete */, NULL /* Timer */ }, { OBJECT_BINARY_OUTPUT, Binary_Output_Init, Binary_Output_Count, Binary_Output_Index_To_Instance, Binary_Output_Valid_Instance, Binary_Output_Object_Name, Binary_Output_Read_Property, @@ -152,7 +152,7 @@ static object_functions_t My_Object_Table[] = { NULL /* ReadRangeInfo */, NULL /* Iterator */, NULL /* Value_Lists */, NULL /* COV */, NULL /* COV Clear */, NULL /* Intrinsic Reporting */, NULL /* Add_List_Element */, NULL /* Remove_List_Element */, - Binary_Output_Create, Binary_Output_Delete}, + Binary_Output_Create, Binary_Output_Delete, NULL /* Timer */}, { OBJECT_BINARY_VALUE, Binary_Value_Init, Binary_Value_Count, Binary_Value_Index_To_Instance, Binary_Value_Valid_Instance, Binary_Value_Object_Name, Binary_Value_Read_Property, @@ -160,7 +160,7 @@ static object_functions_t My_Object_Table[] = { NULL /* ReadRangeInfo */, NULL /* Iterator */, NULL /* Value_Lists */, NULL /* COV */, NULL /* COV Clear */, NULL /* Intrinsic Reporting */, NULL /* Add_List_Element */, NULL /* Remove_List_Element */, - NULL /* Create */, NULL /* Delete */ }, + NULL /* Create */, NULL /* Delete */, NULL /* Timer */ }, { OBJECT_CHARACTERSTRING_VALUE, CharacterString_Value_Init, CharacterString_Value_Count, CharacterString_Value_Index_To_Instance, CharacterString_Value_Valid_Instance, CharacterString_Value_Object_Name, @@ -172,14 +172,14 @@ static object_functions_t My_Object_Table[] = { CharacterString_Value_Change_Of_Value_Clear, NULL /* Intrinsic Reporting */, NULL /* Add_List_Element */, NULL /* Remove_List_Element */, - NULL /* Create */, NULL /* Delete */ }, + NULL /* Create */, NULL /* Delete */, NULL /* Timer */ }, { OBJECT_COMMAND, Command_Init, Command_Count, Command_Index_To_Instance, Command_Valid_Instance, Command_Object_Name, Command_Read_Property, Command_Write_Property, Command_Property_Lists, NULL /* ReadRangeInfo */, NULL /* Iterator */, NULL /* Value_Lists */, NULL /* COV */, NULL /* COV Clear */, NULL /* Intrinsic Reporting */, NULL /* Add_List_Element */, NULL /* Remove_List_Element */, - NULL /* Create */, NULL /* Delete */ }, + NULL /* Create */, NULL /* Delete */, NULL /* Timer */ }, { OBJECT_INTEGER_VALUE, Integer_Value_Init, Integer_Value_Count, Integer_Value_Index_To_Instance, Integer_Value_Valid_Instance, Integer_Value_Object_Name, Integer_Value_Read_Property, @@ -187,7 +187,7 @@ static object_functions_t My_Object_Table[] = { NULL /* ReadRangeInfo */, NULL /* Iterator */, NULL /* Value_Lists */, NULL /* COV */, NULL /* COV Clear */, NULL /* Intrinsic Reporting */, NULL /* Add_List_Element */, NULL /* Remove_List_Element */, - NULL /* Create */, NULL /* Delete */ }, + NULL /* Create */, NULL /* Delete */, NULL /* Timer */ }, #if defined(INTRINSIC_REPORTING) { OBJECT_NOTIFICATION_CLASS, Notification_Class_Init, Notification_Class_Count, Notification_Class_Index_To_Instance, @@ -198,7 +198,7 @@ static object_functions_t My_Object_Table[] = { NULL /* COV Clear */, NULL /* Intrinsic Reporting */, Notification_Class_Add_List_Element, Notification_Class_Remove_List_Element, - NULL /* Create */, NULL /* Delete */ }, + NULL /* Create */, NULL /* Delete */, NULL /* Timer */ }, #endif { OBJECT_LIFE_SAFETY_POINT, Life_Safety_Point_Init, Life_Safety_Point_Count, Life_Safety_Point_Index_To_Instance, Life_Safety_Point_Valid_Instance, @@ -207,7 +207,7 @@ static object_functions_t My_Object_Table[] = { NULL /* ReadRangeInfo */, NULL /* Iterator */, NULL /* Value_Lists */, NULL /* COV */, NULL /* COV Clear */, NULL /* Intrinsic Reporting */, NULL /* Add_List_Element */, NULL /* Remove_List_Element */, - NULL /* Create */, NULL /* Delete */ }, + NULL /* Create */, NULL /* Delete */ , NULL /* Timer */}, { OBJECT_LOAD_CONTROL, Load_Control_Init, Load_Control_Count, Load_Control_Index_To_Instance, Load_Control_Valid_Instance, Load_Control_Object_Name, Load_Control_Read_Property, @@ -215,7 +215,7 @@ static object_functions_t My_Object_Table[] = { NULL /* ReadRangeInfo */, NULL /* Iterator */, NULL /* Value_Lists */, NULL /* COV */, NULL /* COV Clear */, NULL /* Intrinsic Reporting */, NULL /* Add_List_Element */, NULL /* Remove_List_Element */, - NULL /* Create */, NULL /* Delete */ }, + NULL /* Create */, NULL /* Delete */ , NULL /* Timer */}, { OBJECT_MULTI_STATE_INPUT, Multistate_Input_Init, Multistate_Input_Count, Multistate_Input_Index_To_Instance, Multistate_Input_Valid_Instance, Multistate_Input_Object_Name, Multistate_Input_Read_Property, @@ -223,7 +223,7 @@ static object_functions_t My_Object_Table[] = { NULL /* ReadRangeInfo */, NULL /* Iterator */, NULL /* Value_Lists */, NULL /* COV */, NULL /* COV Clear */, NULL /* Intrinsic Reporting */, NULL /* Add_List_Element */, NULL /* Remove_List_Element */, - NULL /* Create */, NULL /* Delete */ }, + NULL /* Create */, NULL /* Delete */ , NULL /* Timer */}, { OBJECT_MULTI_STATE_OUTPUT, Multistate_Output_Init, Multistate_Output_Count, Multistate_Output_Index_To_Instance, Multistate_Output_Valid_Instance, Multistate_Output_Object_Name, @@ -232,7 +232,7 @@ static object_functions_t My_Object_Table[] = { NULL /* Iterator */, NULL /* Value_Lists */, NULL /* COV */, NULL /* COV Clear */, NULL /* Intrinsic Reporting */, NULL /* Add_List_Element */, NULL /* Remove_List_Element */, - Multistate_Output_Create, Multistate_Output_Delete}, + Multistate_Output_Create, Multistate_Output_Delete, NULL /* Timer */}, { OBJECT_MULTI_STATE_VALUE, Multistate_Value_Init, Multistate_Value_Count, Multistate_Value_Index_To_Instance, Multistate_Value_Valid_Instance, Multistate_Value_Object_Name, Multistate_Value_Read_Property, @@ -241,7 +241,7 @@ static object_functions_t My_Object_Table[] = { Multistate_Value_Encode_Value_List, Multistate_Value_Change_Of_Value, Multistate_Value_Change_Of_Value_Clear, NULL /* Intrinsic Reporting */, NULL /* Add_List_Element */, NULL /* Remove_List_Element */, - NULL /* Create */, NULL /* Delete */ }, + NULL /* Create */, NULL /* Delete */ , NULL /* Timer */}, { OBJECT_TRENDLOG, Trend_Log_Init, Trend_Log_Count, Trend_Log_Index_To_Instance, Trend_Log_Valid_Instance, Trend_Log_Object_Name, Trend_Log_Read_Property, @@ -249,7 +249,7 @@ static object_functions_t My_Object_Table[] = { NULL /* Iterator */, NULL /* Value_Lists */, NULL /* COV */, NULL /* COV Clear */, NULL /* Intrinsic Reporting */, NULL /* Add_List_Element */, NULL /* Remove_List_Element */, - NULL /* Create */, NULL /* Delete */ }, + NULL /* Create */, NULL /* Delete */ , NULL /* Timer */}, #if (BACNET_PROTOCOL_REVISION >= 14) { OBJECT_LIGHTING_OUTPUT, Lighting_Output_Init, Lighting_Output_Count, Lighting_Output_Index_To_Instance, Lighting_Output_Valid_Instance, @@ -258,14 +258,14 @@ static object_functions_t My_Object_Table[] = { NULL /* ReadRangeInfo */, NULL /* Iterator */, NULL /* Value_Lists */, NULL /* COV */, NULL /* COV Clear */, NULL /* Intrinsic Reporting */, NULL /* Add_List_Element */, NULL /* Remove_List_Element */, - NULL /* Create */, NULL /* Delete */ }, + Lighting_Output_Create, Lighting_Output_Delete, Lighting_Output_Timer}, { OBJECT_CHANNEL, Channel_Init, Channel_Count, Channel_Index_To_Instance, Channel_Valid_Instance, Channel_Object_Name, Channel_Read_Property, Channel_Write_Property, Channel_Property_Lists, NULL /* ReadRangeInfo */, NULL /* Iterator */, NULL /* Value_Lists */, NULL /* COV */, NULL /* COV Clear */, NULL /* Intrinsic Reporting */, NULL /* Add_List_Element */, NULL /* Remove_List_Element */, - NULL /* Create */, NULL /* Delete */ }, + Channel_Create, Channel_Delete, NULL /* Timer */ }, #endif #if (BACNET_PROTOCOL_REVISION >= 24) { OBJECT_COLOR, Color_Init, Color_Count, Color_Index_To_Instance, @@ -274,7 +274,7 @@ static object_functions_t My_Object_Table[] = { NULL /* Iterator */, NULL /* Value_Lists */, NULL /* COV */, NULL /* COV Clear */, NULL /* Intrinsic Reporting */, NULL /* Add_List_Element */, NULL /* Remove_List_Element */, - Color_Create, Color_Delete}, + Color_Create, Color_Delete, Color_Timer}, { OBJECT_COLOR_TEMPERATURE, Color_Temperature_Init, Color_Temperature_Count, Color_Temperature_Index_To_Instance, Color_Temperature_Valid_Instance, Color_Temperature_Object_Name, Color_Temperature_Read_Property, @@ -282,7 +282,8 @@ static object_functions_t My_Object_Table[] = { NULL /* ReadRangeInfo */, NULL /* Iterator */, NULL /* Value_Lists */, NULL /* COV */, NULL /* COV Clear */, NULL /* Intrinsic Reporting */, NULL /* Add_List_Element */, NULL /* Remove_List_Element */, - Color_Temperature_Create, Color_Temperature_Delete}, + Color_Temperature_Create, Color_Temperature_Delete, + Color_Temperature_Timer}, #endif #if defined(BACFILE) { OBJECT_FILE, bacfile_init, bacfile_count, bacfile_index_to_instance, @@ -291,7 +292,7 @@ static object_functions_t My_Object_Table[] = { NULL /* ReadRangeInfo */, NULL /* Iterator */, NULL /* Value_Lists */, NULL /* COV */, NULL /* COV Clear */, NULL /* Intrinsic Reporting */, NULL /* Add_List_Element */, NULL /* Remove_List_Element */, - bacfile_create, bacfile_delete}, + bacfile_create, bacfile_delete, NULL /* Timer */}, #endif { OBJECT_OCTETSTRING_VALUE, OctetString_Value_Init, OctetString_Value_Count, OctetString_Value_Index_To_Instance, OctetString_Value_Valid_Instance, @@ -300,7 +301,7 @@ static object_functions_t My_Object_Table[] = { NULL /* ReadRangeInfo */, NULL /* Iterator */, NULL /* Value_Lists */, NULL /* COV */, NULL /* COV Clear */, NULL /* Intrinsic Reporting */, NULL /* Add_List_Element */, NULL /* Remove_List_Element */, - NULL /* Create */, NULL /* Delete */ }, + NULL /* Create */, NULL /* Delete */, NULL /* Timer */ }, { OBJECT_POSITIVE_INTEGER_VALUE, PositiveInteger_Value_Init, PositiveInteger_Value_Count, PositiveInteger_Value_Index_To_Instance, PositiveInteger_Value_Valid_Instance, PositiveInteger_Value_Object_Name, @@ -310,7 +311,7 @@ static object_functions_t My_Object_Table[] = { NULL /* Iterator */, NULL /* Value_Lists */, NULL /* COV */, NULL /* COV Clear */, NULL /* Intrinsic Reporting */, NULL /* Add_List_Element */, NULL /* Remove_List_Element */, - NULL /* Create */, NULL /* Delete */ }, + NULL /* Create */, NULL /* Delete */, NULL /* Timer */ }, { OBJECT_SCHEDULE, Schedule_Init, Schedule_Count, Schedule_Index_To_Instance, Schedule_Valid_Instance, Schedule_Object_Name, Schedule_Read_Property, Schedule_Write_Property, @@ -318,7 +319,7 @@ static object_functions_t My_Object_Table[] = { NULL /* Value_Lists */, NULL /* COV */, NULL /* COV Clear */, NULL /* Intrinsic Reporting */, NULL /* Add_List_Element */, NULL /* Remove_List_Element */, - NULL /* Create */, NULL /* Delete */ }, + NULL /* Create */, NULL /* Delete */, NULL /* Timer */ }, { OBJECT_ACCUMULATOR, Accumulator_Init, Accumulator_Count, Accumulator_Index_To_Instance, Accumulator_Valid_Instance, Accumulator_Object_Name, Accumulator_Read_Property, @@ -326,7 +327,7 @@ static object_functions_t My_Object_Table[] = { NULL /* ReadRangeInfo */, NULL /* Iterator */, NULL /* Value_Lists */, NULL /* COV */, NULL /* COV Clear */, NULL /* Intrinsic Reporting */, NULL /* Add_List_Element */, NULL /* Remove_List_Element */, - NULL /* Create */, NULL /* Delete */ }, + NULL /* Create */, NULL /* Delete */ , NULL /* Timer */}, { MAX_BACNET_OBJECT_TYPE, NULL /* Init */, NULL /* Count */, NULL /* Index_To_Instance */, NULL /* Valid_Instance */, NULL /* Object_Name */, NULL /* Read_Property */, @@ -334,7 +335,7 @@ static object_functions_t My_Object_Table[] = { NULL /* ReadRangeInfo */, NULL /* Iterator */, NULL /* Value_Lists */, NULL /* COV */, NULL /* COV Clear */, NULL /* Intrinsic Reporting */, NULL /* Add_List_Element */, NULL /* Remove_List_Element */, - NULL /* Create */, NULL /* Delete */ }, + NULL /* Create */, NULL /* Delete */, NULL /* Timer */ }, }; /** Glue function to let the Device object, when called by a handler, @@ -545,7 +546,7 @@ bool Device_Reinitialize(BACNET_REINITIALIZE_DEVICE_DATA *rd_data) bool status = false; /* From 16.4.1.1.2 Password - This optional parameter shall be a CharacterString of up to + This optional parameter shall be a CharacterString of up to 20 characters. For those devices that require the password as a protection, the service request shall be denied if the parameter is absent or if the password is incorrect. For those devices that @@ -554,7 +555,7 @@ bool Device_Reinitialize(BACNET_REINITIALIZE_DEVICE_DATA *rd_data) rd_data->error_class = ERROR_CLASS_SERVICES; rd_data->error_code = ERROR_CODE_PARAMETER_OUT_OF_RANGE; } else if (characterstring_ansi_same(&rd_data->password, Reinit_Password)) { - /* Note: you could use a mix of state and password to + /* Note: you could use a mix of state and password to accomplish multiple things before restarting */ switch (rd_data->state) { case BACNET_REINIT_COLDSTART: @@ -1976,6 +1977,7 @@ bool Device_Create_Object( } else { /* required by ACK */ data->object_instance = object_instance; + Device_Inc_Database_Revision(); status = true; } } @@ -2013,7 +2015,9 @@ bool Device_Delete_Object( pObject->Object_Valid_Instance(data->object_instance)) { /* The object being deleted must already exist */ status = pObject->Object_Delete(data->object_instance); - if (!status) { + if (status) { + Device_Inc_Database_Revision(); + } else { /* The object exists but cannot be deleted. */ data->error_class = ERROR_CLASS_OBJECT; data->error_code = ERROR_CODE_OBJECT_DELETION_NOT_PERMITTED; @@ -2154,6 +2158,34 @@ bool DeviceGetRRInfo(BACNET_READ_RANGE_DATA *pRequest, /* Info on the request */ return status; } +/** + * @brief Updates all the object timers with elapsed milliseconds + * @param milliseconds - number of milliseconds elapsed + */ +void Device_Timer( + uint16_t milliseconds) +{ + struct object_functions *pObject; + unsigned count = 0; + uint32_t instance; + + pObject = Object_Table; + while (pObject->Object_Type < MAX_BACNET_OBJECT_TYPE) { + if (pObject->Object_Count) { + count = pObject->Object_Count(); + } + while (count) { + count--; + if ((pObject->Object_Timer) && + (pObject->Object_Index_To_Instance)) { + instance = pObject->Object_Index_To_Instance(count); + pObject->Object_Timer(instance, milliseconds); + } + } + pObject++; + } +} + #ifdef BAC_ROUTING /**************************************************************************** ************* BACnet Routing Functionality (Optional) ********************** diff --git a/src/bacnet/basic/object/device.h b/src/bacnet/basic/object/device.h index 477522f3..58ba15c7 100644 --- a/src/bacnet/basic/object/device.h +++ b/src/bacnet/basic/object/device.h @@ -142,6 +142,14 @@ typedef void ( *object_intrinsic_reporting_function) ( uint32_t object_instance); +/** + * @brief Updates the object with the elapsed milliseconds + * @param object_instance - object-instance number of the object + * @param milliseconds - number of milliseconds elapsed + */ +typedef void ( + *object_timer_function) ( + uint32_t object_instance, uint16_t milliseconds); /** Defines the group of object helper functions for any supported Object. * @ingroup ObjHelpers @@ -172,6 +180,7 @@ typedef struct object_functions { list_element_function Object_Remove_List_Element; create_object_function Object_Create; delete_object_function Object_Delete; + object_timer_function Object_Timer; } object_functions_t; /* String Lengths - excluding any nul terminator */ @@ -233,6 +242,10 @@ extern "C" { void Device_Init( object_functions_t * object_table); + BACNET_STACK_EXPORT + void Device_Timer( + uint16_t milliseconds); + BACNET_STACK_EXPORT bool Device_Reinitialize( BACNET_REINITIALIZE_DEVICE_DATA * rd_data); diff --git a/src/bacnet/basic/object/lo.c b/src/bacnet/basic/object/lo.c index 75183814..783338de 100644 --- a/src/bacnet/basic/object/lo.c +++ b/src/bacnet/basic/object/lo.c @@ -27,9 +27,12 @@ * */ +#include +#include +#include #include #include -#include +#include #include "bacnet/bacdef.h" #include "bacnet/bacdcode.h" #include "bacnet/bacenum.h" @@ -38,7 +41,10 @@ #include "bacnet/rp.h" #include "bacnet/wp.h" #include "bacnet/lighting.h" +#include "bacnet/basic/object/device.h" #include "bacnet/basic/services.h" +#include "bacnet/basic/sys/keylist.h" +#include "bacnet/basic/sys/linear.h" #include "bacnet/proplist.h" /* me! */ #include "bacnet/basic/object/lo.h" @@ -47,15 +53,12 @@ #define MAX_LIGHTING_OUTPUTS 8 #endif -struct lighting_output_object { +struct object_data { float Present_Value; float Tracking_Value; float Physical_Value; BACNET_LIGHTING_COMMAND Lighting_Command; BACNET_LIGHTING_IN_PROGRESS In_Progress; - bool Out_Of_Service : 1; - bool Blink_Warn_Enable : 1; - bool Egress_Active : 1; uint32_t Egress_Time; uint32_t Default_Fade_Time; float Default_Ramp_Rate; @@ -70,8 +73,21 @@ struct lighting_output_object { float Min_Actual_Value; float Max_Actual_Value; uint8_t Lighting_Command_Default_Priority; + BACNET_OBJECT_ID Color_Reference; + BACNET_OBJECT_ID Override_Color_Reference; + const char *Object_Name; + const char *Description; + /* bits */ + bool Out_Of_Service : 1; + bool Blink_Warn_Enable : 1; + bool Egress_Active : 1; + bool Color_Override : 1; }; -static struct lighting_output_object Lighting_Output[MAX_LIGHTING_OUTPUTS]; +/* Key List for storing the object data sorted by instance number */ +static OS_Keylist Object_List; +/* callback for present value writes */ +static lighting_output_write_present_value_callback + Lighting_Output_Write_Present_Value_Callback; /* These arrays are used by the ReadPropertyMultiple handler and property-list property (as of protocol-revision 14) */ @@ -88,7 +104,12 @@ static const int Lighting_Output_Properties_Required[] = { #endif -1 }; -static const int Lighting_Output_Properties_Optional[] = { -1 }; +static const int Lighting_Output_Properties_Optional[] = { PROP_DESCRIPTION, + PROP_TRANSITION, +#if (BACNET_PROTOCOL_REVISION >= 24) + PROP_COLOR_OVERRIDE, PROP_COLOR_REFERENCE, PROP_OVERRIDE_COLOR_REFERENCE, +#endif + -1 }; static const int Lighting_Output_Properties_Proprietary[] = { -1 }; @@ -128,10 +149,10 @@ void Lighting_Output_Property_Lists( */ bool Lighting_Output_Valid_Instance(uint32_t object_instance) { - unsigned int index; + struct object_data *pObject; - index = Lighting_Output_Instance_To_Index(object_instance); - if (index < MAX_LIGHTING_OUTPUTS) { + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { return true; } @@ -145,7 +166,7 @@ bool Lighting_Output_Valid_Instance(uint32_t object_instance) */ unsigned Lighting_Output_Count(void) { - return MAX_LIGHTING_OUTPUTS; + return Keylist_Count(Object_List); } /** @@ -158,11 +179,7 @@ unsigned Lighting_Output_Count(void) */ uint32_t Lighting_Output_Index_To_Instance(unsigned index) { - uint32_t instance = 1; - - instance += index; - - return instance; + return Keylist_Key(Object_List, index); } /** @@ -176,16 +193,7 @@ uint32_t Lighting_Output_Index_To_Instance(unsigned index) */ unsigned Lighting_Output_Instance_To_Index(uint32_t object_instance) { - unsigned index = MAX_LIGHTING_OUTPUTS; - - if (object_instance) { - index = object_instance - 1; - if (index > MAX_LIGHTING_OUTPUTS) { - index = MAX_LIGHTING_OUTPUTS; - } - } - - return index; + return Keylist_Index(Object_List, object_instance); } /** @@ -198,15 +206,15 @@ unsigned Lighting_Output_Instance_To_Index(uint32_t object_instance) float Lighting_Output_Present_Value(uint32_t object_instance) { float value = 0.0; - unsigned index = 0; unsigned p = 0; + struct object_data *pObject; - index = Lighting_Output_Instance_To_Index(object_instance); - if (index < MAX_LIGHTING_OUTPUTS) { - value = Lighting_Output[index].Relinquish_Default; + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + value = pObject->Relinquish_Default; for (p = 0; p < BACNET_MAX_PRIORITY; p++) { - if (BIT_CHECK(Lighting_Output[index].Priority_Active_Bits, p)) { - value = Lighting_Output[index].Priority_Array[p]; + if (BIT_CHECK(pObject->Priority_Active_Bits, p)) { + value = pObject->Priority_Array[p]; break; } } @@ -215,6 +223,73 @@ float Lighting_Output_Present_Value(uint32_t object_instance) return value; } +/** + * @brief Get the priority-array active status for the specific priority + * @param object [in] BACnet object instance + * @param priority [in] array index requested: + * 0 to N for individual array members + * @return the priority-array active status for the specific priority + */ +static bool Priority_Array_Active( + struct object_data *pObject, BACNET_ARRAY_INDEX priority) +{ + bool active = false; + + if (priority < BACNET_MAX_PRIORITY) { + if (BIT_CHECK(pObject->Priority_Active_Bits, priority)) { + active = true; + } + } + + return active; +} + +/** + * @brief Get the value of the next highest non-NULL priority, including + * Relinquish_Default + * @param object [in] BACnet object instance + * @param priority [in] array index requested: + * 0 to N for individual array members + * @return The priority-array value for the specific priority + */ +static float Priority_Array_Next_Value( + struct object_data *pObject, BACNET_ARRAY_INDEX priority) +{ + float real_value = 0.0; + unsigned p = 0; + + real_value = pObject->Relinquish_Default; + for (p = priority; p < BACNET_MAX_PRIORITY; p++) { + if (Priority_Array_Active(pObject, p)) { + real_value = pObject->Priority_Array[p]; + break; + } + } + + return real_value; +} + +/** + * @brief Get the priority-array value for the specific priority + * @param object [in] BACnet object instance + * @param priority [in] array index requested: + * 0 to N for individual array members + * @return The priority-array value for the specific priority + */ +static float Priority_Array_Value( + struct object_data *pObject, BACNET_ARRAY_INDEX priority) +{ + float real_value = 0.0; + + if (priority < BACNET_MAX_PRIORITY) { + if (BIT_CHECK(pObject->Priority_Active_Bits, priority)) { + real_value = pObject->Priority_Array[priority]; + } + } + + return real_value; +} + /** * @brief Encode a BACnetARRAY property element * @param object_instance [in] BACnet network port object instance number @@ -230,15 +305,17 @@ static int Lighting_Output_Priority_Array_Encode( { int apdu_len = BACNET_STATUS_ERROR; float real_value = 0.0; - unsigned index = 0; + struct object_data *pObject; - index = Lighting_Output_Instance_To_Index(object_instance); - if ((index < MAX_LIGHTING_OUTPUTS) && (priority < BACNET_MAX_PRIORITY)) { - if (BIT_CHECK(Lighting_Output[index].Priority_Active_Bits, priority)) { - real_value = Lighting_Output[index].Priority_Array[priority]; - apdu_len = encode_application_real(apdu, real_value); - } else { - apdu_len = encode_application_null(apdu); + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + if (priority < BACNET_MAX_PRIORITY) { + if (Priority_Array_Active(pObject, priority)) { + real_value = pObject->Priority_Array[priority]; + apdu_len = encode_application_real(apdu, real_value); + } else { + apdu_len = encode_application_null(apdu); + } } } @@ -252,25 +329,92 @@ static int Lighting_Output_Priority_Array_Encode( * * @return active priority 1..16, or 0 if no priority is active */ -unsigned Lighting_Output_Present_Value_Priority(uint32_t object_instance) +static unsigned Present_Value_Priority(struct object_data *pObject) { - unsigned index = 0; /* instance to index conversion */ unsigned p = 0; /* loop counter */ unsigned priority = 0; /* return value */ - index = Lighting_Output_Instance_To_Index(object_instance); - if (index < MAX_LIGHTING_OUTPUTS) { - for (p = 0; p < BACNET_MAX_PRIORITY; p++) { - if (BIT_CHECK(Lighting_Output[index].Priority_Active_Bits, p)) { - priority = p + 1; - break; - } + for (p = 0; p < BACNET_MAX_PRIORITY; p++) { + if (BIT_CHECK(pObject->Priority_Active_Bits, p)) { + priority = p + 1; + break; } } return priority; } +/** + * For a given object instance, relinquishes the present-value + * at a given priority 1..16. + * + * @param object - object instance + * @param priority - priority 1..16 + * + * @return true if values are within range and present-value is set. + */ +static bool Present_Value_Relinquish( + struct object_data *pObject, unsigned priority) +{ + bool status = false; + + if (priority && (priority <= BACNET_MAX_PRIORITY) && + (priority != 6 /* reserved */)) { + priority--; + BIT_CLEAR(pObject->Priority_Active_Bits, priority); + pObject->Priority_Array[priority] = 0.0; + status = true; + } + + return status; +} + +/** + * For a given object instance, sets the present-value at a given + * priority 1..16. + * + * @param object_instance - object-instance number of the object + * @param value - floating point analog value + * @param priority - priority 1..16 + * + * @return true if values are within range and present-value is set. + */ +static bool Present_Value_Set( + struct object_data *pObject, float value, unsigned priority) +{ + bool status = false; + + if (priority && (priority <= BACNET_MAX_PRIORITY) && + (priority != 6 /* reserved */)) { + priority--; + BIT_SET(pObject->Priority_Active_Bits, priority); + pObject->Priority_Array[priority] = value; + status = true; + } + + return status; +} + +/** + * For a given object instance-number, determines the active priority + * + * @param object_instance - object-instance number of the object + * + * @return active priority 1..16, or 0 if no priority is active + */ +unsigned Lighting_Output_Present_Value_Priority(uint32_t object_instance) +{ + unsigned priority = 0; /* return value */ + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + priority = Present_Value_Priority(pObject); + } + + return priority; +} + /** * For a given object instance-number, sets the present-value at a given * priority 1..16. @@ -284,16 +428,16 @@ unsigned Lighting_Output_Present_Value_Priority(uint32_t object_instance) bool Lighting_Output_Present_Value_Set( uint32_t object_instance, float value, unsigned priority) { - unsigned index = 0; bool status = false; + struct object_data *pObject; - index = Lighting_Output_Instance_To_Index(object_instance); - if (index < MAX_LIGHTING_OUTPUTS) { + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { if (priority && (priority <= BACNET_MAX_PRIORITY) && (priority != 6 /* reserved */)) { priority--; - BIT_SET(Lighting_Output[index].Priority_Active_Bits, priority); - Lighting_Output[index].Priority_Array[priority] = value; + BIT_SET(pObject->Priority_Active_Bits, priority); + pObject->Priority_Array[priority] = value; status = true; } } @@ -301,6 +445,151 @@ bool Lighting_Output_Present_Value_Set( return status; } +/** + * For a given object instance-number, writes the present-value + * + * @param object_instance - object-instance number of the object + * @param value - property value to write + * @param priority - priority-array index value 1..16 + * @param error_class - the BACnet error class + * @param error_code - BACnet Error code + * + * @return true if values are within range and present-value is set. + */ +static bool Lighting_Output_Present_Value_Write(uint32_t object_instance, + float value, + uint8_t priority, + BACNET_ERROR_CLASS *error_class, + BACNET_ERROR_CODE *error_code) +{ + bool status = false; + struct object_data *pObject; + uint8_t current_priority; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + if (priority == 6) { + /* Command priority 6 is reserved for use by Minimum On/Off + algorithm and may not be used for other purposes in any + object. */ + *error_class = ERROR_CLASS_PROPERTY; + *error_code = ERROR_CODE_WRITE_ACCESS_DENIED; + } else if ((priority > 0) && (priority <= BACNET_MAX_PRIORITY)) { + /* Note: Writing a special value has the same effect as writing + the corresponding lighting command and is subject to the same + restrictions. The special value itself is not written to the + priority array. */ + if (!islessgreater(value, -1.0)) { + /* Provides the same functionality as the + WARN lighting command. */ + current_priority = Present_Value_Priority(pObject); + if ((priority <= current_priority) && + (Priority_Array_Active(pObject, priority - 1)) && + (isgreater( + Priority_Array_Value(pObject, priority - 1), 0.0))) { + /* The blink-warn notification shall not occur + if any of the following conditions occur: + (a) The specified priority is not the highest + active priority, or + (b) The value at the specified priority is 0.0%, or + (c) Blink_Warn_Enable is FALSE. */ + pObject->Lighting_Command.operation = BACNET_LIGHTS_WARN; + } + status = true; + } else if (!islessgreater(value, -2.0)) { + /* Provides the same functionality as the + WARN_RELINQUISH lighting command. */ + current_priority = Present_Value_Priority(pObject); + if ((priority <= current_priority) && + (Priority_Array_Active(pObject, priority - 1)) && + (isgreater( + Priority_Array_Value(pObject, priority - 1), 0.0)) && + (isgreater(Priority_Array_Next_Value(pObject, priority - 1), + 0.0))) { + /* The blink-warn notification shall not occur, + and the value at the specified priority shall be + relinquished immediately if any of the following + conditions occur: + (a) The specified priority is not the highest + active priority, or + (b) The value at the specified priority + is 0.0% or NULL, or + (c) The value of the next highest non-NULL + priority, including Relinquish_Default, + is greater than 0.0%, or + (d) Blink_Warn_Enable is FALSE. */ + pObject->Lighting_Command.operation = + BACNET_LIGHTS_WARN_RELINQUISH; + } else { + Present_Value_Relinquish(pObject, priority); + } + status = true; + } else if (!islessgreater(value, -3.0)) { + /* Provides the same functionality as the + WARN_OFF lighting command. */ + current_priority = Present_Value_Priority(pObject); + if ((priority <= current_priority) && + (Priority_Array_Active(pObject, priority - 1)) && + (isgreater( + Priority_Array_Value(pObject, priority - 1), 0.0)) && + (isgreater(Priority_Array_Next_Value(pObject, priority - 1), + 0.0))) { + /* The blink-warn notification shall not occur and + the value 0.0% written at the specified + priority immediately if any of the following + conditions occur: + (a) The specified priority is not the highest + active priority, or + (b) The Present_Value is 0.0%, or + (c) Blink_Warn_Enable is FALSE. */ + pObject->Lighting_Command.operation = + BACNET_LIGHTS_WARN_OFF; + } else { + Present_Value_Set(pObject, 0.0, priority); + } + status = true; + } else if (isgreaterequal(value, 0.0) && + islessequal(value, 100.0)) { + Present_Value_Set(pObject, value, priority); + current_priority = Present_Value_Priority(pObject); + if (priority <= current_priority) { + /* we have priority - configure the Lighting Command */ + if (pObject->Transition == + BACNET_LIGHTING_TRANSITION_FADE) { + pObject->Lighting_Command.fade_time = + pObject->Default_Fade_Time; + pObject->Lighting_Command.operation = + BACNET_LIGHTS_FADE_TO; + } else if (pObject->Transition == + BACNET_LIGHTING_TRANSITION_RAMP) { + pObject->Lighting_Command.ramp_rate = + pObject->Default_Ramp_Rate; + pObject->Lighting_Command.operation = + BACNET_LIGHTS_RAMP_TO; + } else { + pObject->Lighting_Command.fade_time = 0; + pObject->Lighting_Command.operation = + BACNET_LIGHTS_FADE_TO; + } + pObject->Lighting_Command.target_level = value; + } + status = true; + } else { + *error_class = ERROR_CLASS_PROPERTY; + *error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; + } + } else { + *error_class = ERROR_CLASS_PROPERTY; + *error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; + } + } else { + *error_class = ERROR_CLASS_OBJECT; + *error_code = ERROR_CODE_UNKNOWN_OBJECT; + } + + return status; +} + /** * For a given object instance-number, relinquishes the present-value * at a given priority 1..16. @@ -313,18 +602,84 @@ bool Lighting_Output_Present_Value_Set( bool Lighting_Output_Present_Value_Relinquish( uint32_t object_instance, unsigned priority) { - unsigned index = 0; bool status = false; + struct object_data *pObject; - index = Lighting_Output_Instance_To_Index(object_instance); - if (index < MAX_LIGHTING_OUTPUTS) { - if (priority && (priority <= BACNET_MAX_PRIORITY) && - (priority != 6 /* reserved */)) { - priority--; - BIT_CLEAR(Lighting_Output[index].Priority_Active_Bits, priority); - Lighting_Output[index].Priority_Array[priority] = 0.0; + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + status = Present_Value_Relinquish(pObject, priority); + } + + return status; +} + +/** + * For a given object instance-number, relinquishes the present-value + * + * @param object_instance - object-instance number of the object + * @param value - property value to write + * @param priority - priority-array index value 1..16 + * @param error_class - the BACnet error class + * @param error_code - BACnet Error code + * + * @return true if values are within range and present-value is set. + */ +static bool Lighting_Output_Present_Value_Relinquish_Write( + uint32_t object_instance, + uint8_t priority, + BACNET_ERROR_CLASS *error_class, + BACNET_ERROR_CODE *error_code) +{ + bool status = false; + struct object_data *pObject; + uint8_t old_priority, new_priority; + float value; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + if (priority == 6) { + /* Command priority 6 is reserved for use by Minimum On/Off + algorithm and may not be used for other purposes in any + object. */ + *error_class = ERROR_CLASS_PROPERTY; + *error_code = ERROR_CODE_WRITE_ACCESS_DENIED; + } else if ((priority > 0) && (priority <= BACNET_MAX_PRIORITY)) { + old_priority = Present_Value_Priority(pObject); + Lighting_Output_Present_Value_Relinquish(object_instance, priority); + new_priority = + Lighting_Output_Present_Value_Priority(object_instance); + if (old_priority != new_priority) { + if (new_priority > BACNET_MAX_PRIORITY) { + /* BACNET_LIGHTS_WARN_RELINQUISH? */ + value = Lighting_Output_Relinquish_Default(object_instance); + } else { + value = + Lighting_Output_Present_Value_Priority(object_instance); + } + /* we have priority - configure the Lighting Command */ + if (pObject->Transition == BACNET_LIGHTING_TRANSITION_FADE) { + pObject->Lighting_Command.fade_time = + pObject->Default_Fade_Time; + pObject->Lighting_Command.operation = BACNET_LIGHTS_FADE_TO; + } else if (pObject->Transition == + BACNET_LIGHTING_TRANSITION_RAMP) { + pObject->Lighting_Command.ramp_rate = + pObject->Default_Ramp_Rate; + pObject->Lighting_Command.operation = BACNET_LIGHTS_RAMP_TO; + } else { + pObject->Lighting_Command.fade_time = 0; + pObject->Lighting_Command.operation = BACNET_LIGHTS_FADE_TO; + } + pObject->Lighting_Command.target_level = value; + } status = true; + } else { + *error_class = ERROR_CLASS_PROPERTY; + *error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; } + } else { + *error_class = ERROR_CLASS_OBJECT; + *error_code = ERROR_CODE_UNKNOWN_OBJECT; } return status; @@ -343,15 +698,107 @@ bool Lighting_Output_Present_Value_Relinquish( bool Lighting_Output_Object_Name( uint32_t object_instance, BACNET_CHARACTER_STRING *object_name) { - char text_string[32] = ""; bool status = false; - unsigned index = 0; + struct object_data *pObject; + char name_text[24] = "LIGHTING-OUTPUT-4194303"; - index = Lighting_Output_Instance_To_Index(object_instance); - if (index < MAX_LIGHTING_OUTPUTS) { - sprintf( - text_string, "LIGHTING OUTPUT %lu", (unsigned long)object_instance); - status = characterstring_init_ansi(object_name, text_string); + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + if (pObject->Object_Name) { + status = + characterstring_init_ansi(object_name, pObject->Object_Name); + } else { + snprintf(name_text, sizeof(name_text), "LIGHTING-OUTPUT-%u", + object_instance); + status = characterstring_init_ansi(object_name, name_text); + } + } + + return status; +} + +/** + * For a given object instance-number, sets the object-name + * Note that the object name must be unique within this device. + * + * @param object_instance - object-instance number of the object + * @param new_name - holds the object-name to be set + * + * @return true if object-name was set + */ +bool Lighting_Output_Name_Set(uint32_t object_instance, char *new_name) +{ + bool status = false; /* return value */ + BACNET_CHARACTER_STRING object_name; + BACNET_OBJECT_TYPE found_type = 0; + uint32_t found_instance = 0; + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject && new_name) { + /* All the object names in a device must be unique */ + characterstring_init_ansi(&object_name, new_name); + if (Device_Valid_Object_Name( + &object_name, &found_type, &found_instance)) { + if ((found_type == OBJECT_LIGHTING_OUTPUT) && + (found_instance == object_instance)) { + /* writing same name to same object */ + status = true; + } else { + /* duplicate name! */ + status = false; + } + } else { + status = true; + pObject->Object_Name = new_name; + Device_Inc_Database_Revision(); + } + } + + return status; +} + +/** + * For a given object instance-number, returns the description + * + * @param object_instance - object-instance number of the object + * + * @return description text or NULL if not found + */ +char *Lighting_Output_Description(uint32_t object_instance) +{ + char *name = NULL; + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + if (pObject->Description) { + name = (char *)pObject->Description; + } else { + name = ""; + } + } + + return name; +} + +/** + * For a given object instance-number, sets the description + * + * @param object_instance - object-instance number of the object + * @param new_name - holds the description to be set + * + * @return true if object-name was set + */ +bool Lighting_Output_Description_Set(uint32_t object_instance, char *new_name) +{ + bool status = false; /* return value */ + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject && new_name) { + status = true; + pObject->Description = new_name; } return status; @@ -369,13 +816,12 @@ bool Lighting_Output_Lighting_Command_Set( uint32_t object_instance, BACNET_LIGHTING_COMMAND *value) { bool status = false; - unsigned index = 0; + struct object_data *pObject; - index = Lighting_Output_Instance_To_Index(object_instance); - if (index < MAX_LIGHTING_OUTPUTS) { + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { /* FIXME: check lighting command member values */ - status = lighting_command_copy( - &Lighting_Output[index].Lighting_Command, value); + status = lighting_command_copy(&pObject->Lighting_Command, value); /* FIXME: set all the other values, and get the light levels moving */ } @@ -394,12 +840,11 @@ bool Lighting_Output_Lighting_Command( uint32_t object_instance, BACNET_LIGHTING_COMMAND *value) { bool status = false; - unsigned index = 0; + struct object_data *pObject; - index = Lighting_Output_Instance_To_Index(object_instance); - if (index < MAX_LIGHTING_OUTPUTS) { - status = lighting_command_copy( - value, &Lighting_Output[index].Lighting_Command); + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + status = lighting_command_copy(value, &pObject->Lighting_Command); } return status; @@ -416,11 +861,11 @@ BACNET_LIGHTING_IN_PROGRESS Lighting_Output_In_Progress( uint32_t object_instance) { BACNET_LIGHTING_IN_PROGRESS value = BACNET_LIGHTING_IDLE; - unsigned index = 0; + struct object_data *pObject; - index = Lighting_Output_Instance_To_Index(object_instance); - if (index < MAX_LIGHTING_OUTPUTS) { - value = Lighting_Output[index].In_Progress; + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + value = pObject->In_Progress; } return value; @@ -439,11 +884,11 @@ bool Lighting_Output_In_Progress_Set( uint32_t object_instance, BACNET_LIGHTING_IN_PROGRESS in_progress) { bool status = false; - unsigned index = 0; + struct object_data *pObject; - index = Lighting_Output_Instance_To_Index(object_instance); - if (index < MAX_LIGHTING_OUTPUTS) { - Lighting_Output[index].In_Progress = in_progress; + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + pObject->In_Progress = in_progress; } return status; @@ -459,11 +904,11 @@ bool Lighting_Output_In_Progress_Set( float Lighting_Output_Tracking_Value(uint32_t object_instance) { float value = 0.0; - unsigned index = 0; + struct object_data *pObject; - index = Lighting_Output_Instance_To_Index(object_instance); - if (index < MAX_LIGHTING_OUTPUTS) { - value = Lighting_Output[index].Tracking_Value; + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + value = pObject->Tracking_Value; } return value; @@ -481,11 +926,11 @@ float Lighting_Output_Tracking_Value(uint32_t object_instance) bool Lighting_Output_Tracking_Value_Set(uint32_t object_instance, float value) { bool status = false; - unsigned int index = 0; + struct object_data *pObject; - index = Lighting_Output_Instance_To_Index(object_instance); - if (index < MAX_LIGHTING_OUTPUTS) { - Lighting_Output[index].Tracking_Value = value; + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + pObject->Tracking_Value = value; status = true; } @@ -503,11 +948,11 @@ bool Lighting_Output_Tracking_Value_Set(uint32_t object_instance, float value) bool Lighting_Output_Blink_Warn_Enable(uint32_t object_instance) { bool value = false; - unsigned index = 0; + struct object_data *pObject; - index = Lighting_Output_Instance_To_Index(object_instance); - if (index < MAX_LIGHTING_OUTPUTS) { - value = Lighting_Output[index].Blink_Warn_Enable; + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + value = pObject->Blink_Warn_Enable; } return value; @@ -526,11 +971,11 @@ bool Lighting_Output_Blink_Warn_Enable_Set( uint32_t object_instance, bool enable) { bool status = false; - unsigned int index = 0; + struct object_data *pObject; - index = Lighting_Output_Instance_To_Index(object_instance); - if (index < MAX_LIGHTING_OUTPUTS) { - Lighting_Output[index].Blink_Warn_Enable = enable; + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + pObject->Blink_Warn_Enable = enable; status = true; } @@ -548,11 +993,11 @@ bool Lighting_Output_Blink_Warn_Enable_Set( uint32_t Lighting_Output_Egress_Time(uint32_t object_instance) { uint32_t value = 0; - unsigned int index = 0; + struct object_data *pObject; - index = Lighting_Output_Instance_To_Index(object_instance); - if (index < MAX_LIGHTING_OUTPUTS) { - value = Lighting_Output[index].Egress_Time; + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + value = pObject->Egress_Time; } return value; @@ -570,11 +1015,11 @@ uint32_t Lighting_Output_Egress_Time(uint32_t object_instance) bool Lighting_Output_Egress_Time_Set(uint32_t object_instance, uint32_t seconds) { bool status = false; - unsigned int index = 0; + struct object_data *pObject; - index = Lighting_Output_Instance_To_Index(object_instance); - if (index < MAX_LIGHTING_OUTPUTS) { - Lighting_Output[index].Egress_Time = seconds; + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + pObject->Egress_Time = seconds; status = true; } @@ -592,11 +1037,11 @@ bool Lighting_Output_Egress_Time_Set(uint32_t object_instance, uint32_t seconds) bool Lighting_Output_Egress_Active(uint32_t object_instance) { bool value = false; - unsigned int index = 0; + struct object_data *pObject; - index = Lighting_Output_Instance_To_Index(object_instance); - if (index < MAX_LIGHTING_OUTPUTS) { - value = Lighting_Output[index].Egress_Active; + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + value = pObject->Egress_Active; } return value; @@ -613,11 +1058,11 @@ bool Lighting_Output_Egress_Active(uint32_t object_instance) uint32_t Lighting_Output_Default_Fade_Time(uint32_t object_instance) { uint32_t value = 0; - unsigned int index = 0; + struct object_data *pObject; - index = Lighting_Output_Instance_To_Index(object_instance); - if (index < MAX_LIGHTING_OUTPUTS) { - value = Lighting_Output[index].Default_Fade_Time; + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + value = pObject->Default_Fade_Time; } return value; @@ -636,13 +1081,52 @@ bool Lighting_Output_Default_Fade_Time_Set( uint32_t object_instance, uint32_t milliseconds) { bool status = false; - unsigned int index = 0; + struct object_data *pObject; - index = Lighting_Output_Instance_To_Index(object_instance); - if ((index < MAX_LIGHTING_OUTPUTS) && (milliseconds >= 100) && - (milliseconds <= 86400000)) { - Lighting_Output[index].Default_Fade_Time = milliseconds; - status = true; + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + if ((milliseconds >= 100) && (milliseconds <= 86400000)) { + pObject->Default_Fade_Time = milliseconds; + status = true; + } + } + + return status; +} + +/** + * Handle a WriteProperty to a specific property. + * + * @param object_instance - object-instance number of the object + * @param value - property value to be written + * @param priority - priority-array index value 1..16 + * @param error_class - the BACnet error class + * @param error_code - BACnet Error code + * + * @return true if values are within range and present-value is set. + */ +static bool Lighting_Output_Default_Fade_Time_Write(uint32_t object_instance, + uint32_t value, + uint8_t priority, + BACNET_ERROR_CLASS *error_class, + BACNET_ERROR_CODE *error_code) +{ + bool status = false; + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + (void)priority; + if ((value >= 100) && (value <= 86400000)) { + pObject->Default_Fade_Time = value; + status = true; + } else { + *error_class = ERROR_CLASS_PROPERTY; + *error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; + } + } else { + *error_class = ERROR_CLASS_PROPERTY; + *error_code = ERROR_CODE_WRITE_ACCESS_DENIED; } return status; @@ -659,11 +1143,11 @@ bool Lighting_Output_Default_Fade_Time_Set( float Lighting_Output_Default_Ramp_Rate(uint32_t object_instance) { float value = 0.0; - unsigned int index = 0; + struct object_data *pObject; - index = Lighting_Output_Instance_To_Index(object_instance); - if (index < MAX_LIGHTING_OUTPUTS) { - value = Lighting_Output[index].Default_Ramp_Rate; + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + value = pObject->Default_Ramp_Rate; } return value; @@ -682,13 +1166,52 @@ bool Lighting_Output_Default_Ramp_Rate_Set( uint32_t object_instance, float percent_per_second) { bool status = false; - unsigned int index = 0; + struct object_data *pObject; - index = Lighting_Output_Instance_To_Index(object_instance); - if ((index < MAX_LIGHTING_OUTPUTS) && (percent_per_second >= 0.1) && - (percent_per_second <= 100.0)) { - Lighting_Output[index].Default_Ramp_Rate = percent_per_second; - status = true; + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + if ((percent_per_second >= 0.1) && (percent_per_second <= 100.0)) { + pObject->Default_Ramp_Rate = percent_per_second; + status = true; + } + } + + return status; +} + +/** + * Handle a WriteProperty to a specific property. + * + * @param object_instance - object-instance number of the object + * @param value - property value to be written + * @param priority - priority-array index value 1..16 + * @param error_class - the BACnet error class + * @param error_code - BACnet Error code + * + * @return true if values are within range and present-value is set. + */ +static bool Lighting_Output_Default_Ramp_Rate_Write(uint32_t object_instance, + float value, + uint8_t priority, + BACNET_ERROR_CLASS *error_class, + BACNET_ERROR_CODE *error_code) +{ + bool status = false; + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + (void)priority; + if ((value >= 0.1) && (value <= 100.0)) { + pObject->Default_Fade_Time = value; + status = true; + } else { + *error_class = ERROR_CLASS_PROPERTY; + *error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; + } + } else { + *error_class = ERROR_CLASS_PROPERTY; + *error_code = ERROR_CODE_WRITE_ACCESS_DENIED; } return status; @@ -705,11 +1228,11 @@ bool Lighting_Output_Default_Ramp_Rate_Set( float Lighting_Output_Default_Step_Increment(uint32_t object_instance) { float value = 0.0; - unsigned int index = 0; + struct object_data *pObject; - index = Lighting_Output_Instance_To_Index(object_instance); - if (index < MAX_LIGHTING_OUTPUTS) { - value = Lighting_Output[index].Default_Step_Increment; + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + value = pObject->Default_Step_Increment; } return value; @@ -728,13 +1251,53 @@ bool Lighting_Output_Default_Step_Increment_Set( uint32_t object_instance, float step_increment) { bool status = false; - unsigned int index = 0; + struct object_data *pObject; - index = Lighting_Output_Instance_To_Index(object_instance); - if ((index < MAX_LIGHTING_OUTPUTS) && (step_increment >= 0.1) && - (step_increment <= 100.0)) { - Lighting_Output[index].Default_Step_Increment = step_increment; - status = true; + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + if ((step_increment >= 0.1) && (step_increment <= 100.0)) { + pObject->Default_Step_Increment = step_increment; + status = true; + } + } + + return status; +} + +/** + * Handle a WriteProperty to a specific property. + * + * @param object_instance - object-instance number of the object + * @param value - property value to be written + * @param priority - priority-array index value 1..16 + * @param error_class - the BACnet error class + * @param error_code - BACnet Error code + * + * @return true if values are within range and present-value is set. + */ +static bool Lighting_Output_Default_Step_Increment_Write( + uint32_t object_instance, + float value, + uint8_t priority, + BACNET_ERROR_CLASS *error_class, + BACNET_ERROR_CODE *error_code) +{ + bool status = false; + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + (void)priority; + if ((value >= 0.1) && (value <= 100.0)) { + pObject->Default_Step_Increment = value; + status = true; + } else { + *error_class = ERROR_CLASS_PROPERTY; + *error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; + } + } else { + *error_class = ERROR_CLASS_PROPERTY; + *error_code = ERROR_CODE_WRITE_ACCESS_DENIED; } return status; @@ -753,11 +1316,11 @@ bool Lighting_Output_Default_Step_Increment_Set( unsigned Lighting_Output_Default_Priority(uint32_t object_instance) { unsigned value = 0; - unsigned int index = 0; + struct object_data *pObject; - index = Lighting_Output_Instance_To_Index(object_instance); - if (index < MAX_LIGHTING_OUTPUTS) { - value = Lighting_Output[index].Lighting_Command_Default_Priority; + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + value = pObject->Lighting_Command_Default_Priority; } return value; @@ -776,13 +1339,15 @@ bool Lighting_Output_Default_Priority_Set( uint32_t object_instance, unsigned priority) { bool status = false; - unsigned int index = 0; + struct object_data *pObject; - index = Lighting_Output_Instance_To_Index(object_instance); - if ((index < MAX_LIGHTING_OUTPUTS) && (priority >= BACNET_MIN_PRIORITY) && - (priority <= BACNET_MAX_PRIORITY)) { - Lighting_Output[index].Lighting_Command_Default_Priority = priority; - status = true; + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + if ((priority >= BACNET_MIN_PRIORITY) && + (priority <= BACNET_MAX_PRIORITY)) { + pObject->Lighting_Command_Default_Priority = priority; + status = true; + } } return status; @@ -799,11 +1364,11 @@ bool Lighting_Output_Default_Priority_Set( bool Lighting_Output_Out_Of_Service(uint32_t object_instance) { bool value = false; - unsigned int index = 0; + struct object_data *pObject; - index = Lighting_Output_Instance_To_Index(object_instance); - if (index < MAX_LIGHTING_OUTPUTS) { - value = Lighting_Output[index].Out_Of_Service; + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + value = pObject->Out_Of_Service; } return value; @@ -819,11 +1384,11 @@ bool Lighting_Output_Out_Of_Service(uint32_t object_instance) */ void Lighting_Output_Out_Of_Service_Set(uint32_t object_instance, bool value) { - unsigned int index = 0; + struct object_data *pObject; - index = Lighting_Output_Instance_To_Index(object_instance); - if (index < MAX_LIGHTING_OUTPUTS) { - Lighting_Output[index].Out_Of_Service = value; + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + pObject->Out_Of_Service = value; } } @@ -838,11 +1403,11 @@ void Lighting_Output_Out_Of_Service_Set(uint32_t object_instance, bool value) float Lighting_Output_Relinquish_Default(uint32_t object_instance) { float value = 0.0; - unsigned int index = 0; + struct object_data *pObject; - index = Lighting_Output_Instance_To_Index(object_instance); - if (index < MAX_LIGHTING_OUTPUTS) { - value = Lighting_Output[index].Relinquish_Default; + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + value = pObject->Relinquish_Default; } return value; @@ -861,11 +1426,257 @@ bool Lighting_Output_Relinquish_Default_Set( uint32_t object_instance, float value) { bool status = false; - unsigned int index = 0; + struct object_data *pObject; - index = Lighting_Output_Instance_To_Index(object_instance); - if (index < MAX_LIGHTING_OUTPUTS) { - Lighting_Output[index].Relinquish_Default = value; + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + pObject->Relinquish_Default = value; + } + + return status; +} + +/** + * For a given object instance-number, gets the property value + * + * @param object_instance - object-instance number of the object + * @return property value + */ +BACNET_LIGHTING_TRANSITION Lighting_Output_Transition(uint32_t object_instance) +{ + BACNET_LIGHTING_TRANSITION value = BACNET_LIGHTING_TRANSITION_NONE; + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + value = pObject->Transition; + } + + return value; +} + +/** + * For a given object instance-number, sets the property value + * + * @param object_instance - object-instance number of the object + * @param value - BACNET_COLOR_TRANSITION + * @return true if values are within range and value is set. + */ +bool Lighting_Output_Transition_Set( + uint32_t object_instance, BACNET_LIGHTING_TRANSITION value) +{ + bool status = false; + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + if (value <= BACNET_LIGHTING_TRANSITION_PROPRIETARY_LAST) { + pObject->Transition = value; + status = true; + } + } + + return status; +} + +/** + * Handle a WriteProperty to a specific property. + * + * @param object_instance - object-instance number of the object + * @param value - property value to be written + * @param priority - priority-array index value 1..16 + * @param error_class - the BACnet error class + * @param error_code - BACnet Error code + * + * @return true if values are within range and present-value is set. + */ +static bool Lighting_Output_Transition_Write(uint32_t object_instance, + uint32_t value, + uint8_t priority, + BACNET_ERROR_CLASS *error_class, + BACNET_ERROR_CODE *error_code) +{ + bool status = false; + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + (void)priority; + if (value < BACNET_LIGHTING_TRANSITION_PROPRIETARY_LAST) { + pObject->Transition = value; + status = true; + } else { + *error_class = ERROR_CLASS_PROPERTY; + *error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; + } + } else { + *error_class = ERROR_CLASS_OBJECT; + *error_code = ERROR_CODE_UNKNOWN_OBJECT; + } + + return status; +} + +/** + * For a given object instance-number, returns the color-override + * property value + * + * @param object_instance - object-instance number of the object + * + * @return color-override property value + */ +bool Lighting_Output_Color_Override(uint32_t object_instance) +{ + bool value = false; + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + value = pObject->Color_Override; + } + + return value; +} + +/** + * For a given object instance-number, sets the color-override + * property value + * + * @param object_instance - object-instance number of the object + * @param value - color-override boolean value + * + * @return true if the color-override property value was set + */ +bool Lighting_Output_Color_Override_Set(uint32_t object_instance, bool value) +{ + bool status = false; + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + pObject->Color_Override = value; + } + + return status; +} + +/** + * For a given object instance-number, gets the property value + * + * This property, of type BACnetObjectIdentifier, when present, + * shall specify the object identifier of a Color or Color Temperature + * object within the same device that controls the color aspects + * of this Lighting Output. If the object instance portion of + * the object identifier has the value 4194303, then there is no color + * companion object associated with this output. In that case the + * applicable color or color temperature shall be a local matter. + * + * @param object_instance - object-instance number of the object + * @param value - holds the property value + * + * @return true if property was retrieved + */ +bool Lighting_Output_Color_Reference( + uint32_t object_instance, BACNET_OBJECT_ID *value) +{ + bool status = false; + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + if (value) { + value->type = pObject->Color_Reference.type; + value->instance = pObject->Color_Reference.instance; + } + status = true; + } + + return status; +} + +/** + * For a given object instance-number, sets the property value + * + * @param object_instance - object-instance number of the object + * @param value - property value to set + * + * @return true if property value was set + */ +bool Lighting_Output_Color_Reference_Set( + uint32_t object_instance, BACNET_OBJECT_ID *value) +{ + bool status = false; + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + pObject->Color_Reference.type = value->type; + pObject->Color_Reference.instance = value->instance; + status = true; + } + + return status; +} + +/** + * For a given object instance-number, gets the property value + * + * This property, of type BACnetObjectIdentifier, when present, shall + * specify the object identifier of a Color or Color Temperature + * object within the same device that controls the color override + * aspects of this Lighting Output. Color override occurs + * when the Color_Override property of the Lighting Output is + * written with TRUE. In this case the Override_Color_Reference points + * to an object whose color shall be used to control the actual color + * of the lighting output. While color-overridden, any fade that may + * be in progress for the Color_Reference object, shall continue without + * interruption, except that the actual color output shall use the + * override color instead. See Clause 12.55 for a description of + * color override. Color override shall cease when Color_Override + * is written with FALSE. + * + * @param object_instance - object-instance number of the object + * @param value - holds the property value + * + * @return true if property was retrieved + */ +bool Lighting_Output_Override_Color_Reference( + uint32_t object_instance, BACNET_OBJECT_ID *value) +{ + bool status = false; + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + if (value) { + value->type = pObject->Override_Color_Reference.type; + value->instance = pObject->Override_Color_Reference.instance; + } + status = true; + } + + return status; +} + +/** + * For a given object instance-number, sets the property value + * + * @param object_instance - object-instance number of the object + * @param value - property value to set + * + * @return true if property value was set + */ +bool Lighting_Output_Override_Color_Reference_Set( + uint32_t object_instance, BACNET_OBJECT_ID *value) +{ + bool status = false; + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + pObject->Override_Color_Reference.type = value->type; + pObject->Override_Color_Reference.instance = value->instance; + status = true; } return status; @@ -888,6 +1699,9 @@ int Lighting_Output_Read_Property(BACNET_READ_PROPERTY_DATA *rpdata) BACNET_BIT_STRING bit_string; BACNET_CHARACTER_STRING char_string; BACNET_LIGHTING_COMMAND lighting_command; +#if (BACNET_PROTOCOL_REVISION >= 24) + BACNET_OBJECT_ID object_id = { 0 }; +#endif float real_value = (float)1.414; uint32_t unsigned_value = 0; unsigned i = 0; @@ -974,6 +1788,10 @@ int Lighting_Output_Read_Property(BACNET_READ_PROPERTY_DATA *rpdata) Lighting_Output_Default_Step_Increment(rpdata->object_instance); apdu_len = encode_application_real(&apdu[0], real_value); break; + case PROP_TRANSITION: + apdu_len = encode_application_enumerated( + apdu, Lighting_Output_Transition(rpdata->object_instance)); + break; case PROP_PRIORITY_ARRAY: apdu_len = bacnet_array_encode(rpdata->object_instance, rpdata->array_index, Lighting_Output_Priority_Array_Encode, @@ -1006,6 +1824,30 @@ int Lighting_Output_Read_Property(BACNET_READ_PROPERTY_DATA *rpdata) } break; #endif +#if (BACNET_PROTOCOL_REVISION >= 24) + case PROP_COLOR_OVERRIDE: + apdu_len = encode_application_boolean(&apdu[0], + Lighting_Output_Color_Override(rpdata->object_instance)); + break; + case PROP_COLOR_REFERENCE: + (void)Lighting_Output_Color_Reference( + rpdata->object_instance, &object_id); + apdu_len = encode_application_object_id( + &apdu[0], object_id.type, object_id.instance); + break; + case PROP_OVERRIDE_COLOR_REFERENCE: + (void)Lighting_Output_Override_Color_Reference( + rpdata->object_instance, &object_id); + apdu_len = encode_application_object_id( + &apdu[0], object_id.type, object_id.instance); + break; +#endif + case PROP_DESCRIPTION: + characterstring_init_ansi(&char_string, + Lighting_Output_Description(rpdata->object_instance)); + apdu_len = + encode_application_character_string(&apdu[0], &char_string); + break; default: rpdata->error_class = ERROR_CLASS_PROPERTY; rpdata->error_code = ERROR_CODE_UNKNOWN_PROPERTY; @@ -1060,42 +1902,17 @@ bool Lighting_Output_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) status = write_property_type_valid( wp_data, &value, BACNET_APPLICATION_TAG_REAL); if (status) { - /* Command priority 6 is reserved for use by Minimum On/Off - algorithm and may not be used for other purposes in any - object. */ - status = - Lighting_Output_Present_Value_Set(wp_data->object_instance, - value.type.Real, wp_data->priority); - if (wp_data->priority == 6) { - /* Command priority 6 is reserved for use by Minimum On/Off - algorithm and may not be used for other purposes in any - object. */ - wp_data->error_class = ERROR_CLASS_PROPERTY; - wp_data->error_code = ERROR_CODE_WRITE_ACCESS_DENIED; - } else if (!status) { - wp_data->error_class = ERROR_CLASS_PROPERTY; - wp_data->error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; - } + status = Lighting_Output_Present_Value_Write( + wp_data->object_instance, value.type.Real, + wp_data->priority, &wp_data->error_class, + &wp_data->error_code); } else { status = write_property_type_valid( wp_data, &value, BACNET_APPLICATION_TAG_NULL); if (status) { - if (wp_data->priority == 6) { - /* Command priority 6 is reserved for use by Minimum - On/Off algorithm and may not be used for other - purposes in any object. - Note - Lighting_Output_Present_Value_Relinquish() - will have returned false because of this */ - wp_data->error_class = ERROR_CLASS_PROPERTY; - wp_data->error_code = ERROR_CODE_WRITE_ACCESS_DENIED; - } else { - status = Lighting_Output_Present_Value_Relinquish( - wp_data->object_instance, wp_data->priority); - if (!status) { - wp_data->error_class = ERROR_CLASS_PROPERTY; - wp_data->error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; - } - } + status = Lighting_Output_Present_Value_Relinquish_Write( + wp_data->object_instance, wp_data->priority, + &wp_data->error_class, &wp_data->error_code); } } break; @@ -1119,6 +1936,46 @@ bool Lighting_Output_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) wp_data->object_instance, value.type.Boolean); } break; + case PROP_DEFAULT_FADE_TIME: + status = write_property_type_valid( + wp_data, &value, BACNET_APPLICATION_TAG_UNSIGNED_INT); + if (status) { + status = Lighting_Output_Default_Fade_Time_Write( + wp_data->object_instance, value.type.Unsigned_Int, + wp_data->priority, &wp_data->error_class, + &wp_data->error_code); + } + break; + case PROP_DEFAULT_RAMP_RATE: + status = write_property_type_valid( + wp_data, &value, BACNET_APPLICATION_TAG_REAL); + if (status) { + status = Lighting_Output_Default_Ramp_Rate_Write( + wp_data->object_instance, value.type.Real, + wp_data->priority, &wp_data->error_class, + &wp_data->error_code); + } + break; + case PROP_DEFAULT_STEP_INCREMENT: + status = write_property_type_valid( + wp_data, &value, BACNET_APPLICATION_TAG_REAL); + if (status) { + status = Lighting_Output_Default_Step_Increment_Write( + wp_data->object_instance, value.type.Real, + wp_data->priority, &wp_data->error_class, + &wp_data->error_code); + } + break; + case PROP_TRANSITION: + status = write_property_type_valid( + wp_data, &value, BACNET_APPLICATION_TAG_ENUMERATED); + if (status) { + status = + Lighting_Output_Transition_Write(wp_data->object_instance, + value.type.Enumerated, wp_data->priority, + &wp_data->error_class, &wp_data->error_code); + } + break; case PROP_OBJECT_IDENTIFIER: case PROP_OBJECT_NAME: case PROP_OBJECT_TYPE: @@ -1128,14 +1985,18 @@ bool Lighting_Output_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) case PROP_BLINK_WARN_ENABLE: case PROP_EGRESS_TIME: case PROP_EGRESS_ACTIVE: - case PROP_DEFAULT_FADE_TIME: - case PROP_DEFAULT_RAMP_RATE: - case PROP_DEFAULT_STEP_INCREMENT: case PROP_PRIORITY_ARRAY: case PROP_RELINQUISH_DEFAULT: + case PROP_LIGHTING_COMMAND_DEFAULT_PRIORITY: #if (BACNET_PROTOCOL_REVISION >= 17) case PROP_CURRENT_COMMAND_PRIORITY: #endif +#if (BACNET_PROTOCOL_REVISION >= 24) + case PROP_COLOR_OVERRIDE: + case PROP_COLOR_REFERENCE: + case PROP_OVERRIDE_COLOR_REFERENCE: +#endif + case PROP_DESCRIPTION: wp_data->error_class = ERROR_CLASS_PROPERTY; wp_data->error_code = ERROR_CODE_WRITE_ACCESS_DENIED; break; @@ -1148,26 +2009,6 @@ bool Lighting_Output_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) return status; } -/** - * Handles the timing for a single Lighting Output object Ramp - * - * @param pLight - Lighting Output object - * @param pCommand - BACNET_LIGHTING_COMMAND of the Lighting Output object - * @param milliseconds - number of milliseconds elapsed since previously - * called. Works best when called about every 10 milliseconds. - */ -static void Lighting_Output_Ramp_Handler(struct lighting_output_object *pLight, - BACNET_LIGHTING_COMMAND *pCommand, - uint16_t milliseconds) -{ - if (pLight && pCommand) { - /* FIXME: add ramp functionality */ - (void)pLight; - (void)pCommand; - (void)milliseconds; - } -} - /** * Handles the timing for a single Lighting Output object Fade * @@ -1176,49 +2017,328 @@ static void Lighting_Output_Ramp_Handler(struct lighting_output_object *pLight, * @param milliseconds - number of milliseconds elapsed since previously * called. Works best when called about every 10 milliseconds. */ -static void Lighting_Output_Fade_Handler(struct lighting_output_object *pLight, - BACNET_LIGHTING_COMMAND *pCommand, - uint16_t milliseconds) +static void Lighting_Output_Fade_Handler( + uint32_t object_instance, uint16_t milliseconds) { - if (pLight && pCommand) { - /* FIXME: add fade functionality */ - (void)pLight; - (void)pCommand; - (void)milliseconds; + struct object_data *pObject; + float old_value; + + pObject = Keylist_Data(Object_List, object_instance); + if (!pObject) { + return; + } + old_value = pObject->Tracking_Value; + if (milliseconds >= pObject->Lighting_Command.fade_time) { + /* stop fading */ + pObject->Tracking_Value = pObject->Lighting_Command.target_level; + pObject->In_Progress = BACNET_LIGHTING_IDLE; + pObject->Lighting_Command.operation = BACNET_LIGHTS_STOP; + pObject->Lighting_Command.fade_time = 0; + } else { + if (!islessgreater(pObject->Tracking_Value, + pObject->Lighting_Command.target_level)) { + /* stop fading */ + pObject->Tracking_Value = pObject->Lighting_Command.target_level; + pObject->In_Progress = BACNET_LIGHTING_IDLE; + pObject->Lighting_Command.operation = BACNET_LIGHTS_STOP; + pObject->Lighting_Command.fade_time = 0; + } else { + /* fading */ + pObject->Tracking_Value = linear_interpolate(0, milliseconds, + pObject->Lighting_Command.fade_time, old_value, + pObject->Lighting_Command.target_level); + pObject->Lighting_Command.fade_time -= milliseconds; + pObject->In_Progress = BACNET_LIGHTING_FADE_ACTIVE; + } + } + if (Lighting_Output_Write_Present_Value_Callback) { + Lighting_Output_Write_Present_Value_Callback( + object_instance, old_value, pObject->Tracking_Value); } } /** - * Handles the timing for a single Lighting Output object + * Updates the object tracking value while ramping * - * @param index - 0..MAX_LIGHTING_OUTPUTS value - * @param milliseconds - number of milliseconds elapsed since previously - * called. Works best when called about every 10 milliseconds. + * Commands Present_Value to ramp from the current Tracking_Value to the + * target-level specified in the command. The ramp operation + * changes the output from its current value to target-level, + * at a particular percent per second defined by ramp-rate. + * While the ramp operation is executing, In_Progress shall be set + * to RAMP_ACTIVE, and Tracking_Value shall be updated to reflect the current + * progress of the ramp. shall be clamped to + * Min_Actual_Value and Max_Actual_Value. + * + * @param object_instance - object-instance number of the object + * @param milliseconds - number of milliseconds elapsed */ -static void Lighting_Output_Timer_Handler(unsigned index, uint16_t milliseconds) +static void Lighting_Output_Ramp_Handler( + uint32_t object_instance, uint16_t milliseconds) { - struct lighting_output_object *pLight = NULL; - BACNET_LIGHTING_COMMAND *pCommand = NULL; + float old_value, target_value, min_value, max_value, step_value, steps; + struct object_data *pObject; - if (index < MAX_LIGHTING_OUTPUTS) { - pLight = &Lighting_Output[index]; - pCommand = &pLight->Lighting_Command; - switch (pCommand->operation) { + pObject = Keylist_Data(Object_List, object_instance); + if (!pObject) { + return; + } + old_value = pObject->Tracking_Value; + min_value = pObject->Min_Actual_Value; + max_value = pObject->Max_Actual_Value; + target_value = pObject->Lighting_Command.target_level; + /* clamp target within min/max, if needed */ + if (isgreater(target_value, max_value)) { + target_value = max_value; + } + if (isless(target_value, min_value)) { + target_value = min_value; + } + /* determine the number of steps */ + if (milliseconds <= 1000) { + /* percent per second */ + steps = linear_interpolate( + 0, milliseconds, 1000, 0, pObject->Lighting_Command.ramp_rate); + } else { + steps = (milliseconds * pObject->Lighting_Command.ramp_rate) / 1000; + } + if (!islessgreater(pObject->Tracking_Value, target_value)) { + /* stop ramping */ + pObject->Tracking_Value = target_value; + pObject->In_Progress = BACNET_LIGHTING_IDLE; + pObject->Lighting_Command.operation = BACNET_LIGHTS_STOP; + } else { + if (isless(old_value, target_value)) { + step_value = old_value + steps; + } else if (isgreater(old_value, target_value)) { + if (isgreater(steps, old_value)) { + step_value = old_value - steps; + } else { + step_value = target_value; + } + } else { + step_value = target_value; + } + /* clamp target within min/max, if needed */ + if (isgreater(step_value, max_value)) { + step_value = max_value; + } + if (isless(step_value, min_value)) { + step_value = min_value; + } + pObject->Tracking_Value = step_value; + pObject->In_Progress = BACNET_LIGHTING_RAMP_ACTIVE; + } + if (Lighting_Output_Write_Present_Value_Callback) { + Lighting_Output_Write_Present_Value_Callback( + object_instance, old_value, pObject->Tracking_Value); + } +} + +/** + * Updates the object tracking value while stepping + * + * Commands Present_Value to a value equal to the Tracking_Value + * plus the step-increment. The resulting sum shall be clamped to + * Min_Actual_Value and Max_Actual_Value + * + * @param object_instance - object-instance number of the object + */ +static void Lighting_Output_Step_Up_Handler(uint32_t object_instance) +{ + float old_value, target_value, min_value, max_value, step_value; + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (!pObject) { + return; + } + old_value = pObject->Tracking_Value; + min_value = pObject->Min_Actual_Value; + max_value = pObject->Max_Actual_Value; + step_value = pObject->Lighting_Command.step_increment; + /* inhibit ON if the value is already OFF */ + if (isgreaterequal(old_value, min_value)) { + target_value = old_value + step_value; + /* clamp target within min/max, if needed */ + if (isgreater(target_value, max_value)) { + target_value = max_value; + } + if (isless(target_value, min_value)) { + target_value = min_value; + } + pObject->Present_Value = target_value; + pObject->Tracking_Value = target_value; + pObject->In_Progress = BACNET_LIGHTING_IDLE; + pObject->Lighting_Command.operation = BACNET_LIGHTS_STOP; + if (Lighting_Output_Write_Present_Value_Callback) { + Lighting_Output_Write_Present_Value_Callback( + object_instance, old_value, pObject->Tracking_Value); + } + } +} + +/** + * Updates the object tracking value while stepping + * + * Commands Present_Value to a value equal to the Tracking_Value + * plus the step-increment. The resulting sum shall be clamped to + * Min_Actual_Value and Max_Actual_Value + * + * @param object_instance - object-instance number of the object + */ +static void Lighting_Output_Step_Down_Handler(uint32_t object_instance) +{ + float old_value, target_value, min_value, max_value, step_value; + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (!pObject) { + return; + } + old_value = target_value = pObject->Tracking_Value; + min_value = pObject->Min_Actual_Value; + max_value = pObject->Max_Actual_Value; + step_value = pObject->Lighting_Command.step_increment; + if (isgreaterequal(target_value, step_value)) { + target_value -= step_value; + } else { + target_value = 0.0; + } + /* clamp target within min/max, if needed */ + if (isgreater(target_value, max_value)) { + target_value = max_value; + } + if (isless(target_value, min_value)) { + target_value = min_value; + } + pObject->Present_Value = target_value; + pObject->Tracking_Value = target_value; + pObject->In_Progress = BACNET_LIGHTING_IDLE; + pObject->Lighting_Command.operation = BACNET_LIGHTS_STOP; + if (Lighting_Output_Write_Present_Value_Callback) { + Lighting_Output_Write_Present_Value_Callback( + object_instance, old_value, pObject->Tracking_Value); + } +} + +/** + * Updates the object tracking value while stepping + * + * Commands Present_Value to a value equal to the Tracking_Value + * plus the step-increment. The resulting sum shall be clamped to + * Min_Actual_Value and Max_Actual_Value + * + * @param object_instance - object-instance number of the object + */ +static void Lighting_Output_Step_On_Handler(uint32_t object_instance) +{ + float old_value, target_value, min_value, max_value, step_value; + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (!pObject) { + return; + } + old_value = target_value = pObject->Tracking_Value; + min_value = pObject->Min_Actual_Value; + max_value = pObject->Max_Actual_Value; + step_value = pObject->Lighting_Command.step_increment; + target_value += step_value; + /* clamp target within min/max, if needed */ + if (isgreater(target_value, max_value)) { + target_value = max_value; + } + if (isless(target_value, min_value)) { + target_value = min_value; + } + pObject->Present_Value = target_value; + pObject->Tracking_Value = target_value; + pObject->In_Progress = BACNET_LIGHTING_IDLE; + pObject->Lighting_Command.operation = BACNET_LIGHTS_STOP; + if (Lighting_Output_Write_Present_Value_Callback) { + Lighting_Output_Write_Present_Value_Callback( + object_instance, old_value, pObject->Tracking_Value); + } +} + +/** + * Updates the object tracking value while stepping + * + * Commands Present_Value to a value equal to the Tracking_Value + * plus the step-increment. The resulting sum shall be clamped to + * Min_Actual_Value and Max_Actual_Value + * + * @param object_instance - object-instance number of the object + */ +static void Lighting_Output_Step_Off_Handler(uint32_t object_instance) +{ + float old_value, target_value, min_value, max_value, step_value; + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (!pObject) { + return; + } + old_value = target_value = pObject->Tracking_Value; + min_value = pObject->Min_Actual_Value; + max_value = pObject->Max_Actual_Value; + step_value = pObject->Lighting_Command.step_increment; + if (isgreaterequal(target_value, step_value)) { + target_value -= step_value; + } else { + target_value = 0; + } + /* clamp target within max */ + if (isgreater(target_value, max_value)) { + target_value = max_value; + } + /* jump target to OFF if below min */ + if (isless(target_value, min_value)) { + target_value = 0.0; + } + pObject->Present_Value = target_value; + pObject->Tracking_Value = target_value; + pObject->In_Progress = BACNET_LIGHTING_IDLE; + pObject->Lighting_Command.operation = BACNET_LIGHTS_STOP; + if (Lighting_Output_Write_Present_Value_Callback) { + Lighting_Output_Write_Present_Value_Callback( + object_instance, old_value, pObject->Tracking_Value); + } +} + +/** + * @brief Updates the lighting object tracking value per ramp or fade or step + * @param object_instance - object-instance number of the object + * @param milliseconds - number of milliseconds elapsed since previously + * called. Suggest that this is called every 10 milliseconds. + */ +void Lighting_Output_Timer(uint32_t object_instance, uint16_t milliseconds) +{ + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + switch (pObject->Lighting_Command.operation) { case BACNET_LIGHTS_NONE: + pObject->In_Progress = BACNET_LIGHTING_IDLE; break; case BACNET_LIGHTS_FADE_TO: - Lighting_Output_Fade_Handler(pLight, pCommand, milliseconds); + Lighting_Output_Fade_Handler(object_instance, milliseconds); break; case BACNET_LIGHTS_RAMP_TO: - Lighting_Output_Ramp_Handler(pLight, pCommand, milliseconds); + Lighting_Output_Ramp_Handler(object_instance, milliseconds); break; case BACNET_LIGHTS_STEP_UP: + Lighting_Output_Step_Up_Handler(object_instance); break; case BACNET_LIGHTS_STEP_DOWN: + Lighting_Output_Step_Down_Handler(object_instance); break; case BACNET_LIGHTS_STEP_ON: + Lighting_Output_Step_On_Handler(object_instance); break; case BACNET_LIGHTS_STEP_OFF: + Lighting_Output_Step_Off_Handler(object_instance); break; case BACNET_LIGHTS_WARN: break; @@ -1227,6 +2347,7 @@ static void Lighting_Output_Timer_Handler(unsigned index, uint16_t milliseconds) case BACNET_LIGHTS_WARN_RELINQUISH: break; case BACNET_LIGHTS_STOP: + pObject->In_Progress = BACNET_LIGHTING_IDLE; break; default: break; @@ -1235,58 +2356,131 @@ static void Lighting_Output_Timer_Handler(unsigned index, uint16_t milliseconds) } /** - * Initializes the Lighting Output object data - * - * @param milliseconds - number of milliseconds elapsed since previously - * called. Works best when called about every 10 milliseconds. + * @brief Sets a callback used when present-value is written from BACnet + * @param cb - callback used to provide indications */ -void Lighting_Output_Timer(uint16_t milliseconds) +void Lighting_Output_Write_Present_Value_Callback_Set( + lighting_output_write_present_value_callback cb) { - unsigned i = 0; + Lighting_Output_Write_Present_Value_Callback = cb; +} - for (i = 0; i < MAX_LIGHTING_OUTPUTS; i++) { - Lighting_Output_Timer_Handler(i, milliseconds); +/** + * @brief Creates a Color object + * @param object_instance - object-instance number of the object + * @return the object-instance that was created, or BACNET_MAX_INSTANCE + */ +uint32_t Lighting_Output_Create(uint32_t object_instance) +{ + struct object_data *pObject = NULL; + int index = 0; + unsigned p = 0; + + if (object_instance > BACNET_MAX_INSTANCE) { + return BACNET_MAX_INSTANCE; + } else if (object_instance == BACNET_MAX_INSTANCE) { + /* wildcard instance */ + /* the Object_Identifier property of the newly created object + shall be initialized to a value that is unique within the + responding BACnet-user device. The method used to generate + the object identifier is a local matter.*/ + object_instance = Keylist_Next_Empty_Key(Object_List, 1); + } + pObject = Keylist_Data(Object_List, object_instance); + if (!pObject) { + pObject = calloc(1, sizeof(struct object_data)); + if (!pObject) { + return BACNET_MAX_INSTANCE; + } + pObject->Object_Name = NULL; + pObject->Description = NULL; + pObject->Present_Value = 0.0; + pObject->Tracking_Value = 0.0; + pObject->Physical_Value = 0.0; + pObject->Lighting_Command.operation = BACNET_LIGHTS_NONE; + pObject->Lighting_Command.use_target_level = false; + pObject->Lighting_Command.use_ramp_rate = false; + pObject->Lighting_Command.use_step_increment = false; + pObject->Lighting_Command.use_fade_time = false; + pObject->Lighting_Command.use_priority = false; + pObject->In_Progress = BACNET_LIGHTING_IDLE; + pObject->Out_Of_Service = false; + pObject->Blink_Warn_Enable = false; + pObject->Egress_Active = false; + pObject->Egress_Time = 0; + pObject->Default_Fade_Time = 100; + pObject->Default_Ramp_Rate = 100.0; + pObject->Default_Step_Increment = 1.0; + pObject->Transition = BACNET_LIGHTING_TRANSITION_FADE; + pObject->Feedback_Value = 0.0; + for (p = 0; p < BACNET_MAX_PRIORITY; p++) { + pObject->Priority_Array[p] = 0.0; + BIT_CLEAR(pObject->Priority_Active_Bits, p); + } + pObject->Relinquish_Default = 0.0; + pObject->Power = 0.0; + pObject->Instantaneous_Power = 0.0; + pObject->Min_Actual_Value = 0.0; + pObject->Max_Actual_Value = 100.0; + pObject->Lighting_Command_Default_Priority = 16; + pObject->Color_Override = false; + pObject->Color_Reference.type = OBJECT_COLOR; + pObject->Color_Reference.instance = BACNET_MAX_INSTANCE; + pObject->Override_Color_Reference.type = OBJECT_COLOR; + pObject->Override_Color_Reference.instance = BACNET_MAX_INSTANCE; + /* add to list */ + index = Keylist_Data_Add(Object_List, object_instance, pObject); + if (index < 0) { + free(pObject); + return BACNET_MAX_INSTANCE; + } + } + + return object_instance; +} + +/** + * Deletes an object instance + * @param object_instance - object-instance number of the object + * @return true if the object is deleted + */ +bool Lighting_Output_Delete(uint32_t object_instance) +{ + bool status = false; + struct object_data *pObject = NULL; + + pObject = Keylist_Data_Delete(Object_List, object_instance); + if (pObject) { + free(pObject); + status = true; + } + + return status; +} + +/** + * Deletes all the objects and their data + */ +void Lighting_Output_Cleanup(void) +{ + struct object_data *pObject; + + if (Object_List) { + do { + pObject = Keylist_Data_Pop(Object_List); + if (pObject) { + free(pObject); + } + } while (pObject); + Keylist_Delete(Object_List); + Object_List = NULL; } } /** - * Initializes the Lighting Output object data + * Initializes the object list */ void Lighting_Output_Init(void) { - unsigned i, p; - - for (i = 0; i < MAX_LIGHTING_OUTPUTS; i++) { - Lighting_Output[i].Present_Value = 0.0; - Lighting_Output[i].Tracking_Value = 0.0; - Lighting_Output[i].Physical_Value = 0.0; - Lighting_Output[i].Lighting_Command.operation = BACNET_LIGHTS_NONE; - Lighting_Output[i].Lighting_Command.use_target_level = false; - Lighting_Output[i].Lighting_Command.use_ramp_rate = false; - Lighting_Output[i].Lighting_Command.use_step_increment = false; - Lighting_Output[i].Lighting_Command.use_fade_time = false; - Lighting_Output[i].Lighting_Command.use_priority = false; - Lighting_Output[i].In_Progress = BACNET_LIGHTING_IDLE; - Lighting_Output[i].Out_Of_Service = false; - Lighting_Output[i].Blink_Warn_Enable = false; - Lighting_Output[i].Egress_Active = false; - Lighting_Output[i].Egress_Time = 0; - Lighting_Output[i].Default_Fade_Time = 100; - Lighting_Output[i].Default_Ramp_Rate = 100.0; - Lighting_Output[i].Default_Step_Increment = 1.0; - Lighting_Output[i].Transition = BACNET_LIGHTING_TRANSITION_IDLE; - Lighting_Output[i].Feedback_Value = 0.0; - for (p = 0; p < BACNET_MAX_PRIORITY; p++) { - Lighting_Output[i].Priority_Array[p] = 0.0; - BIT_CLEAR(Lighting_Output[i].Priority_Active_Bits, p); - } - Lighting_Output[i].Relinquish_Default = 0.0; - Lighting_Output[i].Power = 0.0; - Lighting_Output[i].Instantaneous_Power = 0.0; - Lighting_Output[i].Min_Actual_Value = 0.0; - Lighting_Output[i].Max_Actual_Value = 100.0; - Lighting_Output[i].Lighting_Command_Default_Priority = 16; - } - - return; + Object_List = Keylist_Create(); } diff --git a/src/bacnet/basic/object/lo.h b/src/bacnet/basic/object/lo.h index af8751e5..0a5f7bbf 100644 --- a/src/bacnet/basic/object/lo.h +++ b/src/bacnet/basic/object/lo.h @@ -33,6 +33,15 @@ #include "bacnet/rp.h" #include "bacnet/wp.h" +/** + * @brief Callback for write present value request + * @param object_instance - object-instance number of the object + * @param old_value - value prior to write + * @param value - value of the write + */ +typedef void (*lighting_output_write_present_value_callback)( + uint32_t object_instance, float old_value, float value); + #ifdef __cplusplus extern "C" { #endif /* __cplusplus */ @@ -193,6 +202,14 @@ extern "C" { uint32_t object_instance, float step_increment); + BACNET_STACK_EXPORT + BACNET_LIGHTING_TRANSITION Lighting_Output_Transition( + uint32_t object_instance); + BACNET_STACK_EXPORT + bool Lighting_Output_Transition_Set( + uint32_t object_instance, + BACNET_LIGHTING_TRANSITION value); + BACNET_STACK_EXPORT unsigned Lighting_Output_Default_Priority( uint32_t object_instance); @@ -201,10 +218,47 @@ extern "C" { uint32_t object_instance, unsigned priority); + BACNET_STACK_EXPORT + bool Lighting_Output_Color_Override( + uint32_t object_instance); + BACNET_STACK_EXPORT + bool Lighting_Output_Color_Override_Set( + uint32_t object_instance, + bool value); + + BACNET_STACK_EXPORT + bool Lighting_Output_Color_Reference( + uint32_t object_instance, + BACNET_OBJECT_ID *value); + BACNET_STACK_EXPORT + bool Lighting_Output_Color_Reference_Set( + uint32_t object_instance, + BACNET_OBJECT_ID *value); + + BACNET_STACK_EXPORT + bool Lighting_Output_Override_Color_Reference( + uint32_t object_instance, + BACNET_OBJECT_ID *value); + BACNET_STACK_EXPORT + bool Lighting_Output_Override_Color_Reference_Set( + uint32_t object_instance, + BACNET_OBJECT_ID *value); + BACNET_STACK_EXPORT void Lighting_Output_Timer( + uint32_t object_instance, uint16_t milliseconds); + BACNET_STACK_EXPORT + void Lighting_Output_Write_Present_Value_Callback_Set( + lighting_output_write_present_value_callback cb); + + BACNET_STACK_EXPORT + uint32_t Lighting_Output_Create(uint32_t object_instance); + BACNET_STACK_EXPORT + bool Lighting_Output_Delete(uint32_t object_instance); + BACNET_STACK_EXPORT + void Lighting_Output_Cleanup(void); BACNET_STACK_EXPORT void Lighting_Output_Init( void); diff --git a/src/bacnet/basic/object/mso.c b/src/bacnet/basic/object/mso.c index 032828af..089e24d8 100644 --- a/src/bacnet/basic/object/mso.c +++ b/src/bacnet/basic/object/mso.c @@ -1164,9 +1164,9 @@ uint32_t Multistate_Output_Create(uint32_t object_instance) return BACNET_MAX_INSTANCE; } else if (object_instance == BACNET_MAX_INSTANCE) { /* wildcard instance */ - /* the Object_Identifier property of the newly created object - shall be initialized to a value that is unique within the - responding BACnet-user device. The method used to generate + /* the Object_Identifier property of the newly created object + shall be initialized to a value that is unique within the + responding BACnet-user device. The method used to generate the object identifier is a local matter.*/ object_instance = Keylist_Next_Empty_Key(Object_List, 1); } @@ -1186,9 +1186,7 @@ uint32_t Multistate_Output_Create(uint32_t object_instance) pObject->Relinquish_Default = 1; /* add to list */ index = Keylist_Data_Add(Object_List, object_instance, pObject); - if (index >= 0) { - Device_Inc_Database_Revision(); - } else { + if (index < 0) { free(pObject); return BACNET_MAX_INSTANCE; } @@ -1214,7 +1212,6 @@ bool Multistate_Output_Delete(uint32_t object_instance) if (pObject) { free(pObject); status = true; - Device_Inc_Database_Revision(); } return status; @@ -1232,7 +1229,6 @@ void Multistate_Output_Cleanup(void) pObject = Keylist_Data_Pop(Object_List); if (pObject) { free(pObject); - Device_Inc_Database_Revision(); } } while (pObject); Keylist_Delete(Object_List); diff --git a/src/bacnet/basic/sys/color_rgb.c b/src/bacnet/basic/sys/color_rgb.c index 3d08a06d..d593ba3d 100644 --- a/src/bacnet/basic/sys/color_rgb.c +++ b/src/bacnet/basic/sys/color_rgb.c @@ -41,14 +41,16 @@ static double clamp(double d, double min, double max) * @param x_coordinate - return x of CIE xy 0.0..1.0 * @param y_coordinate - return y of CIE xy 0.0..1.0 * @param brightness - return brightness of the CIE xy color 0..255 + * @param gamma_correction - true if gamma correction is applied * @note http://en.wikipedia.org/wiki/Srgb */ -void color_rgb_to_xy(uint8_t r, +static void color_rgb_to_xy_gamma_correction(uint8_t r, uint8_t g, uint8_t b, float *x_coordinate, float *y_coordinate, - uint8_t *brightness) + uint8_t *brightness, + bool gamma_correction) { float X, Y, Z; float x, y; @@ -63,18 +65,20 @@ void color_rgb_to_xy(uint8_t r, green /= 255.0f; blue /= 255.0f; - /* Apply a gamma correction to the RGB values, - which makes the color more vivid and more the - like the color displayed on the screen of your device. - This gamma correction is also applied to the screen - of your computer or phone, thus we need this to create - the same color on the light as on screen. */ - red = (red > 0.04045f) ? pow((red + 0.055f) / (1.0f + 0.055f), 2.4f) - : (red / 12.92f); - green = (green > 0.04045f) ? pow((green + 0.055f) / (1.0f + 0.055f), 2.4f) - : (green / 12.92f); - blue = (blue > 0.04045f) ? pow((blue + 0.055f) / (1.0f + 0.055f), 2.4f) - : (blue / 12.92f); + if (gamma_correction) { + /* Apply a gamma correction to the RGB values, + which makes the color more vivid and more the + like the color displayed on the screen of your device. + This gamma correction is also applied to the screen + of your computer or phone, thus we need this to create + the same color on the light as on screen. */ + red = (red > 0.04045f) ? pow((red + 0.055f) / (1.0f + 0.055f), 2.4f) + : (red / 12.92f); + green = (green > 0.04045f) ? pow((green + 0.055f) / (1.0f + 0.055f), 2.4f) + : (green / 12.92f); + blue = (blue > 0.04045f) ? pow((blue + 0.055f) / (1.0f + 0.055f), 2.4f) + : (blue / 12.92f); + } /* Convert the RGB values to XYZ using the Wide RGB D65 conversion formula */ @@ -107,6 +111,48 @@ void color_rgb_to_xy(uint8_t r, } } +/** + * @brief Convert sRGB to CIE xy + * @param r - R value of sRGB 0..255 + * @param g - G value of sRGB 0..255 + * @param b - B value of sRGB 0..255 + * @param x_coordinate - return x of CIE xy 0.0..1.0 + * @param y_coordinate - return y of CIE xy 0.0..1.0 + * @param brightness - return brightness of the CIE xy color 0..255 + * @note http://en.wikipedia.org/wiki/Srgb + */ +void color_rgb_to_xy(uint8_t r, + uint8_t g, + uint8_t b, + float *x_coordinate, + float *y_coordinate, + uint8_t *brightness) +{ + color_rgb_to_xy_gamma_correction(r, g, b, + x_coordinate, y_coordinate, brightness, false); +} + +/** + * @brief Convert sRGB to CIE xy, with gamma correction + * @param r - R value of sRGB 0..255 + * @param g - G value of sRGB 0..255 + * @param b - B value of sRGB 0..255 + * @param x_coordinate - return x of CIE xy 0.0..1.0 + * @param y_coordinate - return y of CIE xy 0.0..1.0 + * @param brightness - return brightness of the CIE xy color 0..255 + * @note http://en.wikipedia.org/wiki/Srgb + */ +void color_rgb_to_xy_gamma(uint8_t r, + uint8_t g, + uint8_t b, + float *x_coordinate, + float *y_coordinate, + uint8_t *brightness) +{ + color_rgb_to_xy_gamma_correction(r, g, b, + x_coordinate, y_coordinate, brightness, true); +} + /** * @brief Convert sRGB from CIE xy and brightness * @param red - return R value of sRGB @@ -115,14 +161,16 @@ void color_rgb_to_xy(uint8_t r, * @param x_coordinate - x of CIE xy * @param y_coordinate - y of CIE xy * @param brightness - brightness of the CIE xy color + * @param gamma_correction - true if gamma correction is needed * @note http://en.wikipedia.org/wiki/Srgb */ -void color_rgb_from_xy(uint8_t *red, +static void color_rgb_from_xy_gamma_correction(uint8_t *red, uint8_t *green, uint8_t *blue, float x_coordinate, float y_coordinate, - uint8_t brightness) + uint8_t brightness, + bool gamma_correction) { float r, g, b; float x, y, z, X, Y, Z; @@ -142,13 +190,15 @@ void color_rgb_from_xy(uint8_t *red, g = -X * 0.5217933f + Y * 1.4472381f + Z * 0.0677227f; b = X * 0.0349342f - Y * 0.0968930f + Z * 1.2884099f; - /* Apply reverse gamma correction */ - r = r <= 0.0031308f ? 12.92f * r - : (1.0f + 0.055f) * pow(r, (1.0f / 2.4f)) - 0.055f; - g = g <= 0.0031308f ? 12.92f * g - : (1.0f + 0.055f) * pow(g, (1.0f / 2.4f)) - 0.055f; - b = b <= 0.0031308f ? 12.92f * b - : (1.0f + 0.055f) * pow(b, (1.0f / 2.4f)) - 0.055f; + if (gamma_correction) { + /* Apply reverse gamma correction */ + r = r <= 0.0031308f ? 12.92f * r + : (1.0f + 0.055f) * pow(r, (1.0f / 2.4f)) - 0.055f; + g = g <= 0.0031308f ? 12.92f * g + : (1.0f + 0.055f) * pow(g, (1.0f / 2.4f)) - 0.055f; + b = b <= 0.0031308f ? 12.92f * b + : (1.0f + 0.055f) * pow(b, (1.0f / 2.4f)) - 0.055f; + } /* Convert the RGB values to your color object The rgb values from the above formulas are @@ -171,6 +221,48 @@ void color_rgb_from_xy(uint8_t *red, } } +/** + * @brief Convert sRGB from CIE xy and brightness + * @param red - return R value of sRGB + * @param green - return G value of sRGB + * @param blue - return B value of sRGB + * @param x_coordinate - x of CIE xy + * @param y_coordinate - y of CIE xy + * @param brightness - brightness of the CIE xy color + * @note http://en.wikipedia.org/wiki/Srgb + */ +void color_rgb_from_xy(uint8_t *red, + uint8_t *green, + uint8_t *blue, + float x_coordinate, + float y_coordinate, + uint8_t brightness) +{ + color_rgb_from_xy_gamma_correction(red, green, blue, + x_coordinate, y_coordinate, brightness, false); +} + +/** + * @brief Convert sRGB from CIE xy and brightness, with gamma correction + * @param red - return R value of sRGB + * @param green - return G value of sRGB + * @param blue - return B value of sRGB + * @param x_coordinate - x of CIE xy + * @param y_coordinate - y of CIE xy + * @param brightness - brightness of the CIE xy color + * @note http://en.wikipedia.org/wiki/Srgb + */ +void color_rgb_from_xy_gamma(uint8_t *red, + uint8_t *green, + uint8_t *blue, + float x_coordinate, + float y_coordinate, + uint8_t brightness) +{ + color_rgb_from_xy_gamma_correction(red, green, blue, + x_coordinate, y_coordinate, brightness, true); +} + /* table for converting RGB to and from ASCII color names */ struct css_color_rgb { const char *name; diff --git a/src/bacnet/basic/sys/color_rgb.h b/src/bacnet/basic/sys/color_rgb.h index 2e754f82..2052d76b 100644 --- a/src/bacnet/basic/sys/color_rgb.h +++ b/src/bacnet/basic/sys/color_rgb.h @@ -22,6 +22,13 @@ BACNET_STACK_EXPORT void color_rgb_from_xy(uint8_t *red, uint8_t *green, uint8_t *blue, float x_coordinate, float y_coordinate, uint8_t brightness); +BACNET_STACK_EXPORT +void color_rgb_to_xy_gamma(uint8_t r, uint8_t g, uint8_t b, + float *x_coordinate, float *y_coordinate, uint8_t *brightness); +BACNET_STACK_EXPORT +void color_rgb_from_xy_gamma(uint8_t *red, uint8_t *green, uint8_t *blue, + float x_coordinate, float y_coordinate, uint8_t brightness); + BACNET_STACK_EXPORT const char * color_rgb_to_ascii(uint8_t red, uint8_t green, uint8_t blue); BACNET_STACK_EXPORT diff --git a/src/bacnet/basic/sys/linear.c b/src/bacnet/basic/sys/linear.c new file mode 100644 index 00000000..62d6d1a6 --- /dev/null +++ b/src/bacnet/basic/sys/linear.c @@ -0,0 +1,128 @@ +/** +* @file +* @author Steve Karg +* @date 2011 +* @brief Performs linear interpolation using single precision floating +* point math or integer math, or a mixture of both. +* +* @section LICENSE +* +* Copyright (C) 2011 Steve Karg +* +* Permission is hereby granted, free of charge, to any person obtaining +* a copy of this software and associated documentation files (the +* "Software"), to deal in the Software without restriction, including +* without limitation the rights to use, copy, modify, merge, publish, +* distribute, sublicense, and/or sell copies of the Software, and to +* permit persons to whom the Software is furnished to do so, subject to +* the following conditions: +* +* The above copyright notice and this permission notice shall be included +* in all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +* +* @section DESCRIPTION +* +* Performs linear interpolation using single precision floating +* point math or integer math, or a mixture of both. +* Linear interpolation is a method of constructing new data +* points within the range of a discrete set of known data points. +*/ +#include +#include +#include "linear.h" + +/** +* Linearly Interpolate the values between y1 and y3 based on x. +* +* @param x1 - first x value, where x1 <= x2 <= x3 or x1 >= x2 >= x3 +* @param x2 - intermediate x value, where x1 <= x2 <= x3 or x1 >= x2 >= x3 +* @param x3 - last x value, where x1 <= x2 <= x3 or x1 >= x2 >= x3 +* @param y1 - first y value, where y1 <= y2 <= y3 or y1 >= y2 >= y3 +* @param y3 - last y value, where y1 <= y2 <= y3 or y1 >= y2 >= y3 +* @return y2 - an intermediate y value y1 <= y2 <= y3 or y1 >= y2 >= y3 +* and the y value is linearly proportional to x1, x2, and x2. +*/ +float linear_interpolate(float x1, + float x2, + float x3, + float y1, + float y3) +{ + float y2; + + if (y3 > y1) { + y2 = y1 + (((x2 - x1) * (y3 - y1)) / (x3 - x1)); + } else { + y2 = y1 - (((x2 - x1) * (y1 - y3)) / (x3 - x1)); + } + + return y2; +} + +/** +* Linearly Interpolate the values between y1 and y3 based on x +* and round up the result. Useful for integer interpolation. +* +* @param x1 - first x value, where x1 <= x2 <= x3 or x1 >= x2 >= x3 +* @param x2 - intermediate x value, where x1 <= x2 <= x3 or x1 >= x2 >= x3 +* @param x3 - last x value, where x1 <= x2 <= x3 or x1 >= x2 >= x3 +* @param y1 - first y value, where y1 <= y2 <= y3 or y1 >= y2 >= y3 +* @param y3 - last y value, where y1 <= y2 <= y3 or y1 >= y2 >= y3 +* @return y2 - an intermediate y value y1 <= y2 <= y3 or y1 >= y2 >= y3 +* and the y value is linearly proportional to x1, x2, and x2. +*/ +float linear_interpolate_round(float x1, + float x2, + float x3, + float y1, + float y3) +{ + float y2; + + y2 = linear_interpolate(x1, x2, x3, y1, y3); + /* round away from zero */ + if (y2 > 0.0) { + y2 += 0.5; + } else if (y2 < 0.0) { + y2 -= 0.5; + } + + return y2; +} + +/** +* Linearly Interpolate the values between y1 and y3 based on x +* using integer math. +* +* @param x1 - first x value, where x1 <= x2 <= x3 or x1 >= x2 >= x3 +* @param x2 - intermediate x value, where x1 <= x2 <= x3 or x1 >= x2 >= x3 +* @param x3 - last x value, where x1 <= x2 <= x3 or x1 >= x2 >= x3 +* @param y1 - first y value, where y1 <= y2 <= y3 or y1 >= y2 >= y3 +* @param y3 - last y value, where y1 <= y2 <= y3 or y1 >= y2 >= y3 +* @return y2 - an intermediate y value y1 <= y2 <= y3 or y1 >= y2 >= y3 +* and the y value is linearly proportional to x1, x2, and x2. +*/ +long linear_interpolate_int(long x1, + long x2, + long x3, + long y1, + long y3) +{ + long y2; + + if (y3 > y1) { + y2 = y1 + (((x2 - x1) * (y3 - y1)) / (x3 - x1)); + } else { + y2 = y1 - (((x2 - x1) * (y1 - y3)) / (x3 - x1)); + } + + return y2; +} diff --git a/src/bacnet/basic/sys/linear.h b/src/bacnet/basic/sys/linear.h new file mode 100644 index 00000000..1bf4c0e1 --- /dev/null +++ b/src/bacnet/basic/sys/linear.h @@ -0,0 +1,34 @@ +/** +* @file +* @author Steve Karg +* @date 2011 +*/ +#ifndef LINEAR_H +#define LINEAR_H + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + + float linear_interpolate(float x1, + float x2, + float x3, + float y1, + float y3); + + float linear_interpolate_round(float x1, + float x2, + float x3, + float y1, + float y3); + + long linear_interpolate_int(long x1, + long x2, + long x3, + long y1, + long y3); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ +#endif diff --git a/src/bacnet/lighting.c b/src/bacnet/lighting.c index 0bf09f86..ed1d23bf 100644 --- a/src/bacnet/lighting.c +++ b/src/bacnet/lighting.c @@ -25,6 +25,18 @@ /** * Encodes into bytes from the lighting-command structure * + * BACnetLightingCommand ::= SEQUENCE { + * operation [0] BACnetLightingOperation, + * target-level [1] REAL (0.0..100.0) OPTIONAL, + * ramp-rate [2] REAL (0.1..100.0) OPTIONAL, + * step-increment [3] REAL (0.1..100.0) OPTIONAL, + * fade-time [4] Unsigned (100..86400000) OPTIONAL, + * priority [5] Unsigned (1..16) OPTIONAL + * } + * -- Note that the combination of level, ramp-rate, + * -- step-increment, and fade-time fields is + * -- dependent on the specific lighting operation. + * * @param apdu - buffer to hold the bytes, or null for length * @param value - lighting command value to encode * @@ -86,6 +98,18 @@ int lighting_command_encode(uint8_t *apdu, BACNET_LIGHTING_COMMAND *data) * Encodes into bytes from the lighting-command structure * a context tagged chunk (opening and closing tag) * + * BACnetLightingCommand ::= SEQUENCE { + * operation [0] BACnetLightingOperation, + * target-level [1] REAL (0.0..100.0) OPTIONAL, + * ramp-rate [2] REAL (0.1..100.0) OPTIONAL, + * step-increment [3] REAL (0.1..100.0) OPTIONAL, + * fade-time [4] Unsigned (100..86400000) OPTIONAL, + * priority [5] Unsigned (1..16) OPTIONAL + * } + * -- Note that the combination of level, ramp-rate, + * -- step-increment, and fade-time fields is + * -- dependent on the specific lighting operation. + * * @param apdu - buffer to hold the bytes * @param tag_number - tag number to encode this chunk * @param value - lighting command value to encode @@ -114,58 +138,64 @@ int lighting_command_encode_context( /** * Decodes from bytes into the lighting-command structure * + * BACnetLightingCommand ::= SEQUENCE { + * operation [0] BACnetLightingOperation, + * target-level [1] REAL (0.0..100.0) OPTIONAL, + * ramp-rate [2] REAL (0.1..100.0) OPTIONAL, + * step-increment [3] REAL (0.1..100.0) OPTIONAL, + * fade-time [4] Unsigned (100..86400000) OPTIONAL, + * priority [5] Unsigned (1..16) OPTIONAL + * } + * -- Note that the combination of level, ramp-rate, + * -- step-increment, and fade-time fields is + * -- dependent on the specific lighting operation. + * * @param apdu - buffer to hold the bytes - * @param apdu_max_len - number of bytes in the buffer to decode + * @param apdu_size - number of bytes in the buffer to decode * @param value - lighting command value to place the decoded values * - * @return number of bytes decoded + * @return number of bytes decoded, or BACNET_STATUS_ERROR */ int lighting_command_decode( - uint8_t *apdu, unsigned apdu_max_len, BACNET_LIGHTING_COMMAND *data) + uint8_t *apdu, unsigned apdu_size, BACNET_LIGHTING_COMMAND *data) { int len = 0; int apdu_len = 0; - uint8_t tag_number = 0; - uint32_t len_value_type = 0; uint32_t enum_value = 0; BACNET_UNSIGNED_INTEGER unsigned_value = 0; BACNET_LIGHTING_OPERATION operation = BACNET_LIGHTS_NONE; float real_value = 0.0; /* check for value pointers */ - if (apdu_max_len) { - /* Tag 0: operation */ - if (!decode_is_context_tag(&apdu[apdu_len], 0)) { + if (!apdu) { + return BACNET_STATUS_ERROR; + } + /* operation [0] BACnetLightingOperation */ + len = bacnet_enumerated_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 0, &enum_value); + if (len > 0) { + apdu_len += len; + if (unsigned_value <= BACNET_LIGHTS_PROPRIETARY_LAST) { + if (data) { + data->operation = (BACNET_LIGHTING_OPERATION)enum_value; + } + } else { return BACNET_STATUS_ERROR; } - len = decode_tag_number_and_value( - &apdu[apdu_len], &tag_number, &len_value_type); - apdu_len += len; - len = decode_enumerated(&apdu[apdu_len], len_value_type, &enum_value); - if (len > 0) { - if (unsigned_value <= BACNET_LIGHTS_PROPRIETARY_LAST) { - if (data) { - data->operation = (BACNET_LIGHTING_OPERATION)enum_value; - } - } else { - return BACNET_STATUS_ERROR; - } - } - apdu_len += len; + } else { + return BACNET_STATUS_ERROR; } switch (operation) { case BACNET_LIGHTS_NONE: break; case BACNET_LIGHTS_FADE_TO: - if ((apdu_max_len - apdu_len) == 0) { + if ((apdu_size - apdu_len) == 0) { return BACNET_STATUS_REJECT; } - /* Tag 1: target-level */ - if (decode_is_context_tag(&apdu[apdu_len], 1)) { - len = decode_tag_number_and_value( - &apdu[apdu_len], &tag_number, &len_value_type); - apdu_len += len; - len = decode_real(&apdu[apdu_len], &real_value); + /* target-level [1] REAL (0.0..100.0) OPTIONAL */ + len = bacnet_real_context_decode( + &apdu[apdu_len], apdu_size, 1, &real_value); + if (len > 0) { apdu_len += len; if (data) { data->target_level = real_value; @@ -176,33 +206,29 @@ int lighting_command_decode( data->use_target_level = false; } } - if ((apdu_max_len - apdu_len) != 0) { + if ((apdu_size - apdu_len) > 0) { /* Tag 4: fade-time - OPTIONAL */ - if (decode_is_context_tag(&apdu[apdu_len], 4)) { - len = decode_tag_number_and_value( - &apdu[apdu_len], &tag_number, &len_value_type); - apdu_len += len; - len = decode_unsigned( - &apdu[apdu_len], len_value_type, &unsigned_value); + len = bacnet_unsigned_context_decode( + &apdu[apdu_len], apdu_size, 4, &unsigned_value); + if (len > 0) { apdu_len += len; if (data) { data->fade_time = (uint32_t)unsigned_value; data->use_fade_time = true; } } else { - if (data) { - data->use_fade_time = false; - } + return BACNET_STATUS_ERROR; + } + } else { + if (data) { + data->use_fade_time = false; } } - if ((apdu_max_len - apdu_len) != 0) { - /* Tag 5: priority - OPTIONAL */ - if (decode_is_context_tag(&apdu[apdu_len], 4)) { - len = decode_tag_number_and_value( - &apdu[apdu_len], &tag_number, &len_value_type); - apdu_len += len; - len = decode_unsigned( - &apdu[apdu_len], len_value_type, &unsigned_value); + if ((apdu_size - apdu_len) > 0) { + /* priority [5] Unsigned (1..16) OPTIONAL */ + len = bacnet_unsigned_context_decode( + &apdu[apdu_len], apdu_size, 5, &unsigned_value); + if (len > 0) { apdu_len += len; if (data) { data->priority = (uint8_t)unsigned_value; @@ -213,18 +239,20 @@ int lighting_command_decode( data->use_priority = false; } } + } else { + if (data) { + data->use_priority = false; + } } break; case BACNET_LIGHTS_RAMP_TO: - if ((apdu_max_len - apdu_len) == 0) { + if ((apdu_size - apdu_len) == 0) { return BACNET_STATUS_REJECT; } - /* Tag 1: target-level */ - if (decode_is_context_tag(&apdu[apdu_len], 1)) { - len = decode_tag_number_and_value( - &apdu[apdu_len], &tag_number, &len_value_type); - apdu_len += len; - len = decode_real(&apdu[apdu_len], &real_value); + /* target-level [1] REAL (0.0..100.0) OPTIONAL */ + len = bacnet_real_context_decode( + &apdu[apdu_len], apdu_size, 1, &real_value); + if (len > 0) { apdu_len += len; if (data) { data->target_level = real_value; @@ -235,13 +263,11 @@ int lighting_command_decode( data->use_target_level = false; } } - if ((apdu_max_len - apdu_len) != 0) { - /* Tag 2: ramp-rate - OPTIONAL */ - if (decode_is_context_tag(&apdu[apdu_len], 2)) { - len = decode_tag_number_and_value( - &apdu[apdu_len], &tag_number, &len_value_type); - apdu_len += len; - len = decode_real(&apdu[apdu_len], &real_value); + if ((apdu_size - apdu_len) > 0) { + /* ramp-rate [2] REAL (0.1..100.0) OPTIONAL */ + len = bacnet_real_context_decode( + &apdu[apdu_len], apdu_size, 2, &real_value); + if (len > 0) { apdu_len += len; if (data) { data->ramp_rate = real_value; @@ -252,15 +278,16 @@ int lighting_command_decode( data->use_ramp_rate = false; } } + } else { + if (data) { + data->use_ramp_rate = false; + } } - if ((apdu_max_len - apdu_len) != 0) { - /* Tag 5: priority - OPTIONAL */ - if (decode_is_context_tag(&apdu[apdu_len], 4)) { - len = decode_tag_number_and_value( - &apdu[apdu_len], &tag_number, &len_value_type); - apdu_len += len; - len = decode_unsigned( - &apdu[apdu_len], len_value_type, &unsigned_value); + if ((apdu_size - apdu_len) > 0) { + /* priority [5] Unsigned (1..16) OPTIONAL */ + len = bacnet_unsigned_context_decode( + &apdu[apdu_len], apdu_size, 5, &unsigned_value); + if (len > 0) { apdu_len += len; if (data) { data->priority = (uint8_t)unsigned_value; @@ -271,19 +298,21 @@ int lighting_command_decode( data->use_priority = false; } } + } else { + if (data) { + data->use_priority = false; + } } break; case BACNET_LIGHTS_STEP_UP: case BACNET_LIGHTS_STEP_DOWN: case BACNET_LIGHTS_STEP_ON: case BACNET_LIGHTS_STEP_OFF: - if ((apdu_max_len - apdu_len) != 0) { - /* Tag 3: step-increment - OPTIONAL */ - if (decode_is_context_tag(&apdu[apdu_len], 3)) { - len = decode_tag_number_and_value( - &apdu[apdu_len], &tag_number, &len_value_type); - apdu_len += len; - len = decode_real(&apdu[apdu_len], &real_value); + if ((apdu_size - apdu_len) > 0) { + /* step-increment [3] REAL (0.1..100.0) OPTIONAL */ + len = bacnet_real_context_decode( + &apdu[apdu_len], apdu_size, 3, &real_value); + if (len > 0) { apdu_len += len; if (data) { data->step_increment = real_value; @@ -294,15 +323,16 @@ int lighting_command_decode( data->use_step_increment = false; } } + } else { + if (data) { + data->use_step_increment = false; + } } - if ((apdu_max_len - apdu_len) != 0) { - /* Tag 5: priority - OPTIONAL */ - if (decode_is_context_tag(&apdu[apdu_len], 4)) { - len = decode_tag_number_and_value( - &apdu[apdu_len], &tag_number, &len_value_type); - apdu_len += len; - len = decode_unsigned( - &apdu[apdu_len], len_value_type, &unsigned_value); + if ((apdu_size - apdu_len) > 0) { + /* priority [5] Unsigned (1..16) OPTIONAL */ + len = bacnet_unsigned_context_decode( + &apdu[apdu_len], apdu_size, 5, &unsigned_value); + if (len > 0) { apdu_len += len; if (data) { data->priority = (uint8_t)unsigned_value; @@ -313,20 +343,21 @@ int lighting_command_decode( data->use_priority = false; } } + } else { + if (data) { + data->use_priority = false; + } } break; case BACNET_LIGHTS_WARN: case BACNET_LIGHTS_WARN_OFF: case BACNET_LIGHTS_WARN_RELINQUISH: case BACNET_LIGHTS_STOP: - if ((apdu_max_len - apdu_len) != 0) { - /* Tag 5: priority - OPTIONAL */ - if (decode_is_context_tag(&apdu[apdu_len], 4)) { - len = decode_tag_number_and_value( - &apdu[apdu_len], &tag_number, &len_value_type); - apdu_len += len; - len = decode_unsigned( - &apdu[apdu_len], len_value_type, &unsigned_value); + if ((apdu_size - apdu_len) > 0) { + /* priority [5] Unsigned (1..16) OPTIONAL */ + len = bacnet_unsigned_context_decode( + &apdu[apdu_len], apdu_size, 5, &unsigned_value); + if (len > 0) { apdu_len += len; if (data) { data->priority = (uint8_t)unsigned_value; @@ -337,6 +368,10 @@ int lighting_command_decode( data->use_priority = false; } } + } else { + if (data) { + data->use_priority = false; + } } break; default: @@ -437,20 +472,16 @@ int xy_color_encode(uint8_t *apdu, BACNET_XY_COLOR *value) { int len = 0; int apdu_len = 0; - uint8_t *apdu_offset = NULL; if (value) { /* x-coordinate REAL */ - if (apdu) { - apdu_offset = &apdu[apdu_len]; - } - len = encode_bacnet_real(value->x_coordinate, apdu_offset); + len = encode_application_real(apdu, value->x_coordinate); apdu_len += len; - /* y-coordinate REAL */ if (apdu) { - apdu_offset = &apdu[apdu_len]; + apdu += len; } - len = encode_bacnet_real(value->y_coordinate, apdu_offset); + /* y-coordinate REAL */ + len = encode_application_real(apdu, value->y_coordinate); apdu_len += len; } @@ -497,7 +528,7 @@ int xy_color_context_encode( * @param apdu_size - the size of the data buffer * @param value - decoded BACnetxyColor, if decoded * - * @return the number of apdu bytes consumed + * @return the number of apdu bytes consumed, or BACNET_STATUS_ERROR */ int xy_color_decode(uint8_t *apdu, uint32_t apdu_size, BACNET_XY_COLOR *value) { @@ -505,21 +536,26 @@ int xy_color_decode(uint8_t *apdu, uint32_t apdu_size, BACNET_XY_COLOR *value) int len = 0; int apdu_len = 0; - if (apdu && value && (apdu_size >= 8)) { - /* each REAL is encoded in 4 octets */ - len = decode_real(&apdu[0], &real_value); - if (len == 4) { + if (apdu) { + len = bacnet_real_application_decode( + &apdu[apdu_len], apdu_size, &real_value); + if (len > 0) { if (value) { value->x_coordinate = real_value; } apdu_len += len; + } else { + return BACNET_STATUS_ERROR; } - len = decode_real(&apdu[4], &real_value); - if (len == 4) { + len = bacnet_real_application_decode( + &apdu[apdu_len], apdu_size, &real_value); + if (len > 0) { if (value) { value->y_coordinate = real_value; } apdu_len += len; + } else { + return BACNET_STATUS_ERROR; } } @@ -533,7 +569,7 @@ int xy_color_decode(uint8_t *apdu, uint32_t apdu_size, BACNET_XY_COLOR *value) * @param apdu_size - the size of the data buffer * @param value - decoded BACnetxyColor, if decoded * - * @return the number of apdu bytes consumed + * @return the number of apdu bytes consumed, or BACNET_STATUS_ERROR */ int xy_color_context_decode(uint8_t *apdu, uint32_t apdu_size, @@ -541,33 +577,45 @@ int xy_color_context_decode(uint8_t *apdu, BACNET_XY_COLOR *value) { int len = 0; - int rlen = 0; int apdu_len = 0; BACNET_XY_COLOR color = { 0.0, 0.0 }; - if (apdu_size > 0) { - if (decode_is_opening_tag_number(&apdu[apdu_len], tag_number)) { - apdu_len += 1; - len = - xy_color_decode(&apdu[apdu_len], apdu_size - apdu_len, &color); - if (len > 0) { - apdu_len += len; - if (value) { - value->x_coordinate = color.x_coordinate; - value->y_coordinate = color.y_coordinate; - } - if ((apdu_size - apdu_len) > 0) { - if (decode_is_closing_tag_number( - &apdu[apdu_len], tag_number)) { - apdu_len += 1; - rlen = apdu_len; - } - } - } - } + if (!bacnet_is_opening_tag_number( + &apdu[apdu_len], apdu_size - apdu_len, tag_number, &len)) { + return BACNET_STATUS_ERROR; } + apdu_len += len; + len = xy_color_decode(&apdu[apdu_len], apdu_size - apdu_len, &color); + if (len > 0) { + apdu_len += len; + if (value) { + value->x_coordinate = color.x_coordinate; + value->y_coordinate = color.y_coordinate; + } + } else { + return BACNET_STATUS_ERROR; + } + if (!bacnet_is_closing_tag_number( + &apdu[apdu_len], apdu_size - apdu_len, tag_number, &len)) { + return BACNET_STATUS_ERROR; + } + apdu_len += len; - return rlen; + return apdu_len; +} + +/** + * @brief Set the BACnetxyColor complex data for x and y coordinates + * @param dst - destination BACNET_XY_COLOR structure + * @param x - x coordinate + * @param x - y coordinate + */ +void xy_color_set(BACNET_XY_COLOR *dst, float x, float y) +{ + if (dst) { + dst->x_coordinate = x; + dst->y_coordinate = y; + } } /** @@ -609,6 +657,64 @@ bool xy_color_same(BACNET_XY_COLOR *value1, BACNET_XY_COLOR *value2) return status; } +/** + * Convert BACnetXYcolor to ASCII for printing + * + * @param value - struct to convert to ASCII + * @param buf - ASCII output buffer (or NULL for size) + * @param buf_size - ASCII output buffer capacity (or 0 for size) + * + * @return the number of characters which would be generated for the given + * input, excluding the trailing null. negative is returned if the + * capacity was not sufficient. + * + * @note buf and buf_size may be null and zero to return only the size + */ +int xy_color_to_ascii( + const BACNET_XY_COLOR *value, + char *buf, + size_t buf_size) +{ + return snprintf(buf, buf_size, "(%f,%f)", value->x_coordinate, + value->x_coordinate); +} + +/** + * @brief Parse an ASCII string for a BACnetXYcolor + * @param mac [out] BACNET_XY_COLOR structure to store the results + * @param arg [in] nul terminated ASCII string to parse + * @return true if the address was parsed + */ +bool xy_color_from_ascii(BACNET_XY_COLOR *value, const char *argv) +{ + bool status = false; + int count; + float x,y; + + count = sscanf(argv, "%f,%f", &x, &y); + if (count == 2) { + value->x_coordinate = x; + value->y_coordinate = y; + status = true; + } else { +#if defined(BACAPP_COLOR_RGB_CONVERSION_ENABLED) + uint8_t red, green, blue; + unsigned rgb_max; + + rgb_max = color_rgb_count(); + count = color_rgb_from_ascii(&red, &green, &blue, argv); + if (count < rgb_max) { + color_rgb_to_xy(red, green, blue, &x, &y, NULL); + value->x_coordinate = x; + value->y_coordinate = y; + status = true; + } +#endif + } + + return status; +} + /** * @brief Encode a BACnetColorCommand complex data type * @@ -629,82 +735,62 @@ int color_command_encode(uint8_t *apdu, BACNET_COLOR_COMMAND *value) { int len = 0; int apdu_len = 0; - uint8_t *apdu_offset = NULL; BACNET_UNSIGNED_INTEGER unsigned_value; if (value) { /* operation [0] BACnetColorOperation */ - if (apdu) { - apdu_offset = &apdu[apdu_len]; - } - len = encode_context_enumerated(apdu_offset, 0, value->operation); + len = encode_context_enumerated(apdu, 0, value->operation); apdu_len += len; + if (apdu) { + apdu += len; + } switch (value->operation) { case BACNET_COLOR_OPERATION_NONE: break; case BACNET_COLOR_OPERATION_FADE_TO_COLOR: - if (apdu) { - apdu_offset = &apdu[apdu_len]; - } /* target-color [1] BACnetxyColor */ - len = xy_color_context_encode( - apdu_offset, 1, &value->target.color); + len = xy_color_context_encode(apdu, 1, &value->target.color); apdu_len += len; + if (apdu) { + apdu += len; + } if ((value->transit.fade_time >= BACNET_COLOR_FADE_TIME_MIN) && (value->transit.fade_time <= BACNET_COLOR_FADE_TIME_MAX)) { - /* fade-time [3] Unsigned (100.. 86400000) */ + /* fade-time [3] Unsigned (100.. 86400000) OPTIONAL */ unsigned_value = value->transit.fade_time; - if (apdu) { - apdu_offset = &apdu[apdu_len]; - } - len = - encode_context_unsigned(apdu_offset, 3, unsigned_value); + len = encode_context_unsigned(apdu, 3, unsigned_value); apdu_len += len; } break; case BACNET_COLOR_OPERATION_FADE_TO_CCT: - if (apdu) { - apdu_offset = &apdu[apdu_len]; - } /* target-color-temperature [2] Unsigned */ unsigned_value = value->target.color_temperature; - if (apdu) { - apdu_offset = &apdu[apdu_len]; - } - len = encode_context_unsigned(apdu_offset, 2, unsigned_value); + len = encode_context_unsigned(apdu, 2, unsigned_value); apdu_len += len; + if (apdu) { + apdu += len; + } if ((value->transit.fade_time >= BACNET_COLOR_FADE_TIME_MIN) && (value->transit.fade_time <= BACNET_COLOR_FADE_TIME_MAX)) { - /* fade-time [3] Unsigned (100.. 86400000) */ + /* fade-time [3] Unsigned (100.. 86400000) OPTIONAL */ unsigned_value = value->transit.fade_time; - if (apdu) { - apdu_offset = &apdu[apdu_len]; - } - len = - encode_context_unsigned(apdu_offset, 3, unsigned_value); + len = encode_context_unsigned(apdu, 3, unsigned_value); apdu_len += len; } break; case BACNET_COLOR_OPERATION_RAMP_TO_CCT: - if (apdu) { - apdu_offset = &apdu[apdu_len]; - } /* target-color-temperature [2] Unsigned */ unsigned_value = value->target.color_temperature; - if (apdu) { - apdu_offset = &apdu[apdu_len]; - } - len = encode_context_unsigned(apdu_offset, 2, unsigned_value); + len = encode_context_unsigned(apdu, 2, unsigned_value); apdu_len += len; + if (apdu) { + apdu += len; + } if ((value->transit.ramp_rate >= BACNET_COLOR_RAMP_RATE_MIN) && (value->transit.ramp_rate <= BACNET_COLOR_RAMP_RATE_MAX)) { - /* ramp-rate [4] Unsigned (1..30000) */ + /* ramp-rate [4] Unsigned (1..30000) OPTIONAL */ unsigned_value = value->transit.ramp_rate; - if (apdu) { - apdu_offset = &apdu[apdu_len]; - } - len = - encode_context_unsigned(apdu_offset, 4, unsigned_value); + len = encode_context_unsigned(apdu, 4, unsigned_value); apdu_len += len; } break; @@ -714,16 +800,9 @@ int color_command_encode(uint8_t *apdu, BACNET_COLOR_COMMAND *value) BACNET_COLOR_STEP_INCREMENT_MIN) && (value->transit.step_increment <= BACNET_COLOR_STEP_INCREMENT_MAX)) { - if (apdu) { - apdu_offset = &apdu[apdu_len]; - } - /* step-increment [5] Unsigned (1..30000) */ + /* step-increment [5] Unsigned (1..30000) OPTIONAL */ unsigned_value = value->transit.step_increment; - if (apdu) { - apdu_offset = &apdu[apdu_len]; - } - len = - encode_context_unsigned(apdu_offset, 5, unsigned_value); + len = encode_context_unsigned(apdu, 5, unsigned_value); apdu_len += len; } break; @@ -749,21 +828,19 @@ int color_command_context_encode( { int len = 0; int apdu_len = 0; - uint8_t *apdu_offset = NULL; if (value) { - apdu_offset = apdu; - len = encode_opening_tag(apdu_offset, tag_number); + len = encode_opening_tag(apdu, tag_number); apdu_len += len; if (apdu) { - apdu_offset = &apdu[apdu_len]; + apdu += len; } - len = color_command_encode(apdu_offset, value); + len = color_command_encode(apdu, value); apdu_len += len; if (apdu) { - apdu_offset = &apdu[apdu_len]; + apdu += len; } - len = encode_closing_tag(apdu_offset, tag_number); + len = encode_closing_tag(apdu, tag_number); apdu_len += len; } @@ -808,8 +885,10 @@ int color_command_decode(uint8_t *apdu, } /* operation [0] BACnetColorOperation */ len = bacnet_unsigned_context_decode( - apdu, apdu_size - apdu_len, 0, &unsigned_value); - if (len <= 0) { + &apdu[apdu_len], apdu_size - apdu_len, 0, &unsigned_value); + if (len > 0) { + apdu_len += len; + } else { if (len == 0) { if (error_code) { *error_code = ERROR_CODE_REJECT_INVALID_TAG; @@ -821,7 +900,6 @@ int color_command_decode(uint8_t *apdu, } return BACNET_STATUS_REJECT; } - apdu_len += len; if (unsigned_value >= BACNET_COLOR_OPERATION_MAX) { if (error_code) { *error_code = ERROR_CODE_REJECT_PARAMETER_OUT_OF_RANGE; @@ -837,30 +915,27 @@ int color_command_decode(uint8_t *apdu, break; case BACNET_COLOR_OPERATION_FADE_TO_COLOR: /* target-color [1] BACnetxyColor */ - if ((apdu_size - apdu_len) == 0) { - if (error_code) { - *error_code = ERROR_CODE_REJECT_MISSING_REQUIRED_PARAMETER; - } - return BACNET_STATUS_REJECT; - } len = xy_color_context_decode( &apdu[apdu_len], apdu_size - apdu_len, 1, &color); - if (len == 0) { + if (len > 0) { + apdu_len += len; + } else { if (error_code) { *error_code = ERROR_CODE_REJECT_MISSING_REQUIRED_PARAMETER; } return BACNET_STATUS_REJECT; } - apdu_len += len; if (value) { value->target.color.x_coordinate = color.x_coordinate; value->target.color.y_coordinate = color.y_coordinate; } - if ((apdu_size - apdu_len) != 0) { + if ((apdu_size - apdu_len) > 0) { /* fade-time [3] Unsigned (100.. 86400000) OPTIONAL */ len = bacnet_unsigned_context_decode( &apdu[apdu_len], apdu_size - apdu_len, 3, &unsigned_value); - if (len <= 0) { + if (len > 0) { + apdu_len += len; + } else { if (len == 0) { if (error_code) { *error_code = ERROR_CODE_REJECT_INVALID_TAG; @@ -873,7 +948,6 @@ int color_command_decode(uint8_t *apdu, } return BACNET_STATUS_REJECT; } - apdu_len += len; if ((unsigned_value < BACNET_COLOR_FADE_TIME_MIN) || (unsigned_value > BACNET_COLOR_FADE_TIME_MAX)) { if (error_code) { @@ -881,22 +955,20 @@ int color_command_decode(uint8_t *apdu, } return BACNET_STATUS_REJECT; } - if (value) { - value->transit.fade_time = unsigned_value; - } + } else { + unsigned_value = 0; + } + if (value) { + value->transit.fade_time = unsigned_value; } break; case BACNET_COLOR_OPERATION_FADE_TO_CCT: /* target-color-temperature [2] Unsigned */ - if ((apdu_size - apdu_len) == 0) { - if (error_code) { - *error_code = ERROR_CODE_REJECT_MISSING_REQUIRED_PARAMETER; - } - return BACNET_STATUS_REJECT; - } len = bacnet_unsigned_context_decode( - apdu, apdu_size - apdu_len, 2, &unsigned_value); - if (len <= 0) { + &apdu[apdu_len], apdu_size - apdu_len, 2, &unsigned_value); + if (len > 0) { + apdu_len += len; + } else { if (len == 0) { if (error_code) { *error_code = ERROR_CODE_REJECT_INVALID_TAG; @@ -909,7 +981,6 @@ int color_command_decode(uint8_t *apdu, } return BACNET_STATUS_REJECT; } - apdu_len += len; if (unsigned_value > UINT16_MAX) { if (error_code) { *error_code = ERROR_CODE_REJECT_PARAMETER_OUT_OF_RANGE; @@ -919,11 +990,13 @@ int color_command_decode(uint8_t *apdu, if (value) { value->target.color_temperature = unsigned_value; } - if ((apdu_size - apdu_len) != 0) { + if ((apdu_size - apdu_len) > 0) { /* fade-time [3] Unsigned (100.. 86400000) OPTIONAL */ len = bacnet_unsigned_context_decode( &apdu[apdu_len], apdu_size - apdu_len, 3, &unsigned_value); - if (len <= 0) { + if (len > 0) { + apdu_len += len; + } else { if (len == 0) { if (error_code) { *error_code = ERROR_CODE_REJECT_INVALID_TAG; @@ -936,7 +1009,6 @@ int color_command_decode(uint8_t *apdu, } return BACNET_STATUS_REJECT; } - apdu_len += len; if ((unsigned_value < BACNET_COLOR_FADE_TIME_MIN) || (unsigned_value > BACNET_COLOR_FADE_TIME_MAX)) { if (error_code) { @@ -944,22 +1016,20 @@ int color_command_decode(uint8_t *apdu, } return BACNET_STATUS_REJECT; } - if (value) { - value->transit.fade_time = unsigned_value; - } + } else { + unsigned_value = 0; + } + if (value) { + value->transit.fade_time = unsigned_value; } break; case BACNET_COLOR_OPERATION_RAMP_TO_CCT: /* target-color-temperature [2] Unsigned */ - if ((apdu_size - apdu_len) == 0) { - if (error_code) { - *error_code = ERROR_CODE_REJECT_MISSING_REQUIRED_PARAMETER; - } - return BACNET_STATUS_REJECT; - } len = bacnet_unsigned_context_decode( - apdu, apdu_size - apdu_len, 2, &unsigned_value); - if (len <= 0) { + &apdu[apdu_len], apdu_size - apdu_len, 2, &unsigned_value); + if (len > 0) { + apdu_len += len; + } else { if (len == 0) { if (error_code) { *error_code = ERROR_CODE_REJECT_INVALID_TAG; @@ -972,7 +1042,6 @@ int color_command_decode(uint8_t *apdu, } return BACNET_STATUS_REJECT; } - apdu_len += len; if (unsigned_value > UINT16_MAX) { if (error_code) { *error_code = ERROR_CODE_REJECT_PARAMETER_OUT_OF_RANGE; @@ -982,11 +1051,13 @@ int color_command_decode(uint8_t *apdu, if (value) { value->target.color_temperature = unsigned_value; } - if ((apdu_size - apdu_len) != 0) { - /* ramp-rate [4] Unsigned (1..30000) */ + if ((apdu_size - apdu_len) > 0) { + /* ramp-rate [4] Unsigned (1..30000) OPTIONAL */ len = bacnet_unsigned_context_decode( &apdu[apdu_len], apdu_size - apdu_len, 4, &unsigned_value); - if (len <= 0) { + if (len > 0) { + apdu_len += len; + } else { if (len == 0) { if (error_code) { *error_code = ERROR_CODE_REJECT_INVALID_TAG; @@ -999,7 +1070,6 @@ int color_command_decode(uint8_t *apdu, } return BACNET_STATUS_REJECT; } - apdu_len += len; if ((unsigned_value < BACNET_COLOR_RAMP_RATE_MIN) || (unsigned_value > BACNET_COLOR_RAMP_RATE_MAX)) { if (error_code) { @@ -1007,42 +1077,43 @@ int color_command_decode(uint8_t *apdu, } return BACNET_STATUS_REJECT; } - if (value) { - value->transit.ramp_rate = unsigned_value; - } + } else { + unsigned_value = 0; + } + if (value) { + value->transit.ramp_rate = unsigned_value; } break; case BACNET_COLOR_OPERATION_STEP_UP_CCT: case BACNET_COLOR_OPERATION_STEP_DOWN_CCT: - /* step-increment [5] Unsigned (1..30000) */ - if ((apdu_size - apdu_len) == 0) { - if (error_code) { - *error_code = ERROR_CODE_REJECT_MISSING_REQUIRED_PARAMETER; - } - return BACNET_STATUS_REJECT; - } - len = bacnet_unsigned_context_decode( - apdu, apdu_size - apdu_len, 3, &unsigned_value); - if (len <= 0) { - if (len == 0) { - if (error_code) { - *error_code = ERROR_CODE_REJECT_INVALID_TAG; - } + if ((apdu_size - apdu_len) > 0) { + /* step-increment [5] Unsigned (1..30000) OPTIONAL */ + len = bacnet_unsigned_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 5, &unsigned_value); + if (len > 0) { + apdu_len += len; } else { - if (error_code) { - *error_code = - ERROR_CODE_REJECT_MISSING_REQUIRED_PARAMETER; + if (len == 0) { + if (error_code) { + *error_code = ERROR_CODE_REJECT_INVALID_TAG; + } + } else { + if (error_code) { + *error_code = + ERROR_CODE_REJECT_MISSING_REQUIRED_PARAMETER; + } } + return BACNET_STATUS_REJECT; } - return BACNET_STATUS_REJECT; - } - apdu_len += len; - if ((unsigned_value < BACNET_COLOR_STEP_INCREMENT_MIN) || - (unsigned_value > BACNET_COLOR_STEP_INCREMENT_MAX)) { - if (error_code) { - *error_code = ERROR_CODE_REJECT_PARAMETER_OUT_OF_RANGE; + if ((unsigned_value < BACNET_COLOR_STEP_INCREMENT_MIN) || + (unsigned_value > BACNET_COLOR_STEP_INCREMENT_MAX)) { + if (error_code) { + *error_code = ERROR_CODE_REJECT_PARAMETER_OUT_OF_RANGE; + } + return BACNET_STATUS_REJECT; } - return BACNET_STATUS_REJECT; + } else { + unsigned_value = 0; } if (value) { value->transit.step_increment = unsigned_value; diff --git a/src/bacnet/lighting.h b/src/bacnet/lighting.h index 2b9f83a8..caf3f528 100644 --- a/src/bacnet/lighting.h +++ b/src/bacnet/lighting.h @@ -86,6 +86,9 @@ typedef struct BACnetColorCommand { #define BACNET_COLOR_STEP_INCREMENT_MIN 1ul #define BACNET_COLOR_STEP_INCREMENT_MAX 30000ul +#define BACNET_COLOR_TEMPERATURE_MIN 1000ul +#define BACNET_COLOR_TEMPERATURE_MAX 30000ul + #ifdef __cplusplus extern "C" { #endif /* __cplusplus */ @@ -140,6 +143,20 @@ extern "C" { bool xy_color_same( BACNET_XY_COLOR *value1, BACNET_XY_COLOR *value2); + BACNET_STACK_EXPORT + void xy_color_set( + BACNET_XY_COLOR *dst, + float x, + float y); + BACNET_STACK_EXPORT + int xy_color_to_ascii( + const BACNET_XY_COLOR *value, + char *buf, + size_t buf_size); + BACNET_STACK_EXPORT + bool xy_color_from_ascii( + BACNET_XY_COLOR *value, + const char *arg); BACNET_STACK_EXPORT int color_command_encode( diff --git a/src/bacnet/wp.c b/src/bacnet/wp.c index 2195539e..6cfc742c 100644 --- a/src/bacnet/wp.c +++ b/src/bacnet/wp.c @@ -66,7 +66,6 @@ int wp_encode_apdu( { int apdu_len = 0; /* total length of the apdu, return value */ int len = 0; /* total length of the apdu, return value */ - int imax = 0; /* maximum application data length */ if (!wpdata) { return BACNET_STATUS_ERROR; @@ -155,8 +154,6 @@ int wp_decode_service_request( { int len = 0; int apdu_len = 0; - int tag_len = 0; - uint8_t tag_number = 0; uint32_t instance = 0; BACNET_OBJECT_TYPE type = OBJECT_NONE; /* for decoding */ uint32_t property = 0; /* for decoding */ diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 56417dfc..b3da4f14 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -136,6 +136,7 @@ list(APPEND testdirs bacnet/basic/sys/fifo bacnet/basic/sys/filename bacnet/basic/sys/keylist + bacnet/basic/sys/linear bacnet/basic/sys/ringbuf bacnet/basic/sys/sbuf ) diff --git a/test/bacnet/basic/object/channel/CMakeLists.txt b/test/bacnet/basic/object/channel/CMakeLists.txt index a740efbf..2cc35733 100644 --- a/test/bacnet/basic/object/channel/CMakeLists.txt +++ b/test/bacnet/basic/object/channel/CMakeLists.txt @@ -45,8 +45,9 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/bactext.c ${SRC_DIR}/bacnet/bactimevalue.c ${SRC_DIR}/bacnet/basic/sys/bigend.c - ${SRC_DIR}/bacnet/datetime.c ${SRC_DIR}/bacnet/basic/sys/days.c + ${SRC_DIR}/bacnet/basic/sys/keylist.c + ${SRC_DIR}/bacnet/datetime.c ${SRC_DIR}/bacnet/indtext.c ${SRC_DIR}/bacnet/hostnport.c ${SRC_DIR}/bacnet/lighting.c diff --git a/test/bacnet/basic/object/channel/src/main.c b/test/bacnet/basic/object/channel/src/main.c index d0487e71..87304816 100644 --- a/test/bacnet/basic/object/channel/src/main.c +++ b/test/bacnet/basic/object/channel/src/main.c @@ -24,22 +24,30 @@ static void test_Channel_ReadProperty(void) uint8_t apdu[MAX_APDU] = { 0 }; int len = 0; int test_len = 0; - BACNET_READ_PROPERTY_DATA rpdata; - /* for decode value data */ - BACNET_APPLICATION_DATA_VALUE value; + BACNET_READ_PROPERTY_DATA rpdata = { 0 }; + BACNET_APPLICATION_DATA_VALUE value = { 0 }; + BACNET_WRITE_PROPERTY_DATA wpdata = { 0 }; + const uint32_t instance = 123; const int *pRequired = NULL; const int *pOptional = NULL; const int *pProprietary = NULL; unsigned count = 0; bool status = false; + unsigned index; Channel_Init(); + Channel_Create(instance); + status = Channel_Valid_Instance(instance); + zassert_true(status, NULL); + index = Channel_Instance_To_Index(instance); + zassert_equal(index, 0, NULL); + count = Channel_Count(); zassert_true(count > 0, NULL); rpdata.application_data = &apdu[0]; rpdata.application_data_len = sizeof(apdu); rpdata.object_type = OBJECT_CHANNEL; - rpdata.object_instance = Channel_Index_To_Instance(0);; + rpdata.object_instance = Channel_Index_To_Instance(0); status = Channel_Valid_Instance(rpdata.object_instance); zassert_true(status, NULL); Channel_Property_Lists(&pRequired, &pOptional, &pProprietary); @@ -47,22 +55,37 @@ static void test_Channel_ReadProperty(void) rpdata.object_property = *pRequired; rpdata.array_index = BACNET_ARRAY_ALL; len = Channel_Read_Property(&rpdata); - zassert_not_equal(len, BACNET_STATUS_ERROR, NULL); + zassert_not_equal(len, BACNET_STATUS_ERROR, + "property '%s': failed to ReadProperty!\n", + bactext_property_name(rpdata.object_property)); if (len > 0) { test_len = bacapp_decode_application_data(rpdata.application_data, (uint8_t)rpdata.application_data_len, &value); - if (len != test_len) { - printf("property '%s': failed to decode!\n", - bactext_property_name(rpdata.object_property)); - } - if (rpdata.object_property == PROP_PRIORITY_ARRAY) { + if ((rpdata.object_property == PROP_PRIORITY_ARRAY) || + (rpdata.object_property == PROP_CONTROL_GROUPS) || + (rpdata.object_property == + PROP_LIST_OF_OBJECT_PROPERTY_REFERENCES)) { /* FIXME: known fail to decode */ len = test_len; } - zassert_true(test_len >= 0, NULL); - } else { - printf("property '%s': failed to read!\n", + zassert_equal(len, test_len, "property '%s': failed to decode!\n", bactext_property_name(rpdata.object_property)); + /* check WriteProperty properties */ + wpdata.object_type = rpdata.object_type; + wpdata.object_instance = rpdata.object_instance; + wpdata.object_property = rpdata.object_property; + wpdata.array_index = rpdata.array_index; + memcpy(&wpdata.application_data, rpdata.application_data, MAX_APDU); + wpdata.application_data_len = len; + wpdata.error_code = ERROR_CODE_SUCCESS; + status = Channel_Write_Property(&wpdata); + if (!status) { + /* verify WriteProperty property is known */ + zassert_not_equal(wpdata.error_code, + ERROR_CODE_UNKNOWN_PROPERTY, + "property '%s': WriteProperty Unknown!\n", + bactext_property_name(rpdata.object_property)); + } } pRequired++; } @@ -70,21 +93,41 @@ static void test_Channel_ReadProperty(void) rpdata.object_property = *pOptional; rpdata.array_index = BACNET_ARRAY_ALL; len = Channel_Read_Property(&rpdata); - zassert_not_equal(len, BACNET_STATUS_ERROR, NULL); + zassert_not_equal(len, BACNET_STATUS_ERROR, + "property '%s': failed to ReadProperty!\n", + bactext_property_name(rpdata.object_property)); if (len > 0) { test_len = bacapp_decode_application_data(rpdata.application_data, (uint8_t)rpdata.application_data_len, &value); - if (len != test_len) { - printf("property '%s': failed to decode!\n", + zassert_equal(len, test_len, "property '%s': failed to decode!\n", + bactext_property_name(rpdata.object_property)); + /* check WriteProperty properties */ + wpdata.object_type = rpdata.object_type; + wpdata.object_instance = rpdata.object_instance; + wpdata.object_property = rpdata.object_property; + wpdata.array_index = rpdata.array_index; + memcpy(&wpdata.application_data, rpdata.application_data, MAX_APDU); + wpdata.application_data_len = len; + wpdata.error_code = ERROR_CODE_SUCCESS; + status = Channel_Write_Property(&wpdata); + if (!status) { + /* verify WriteProperty property is known */ + zassert_not_equal(wpdata.error_code, + ERROR_CODE_UNKNOWN_PROPERTY, + "property '%s': WriteProperty Unknown!\n", bactext_property_name(rpdata.object_property)); } - zassert_true(test_len >= 0, NULL); - } else { - printf("property '%s': failed to read!\n", - bactext_property_name(rpdata.object_property)); } pOptional++; } + rpdata.object_property = PROP_ALL; + len = Channel_Read_Property(&rpdata); + zassert_equal(len, BACNET_STATUS_ERROR, NULL); + wpdata.object_property = PROP_ALL; + status = Channel_Write_Property(&wpdata); + zassert_false(status, NULL); + status = Channel_Delete(instance); + zassert_true(status, NULL); } /** * @} @@ -92,8 +135,7 @@ static void test_Channel_ReadProperty(void) void test_main(void) { - ztest_test_suite(channel_tests, - ztest_unit_test(test_Channel_ReadProperty)); + ztest_test_suite(channel_tests, ztest_unit_test(test_Channel_ReadProperty)); ztest_run_test_suite(channel_tests); } diff --git a/test/bacnet/basic/object/color_object/CMakeLists.txt b/test/bacnet/basic/object/color_object/CMakeLists.txt index ac4815f9..59fd44b2 100644 --- a/test/bacnet/basic/object/color_object/CMakeLists.txt +++ b/test/bacnet/basic/object/color_object/CMakeLists.txt @@ -45,6 +45,7 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/bactext.c ${SRC_DIR}/bacnet/basic/sys/bigend.c ${SRC_DIR}/bacnet/basic/sys/keylist.c + ${SRC_DIR}/bacnet/basic/sys/linear.c ${SRC_DIR}/bacnet/cov.c ${SRC_DIR}/bacnet/datetime.c ${SRC_DIR}/bacnet/basic/sys/days.c diff --git a/test/bacnet/basic/object/color_object/src/main.c b/test/bacnet/basic/object/color_object/src/main.c index d0767b8a..71d9e918 100644 --- a/test/bacnet/basic/object/color_object/src/main.c +++ b/test/bacnet/basic/object/color_object/src/main.c @@ -27,13 +27,22 @@ static void testColorObject(void) uint8_t apdu[MAX_APDU] = { 0 }; int len = 0; int test_len = 0; - BACNET_READ_PROPERTY_DATA rpdata = {0}; - BACNET_APPLICATION_DATA_VALUE value = {0}; - const int *required_property = NULL; + BACNET_READ_PROPERTY_DATA rpdata = { 0 }; + BACNET_APPLICATION_DATA_VALUE value = { 0 }; + const int *pRequired = NULL; + const int *pOptional = NULL; + const int *pProprietary = NULL; const uint32_t instance = 123; + BACNET_WRITE_PROPERTY_DATA wpdata = { 0 }; + bool status = false; + unsigned index; Color_Init(); Color_Create(instance); + status = Color_Valid_Instance(instance); + zassert_true(status, NULL); + index = Color_Instance_To_Index(instance); + zassert_equal(index, 0, NULL); rpdata.application_data = &apdu[0]; rpdata.application_data_len = sizeof(apdu); @@ -41,23 +50,77 @@ static void testColorObject(void) rpdata.object_instance = instance; rpdata.object_property = PROP_OBJECT_IDENTIFIER; - Color_Property_Lists(&required_property, NULL, NULL); - while ((*required_property) >= 0) { - rpdata.object_property = *required_property; + Color_Property_Lists(&pRequired, &pOptional, &pProprietary); + while ((*pRequired) >= 0) { + rpdata.object_property = *pRequired; rpdata.array_index = BACNET_ARRAY_ALL; len = Color_Read_Property(&rpdata); - zassert_true(len >= 0, NULL); + zassert_not_equal(len, BACNET_STATUS_ERROR, + "property '%s': failed to ReadProperty!\n", + bactext_property_name(rpdata.object_property)); if (len >= 0) { test_len = bacapp_decode_known_property(rpdata.application_data, len, &value, rpdata.object_type, rpdata.object_property); - if (len != test_len) { - printf("property '%s': failed to decode!\n", + zassert_equal(len, test_len, "property '%s': failed to decode!\n", + bactext_property_name(rpdata.object_property)); + /* check WriteProperty properties */ + wpdata.object_type = rpdata.object_type; + wpdata.object_instance = rpdata.object_instance; + wpdata.object_property = rpdata.object_property; + wpdata.array_index = rpdata.array_index; + memcpy(&wpdata.application_data, rpdata.application_data, MAX_APDU); + wpdata.application_data_len = len; + wpdata.error_code = ERROR_CODE_SUCCESS; + status = Color_Write_Property(&wpdata); + if (!status) { + /* verify WriteProperty property is known */ + zassert_not_equal(wpdata.error_code, + ERROR_CODE_UNKNOWN_PROPERTY, + "property '%s': WriteProperty Unknown!\n", bactext_property_name(rpdata.object_property)); } - zassert_equal(len, test_len, NULL); } - required_property++; + pRequired++; } + while ((*pOptional) != -1) { + rpdata.object_property = *pOptional; + rpdata.array_index = BACNET_ARRAY_ALL; + len = Color_Read_Property(&rpdata); + zassert_not_equal(len, BACNET_STATUS_ERROR, + "property '%s': failed to ReadProperty!\n", + bactext_property_name(rpdata.object_property)); + if (len > 0) { + test_len = bacapp_decode_application_data(rpdata.application_data, + (uint8_t)rpdata.application_data_len, &value); + zassert_equal(len, test_len, "property '%s': failed to decode!\n", + bactext_property_name(rpdata.object_property)); + /* check WriteProperty properties */ + wpdata.object_type = rpdata.object_type; + wpdata.object_instance = rpdata.object_instance; + wpdata.object_property = rpdata.object_property; + wpdata.array_index = rpdata.array_index; + memcpy(&wpdata.application_data, rpdata.application_data, MAX_APDU); + wpdata.application_data_len = len; + wpdata.error_code = ERROR_CODE_SUCCESS; + status = Color_Write_Property(&wpdata); + if (!status) { + /* verify WriteProperty property is known */ + zassert_not_equal(wpdata.error_code, + ERROR_CODE_UNKNOWN_PROPERTY, + "property '%s': WriteProperty Unknown!\n", + bactext_property_name(rpdata.object_property)); + } + } + pOptional++; + } + rpdata.object_property = PROP_ALL; + len = Color_Read_Property(&rpdata); + zassert_equal(len, BACNET_STATUS_ERROR, NULL); + wpdata.object_property = PROP_ALL; + status = Color_Write_Property(&wpdata); + zassert_false(status, NULL); + status = Color_Delete(instance); + zassert_true(status, NULL); return; } @@ -65,15 +128,12 @@ static void testColorObject(void) * @} */ - #if defined(CONFIG_ZTEST_NEW_API) ZTEST_SUITE(color_object_tests, NULL, NULL, NULL, NULL, NULL); #else void test_main(void) { - ztest_test_suite(color_object_tests, - ztest_unit_test(testColorObject) - ); + ztest_test_suite(color_object_tests, ztest_unit_test(testColorObject)); ztest_run_test_suite(color_object_tests); } diff --git a/test/bacnet/basic/object/color_temperature/CMakeLists.txt b/test/bacnet/basic/object/color_temperature/CMakeLists.txt index 951b951d..f398eea9 100644 --- a/test/bacnet/basic/object/color_temperature/CMakeLists.txt +++ b/test/bacnet/basic/object/color_temperature/CMakeLists.txt @@ -45,6 +45,7 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/bactext.c ${SRC_DIR}/bacnet/basic/sys/bigend.c ${SRC_DIR}/bacnet/basic/sys/keylist.c + ${SRC_DIR}/bacnet/basic/sys/linear.c ${SRC_DIR}/bacnet/cov.c ${SRC_DIR}/bacnet/datetime.c ${SRC_DIR}/bacnet/basic/sys/days.c diff --git a/test/bacnet/basic/object/color_temperature/src/main.c b/test/bacnet/basic/object/color_temperature/src/main.c index 4a6b7cc9..a74e0467 100644 --- a/test/bacnet/basic/object/color_temperature/src/main.c +++ b/test/bacnet/basic/object/color_temperature/src/main.c @@ -30,8 +30,12 @@ static void testColorTemperature(void) int test_len = 0; BACNET_READ_PROPERTY_DATA rpdata = {0}; BACNET_APPLICATION_DATA_VALUE value = {0}; - const int *required_property = NULL; + const int *pRequired = NULL; + const int *pOptional = NULL; + const int *pProprietary = NULL; const uint32_t instance = 123; + BACNET_WRITE_PROPERTY_DATA wpdata = { 0 }; + bool status = false; Color_Temperature_Init(); Color_Temperature_Create(instance); @@ -42,23 +46,78 @@ static void testColorTemperature(void) rpdata.object_instance = instance; rpdata.object_property = PROP_OBJECT_IDENTIFIER; - Color_Temperature_Property_Lists(&required_property, NULL, NULL); - while ((*required_property) >= 0) { - rpdata.object_property = *required_property; + Color_Temperature_Property_Lists(&pRequired, &pOptional, &pProprietary); + while ((*pRequired) >= 0) { + rpdata.object_property = *pRequired; rpdata.array_index = BACNET_ARRAY_ALL; len = Color_Temperature_Read_Property(&rpdata); - zassert_true(len >= 0, NULL); + zassert_not_equal(len, BACNET_STATUS_ERROR, + "property '%s': failed to ReadProperty!\n", + bactext_property_name(rpdata.object_property)); if (len >= 0) { test_len = bacapp_decode_known_property(rpdata.application_data, len, &value, rpdata.object_type, rpdata.object_property); - if (len != test_len) { - printf("property '%s': failed to decode!\n", + zassert_equal(len, test_len, "property '%s': failed to decode!\n", + bactext_property_name(rpdata.object_property)); + /* check WriteProperty properties */ + wpdata.object_type = rpdata.object_type; + wpdata.object_instance = rpdata.object_instance; + wpdata.object_property = rpdata.object_property; + wpdata.array_index = rpdata.array_index; + memcpy(&wpdata.application_data, rpdata.application_data, MAX_APDU); + wpdata.application_data_len = len; + wpdata.error_code = ERROR_CODE_SUCCESS; + status = Color_Temperature_Write_Property(&wpdata); + if (!status) { + /* verify WriteProperty property is known */ + zassert_not_equal(wpdata.error_code, + ERROR_CODE_UNKNOWN_PROPERTY, + "property '%s': WriteProperty Unknown!\n", bactext_property_name(rpdata.object_property)); } - zassert_equal(len, test_len, NULL); } - required_property++; + pRequired++; } + while ((*pOptional) != -1) { + rpdata.object_property = *pOptional; + rpdata.array_index = BACNET_ARRAY_ALL; + len = Color_Temperature_Read_Property(&rpdata); + zassert_not_equal(len, BACNET_STATUS_ERROR, + "property '%s': failed to ReadProperty!\n", + bactext_property_name(rpdata.object_property)); + if (len > 0) { + test_len = bacapp_decode_application_data( + rpdata.application_data, + (uint8_t)rpdata.application_data_len, &value); + zassert_equal(len, test_len, "property '%s': failed to decode!\n", + bactext_property_name(rpdata.object_property)); + /* check WriteProperty properties */ + wpdata.object_type = rpdata.object_type; + wpdata.object_instance = rpdata.object_instance; + wpdata.object_property = rpdata.object_property; + wpdata.array_index = rpdata.array_index; + memcpy(&wpdata.application_data, rpdata.application_data, MAX_APDU); + wpdata.application_data_len = len; + wpdata.error_code = ERROR_CODE_SUCCESS; + status = Color_Temperature_Write_Property(&wpdata); + if (!status) { + /* verify WriteProperty property is known */ + zassert_not_equal(wpdata.error_code, + ERROR_CODE_UNKNOWN_PROPERTY, + "property '%s': WriteProperty Unknown!\n", + bactext_property_name(rpdata.object_property)); + } + } + pOptional++; + } + rpdata.object_property = PROP_ALL; + len = Color_Temperature_Read_Property(&rpdata); + zassert_equal(len, BACNET_STATUS_ERROR, NULL); + wpdata.object_property = PROP_ALL; + status = Color_Temperature_Write_Property(&wpdata); + zassert_false(status, NULL); + status = Color_Temperature_Delete(instance); + zassert_true(status, NULL); return; } diff --git a/test/bacnet/basic/object/device/CMakeLists.txt b/test/bacnet/basic/object/device/CMakeLists.txt index c6bb0b64..c287b70d 100644 --- a/test/bacnet/basic/object/device/CMakeLists.txt +++ b/test/bacnet/basic/object/device/CMakeLists.txt @@ -77,6 +77,7 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/sys/bigend.c ${SRC_DIR}/bacnet/basic/sys/debug.c ${SRC_DIR}/bacnet/basic/sys/keylist.c + ${SRC_DIR}/bacnet/basic/sys/linear.c ${SRC_DIR}/bacnet/basic/tsm/tsm.c ${SRC_DIR}/bacnet/datalink/bvlc.c ${SRC_DIR}/bacnet/cov.c diff --git a/test/bacnet/basic/object/lo/CMakeLists.txt b/test/bacnet/basic/object/lo/CMakeLists.txt index cbdd938a..e8de6ac2 100644 --- a/test/bacnet/basic/object/lo/CMakeLists.txt +++ b/test/bacnet/basic/object/lo/CMakeLists.txt @@ -45,8 +45,11 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/bacstr.c ${SRC_DIR}/bacnet/bactext.c ${SRC_DIR}/bacnet/basic/sys/bigend.c - ${SRC_DIR}/bacnet/datetime.c ${SRC_DIR}/bacnet/basic/sys/days.c + ${SRC_DIR}/bacnet/basic/sys/color_rgb.c + ${SRC_DIR}/bacnet/basic/sys/keylist.c + ${SRC_DIR}/bacnet/basic/sys/linear.c + ${SRC_DIR}/bacnet/datetime.c ${SRC_DIR}/bacnet/indtext.c ${SRC_DIR}/bacnet/hostnport.c ${SRC_DIR}/bacnet/lighting.c @@ -57,6 +60,10 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/dailyschedule.c # Test and test library files ./src/main.c + ../mock/device_mock.c ${ZTST_DIR}/ztest_mock.c ${ZTST_DIR}/ztest.c ) + +target_link_libraries(${PROJECT_NAME} PRIVATE + m) diff --git a/test/bacnet/basic/object/lo/src/main.c b/test/bacnet/basic/object/lo/src/main.c index e75104fb..367ed8fe 100644 --- a/test/bacnet/basic/object/lo/src/main.c +++ b/test/bacnet/basic/object/lo/src/main.c @@ -30,36 +30,104 @@ static void testLightingOutput(void) int len = 0, test_len = 0; BACNET_READ_PROPERTY_DATA rpdata; BACNET_APPLICATION_DATA_VALUE value = {0}; - const int *required_property = NULL; - const uint32_t instance = 1; + const int *pRequired = NULL; + const int *pOptional = NULL; + const int *pProprietary = NULL; + const uint32_t instance = 123; + BACNET_WRITE_PROPERTY_DATA wpdata = { 0 }; + bool status = false; + unsigned index; + uint16_t milliseconds = 10; Lighting_Output_Init(); + Lighting_Output_Create(instance); + status = Lighting_Output_Valid_Instance(instance); + zassert_true(status, NULL); + index = Lighting_Output_Instance_To_Index(instance); + zassert_equal(index, 0, NULL); + rpdata.application_data = &apdu[0]; rpdata.application_data_len = sizeof(apdu); rpdata.object_type = OBJECT_LIGHTING_OUTPUT; rpdata.object_instance = instance; rpdata.array_index = BACNET_ARRAY_ALL; - Lighting_Output_Property_Lists(&required_property, NULL, NULL); - while ((*required_property) >= 0) { - rpdata.object_property = *required_property; + Lighting_Output_Property_Lists(&pRequired, &pOptional, &pProprietary); + while ((*pRequired) >= 0) { + rpdata.object_property = *pRequired; + rpdata.array_index = BACNET_ARRAY_ALL; len = Lighting_Output_Read_Property(&rpdata); - zassert_true(len >= 0, NULL); + zassert_not_equal(len, BACNET_STATUS_ERROR, + "property '%s': failed to ReadProperty!\n", + bactext_property_name(rpdata.object_property)); if (len >= 0) { test_len = bacapp_decode_known_property(rpdata.application_data, len, &value, rpdata.object_type, rpdata.object_property); - if (len != test_len) { - printf("property '%s': failed to decode!\n", + if (rpdata.object_property != PROP_PRIORITY_ARRAY) { + zassert_equal(len, test_len, "property '%s': failed to decode!\n", + bactext_property_name(rpdata.object_property)); + } + /* check WriteProperty properties */ + wpdata.object_type = rpdata.object_type; + wpdata.object_instance = rpdata.object_instance; + wpdata.object_property = rpdata.object_property; + wpdata.array_index = rpdata.array_index; + memcpy(&wpdata.application_data, rpdata.application_data, MAX_APDU); + wpdata.application_data_len = len; + wpdata.error_code = ERROR_CODE_SUCCESS; + status = Lighting_Output_Write_Property(&wpdata); + if (!status) { + /* verify WriteProperty property is known */ + zassert_not_equal(wpdata.error_code, + ERROR_CODE_UNKNOWN_PROPERTY, + "property '%s': WriteProperty Unknown!\n", bactext_property_name(rpdata.object_property)); } - if (rpdata.object_property == PROP_PRIORITY_ARRAY) { - /* FIXME: known fail to decode */ - len = test_len; - } - zassert_equal(len, test_len, NULL); } - required_property++; + pRequired++; } + while ((*pOptional) != -1) { + rpdata.object_property = *pOptional; + rpdata.array_index = BACNET_ARRAY_ALL; + len = Lighting_Output_Read_Property(&rpdata); + zassert_not_equal(len, BACNET_STATUS_ERROR, + "property '%s': failed to ReadProperty!\n", + bactext_property_name(rpdata.object_property)); + if (len > 0) { + test_len = bacapp_decode_application_data(rpdata.application_data, + (uint8_t)rpdata.application_data_len, &value); + zassert_equal(len, test_len, "property '%s': failed to decode!\n", + bactext_property_name(rpdata.object_property)); + /* check WriteProperty properties */ + wpdata.object_type = rpdata.object_type; + wpdata.object_instance = rpdata.object_instance; + wpdata.object_property = rpdata.object_property; + wpdata.array_index = rpdata.array_index; + memcpy(&wpdata.application_data, rpdata.application_data, MAX_APDU); + wpdata.application_data_len = len; + wpdata.error_code = ERROR_CODE_SUCCESS; + status = Lighting_Output_Write_Property(&wpdata); + if (!status) { + /* verify WriteProperty property is known */ + zassert_not_equal(wpdata.error_code, + ERROR_CODE_UNKNOWN_PROPERTY, + "property '%s': WriteProperty Unknown!\n", + bactext_property_name(rpdata.object_property)); + } + } + pOptional++; + } + /* check for unsupported property - use ALL */ + rpdata.object_property = PROP_ALL; + len = Lighting_Output_Read_Property(&rpdata); + zassert_equal(len, BACNET_STATUS_ERROR, NULL); + status = Lighting_Output_Write_Property(&wpdata); + zassert_false(status, NULL); + /* check the dimming/ramping/stepping engine*/ + Lighting_Output_Timer(instance, milliseconds); + /* check the delete function */ + status = Lighting_Output_Delete(instance); + zassert_true(status, NULL); return; } diff --git a/test/bacnet/basic/sys/color_rgb/src/main.c b/test/bacnet/basic/sys/color_rgb/src/main.c index f6b81037..16c995a3 100644 --- a/test/bacnet/basic/sys/color_rgb/src/main.c +++ b/test/bacnet/basic/sys/color_rgb/src/main.c @@ -8,6 +8,9 @@ * * SPDX-License-Identifier: MIT */ +#include +#include +#include #include #include @@ -16,34 +19,69 @@ * @{ */ +/** + * @brief compare two floating point values to 3 decimal places + * + * @param x1 - first comparison value + * @param x2 - second comparison value + * @return true if the value is the same to 3 decimal points + */ +static bool is_float_equal(float x1, float x2) +{ + return fabs(x1 - x2) < 0.001; +} + /** * Unit Test for sRGB to CIE xy */ -static void test_color_rgb_xy_unit( - uint8_t red, uint8_t green, uint8_t blue, - float x_coordinate, float y_coordinate, +static void test_color_rgb_xy_gamma_unit(uint8_t red, + uint8_t green, + uint8_t blue, + float x_coordinate, + float y_coordinate, + uint8_t brightness) +{ + float test_x_coordinate = 0.0, test_y_coordinate = 0.0; + uint8_t test_brightness = 0; + uint8_t test_red = 0, test_green = 0, test_blue = 0; + + /* functions with gamma correction */ + color_rgb_to_xy_gamma(red, green, blue, &test_x_coordinate, + &test_y_coordinate, &test_brightness); + color_rgb_from_xy_gamma(&test_red, &test_green, &test_blue, x_coordinate, + y_coordinate, brightness); + zassert_true(is_float_equal(x_coordinate, test_x_coordinate), + "(x=%.3f,test_x=%.3f)", x_coordinate, test_x_coordinate); + zassert_true(is_float_equal(y_coordinate, test_y_coordinate), + "(y=%.3f,test_y=%.3f)", y_coordinate, test_y_coordinate); + zassert_equal(brightness, test_brightness, "b=%u, test_b=%u", brightness, + test_brightness); +} + +/** + * Unit Test for sRGB to CIE xy + */ +static void test_color_rgb_xy_unit(uint8_t red, + uint8_t green, + uint8_t blue, + float x_coordinate, + float y_coordinate, uint8_t brightness) { float test_x_coordinate = 0.0, test_y_coordinate = 0.0; uint8_t test_brightness = 0; uint8_t test_red = 0, test_green = 0, test_blue = 0; - printf("test value:(%u,%u,%u)=(%.3f,%.3f,%u)\n", - (unsigned)red, (unsigned)green, (unsigned)blue, - x_coordinate, y_coordinate, (unsigned)brightness); color_rgb_to_xy(red, green, blue, &test_x_coordinate, &test_y_coordinate, &test_brightness); - color_rgb_from_xy(&test_red, &test_green, &test_blue, - x_coordinate, y_coordinate, brightness); - printf("calculated:(%u,%u,%u)=(%.3f,%.3f,%u)\n", - (unsigned)test_red, (unsigned)test_green, (unsigned)test_blue, - test_x_coordinate, test_y_coordinate, (unsigned)test_brightness); - //zassert_equal(x_coordinate, test_x_coordinate, NULL); - //zassert_equal(y_coordinate, test_y_coordinate, NULL); - //zassert_equal(brightness, test_brightness, NULL); - //zassert_equal(red, test_red, NULL); - //zassert_equal(green, test_green, NULL); - //zassert_equal(blue, test_blue, NULL); + color_rgb_from_xy(&test_red, &test_green, &test_blue, x_coordinate, + y_coordinate, brightness); + zassert_true(is_float_equal(x_coordinate, test_x_coordinate), + "(x=%.3f,test_x=%.3f)", x_coordinate, test_x_coordinate); + zassert_true(is_float_equal(y_coordinate, test_y_coordinate), + "(y=%.3f,test_y=%.3f)", y_coordinate, test_y_coordinate); + zassert_equal(brightness, test_brightness, "b=%u, test_b=%u", brightness, + test_brightness); } /** @@ -55,17 +93,40 @@ ZTEST(color_rgb_tests, test_color_rgb_xy) static void test_color_rgb_xy(void) #endif { - test_color_rgb_xy_unit(0, 0, 0, 0.0, 0.0, 0); - test_color_rgb_xy_unit(255, 255, 255, 0.323, 0.329, 255); - test_color_rgb_xy_unit(0, 0, 255, 0.136, 0.04, 12); - test_color_rgb_xy_unit(0, 255, 0, 0.172, 0.747, 170); - test_color_rgb_xy_unit(255, 0, 0, 0.701, 0.299, 72); - test_color_rgb_xy_unit(128, 0, 0, 0.701, 0.299, 16); + uint8_t red, green, blue; + + /* functions without gamma correction */ + color_rgb_from_ascii(&red, &green, &blue, "black"); + test_color_rgb_xy_unit(red, green, blue, 0.0, 0.0, 0); + color_rgb_from_ascii(&red, &green, &blue, "white"); + test_color_rgb_xy_unit(red, green, blue, 0.313, 0.329, 255); + color_rgb_from_ascii(&red, &green, &blue, "blue"); + test_color_rgb_xy_unit(red, green, blue, 0.157, 0.017, 5); + color_rgb_from_ascii(&red, &green, &blue, "green"); + test_color_rgb_xy_unit(red, green, blue, 0.115, 0.826, 95); + color_rgb_from_ascii(&red, &green, &blue, "red"); + test_color_rgb_xy_unit(red, green, blue, 0.735, 0.265, 59); + color_rgb_from_ascii(&red, &green, &blue, "maroon"); + test_color_rgb_xy_unit(red, green, blue, 0.735, 0.265, 29); + + /* functions with gamma correction */ + color_rgb_from_ascii(&red, &green, &blue, "black"); + test_color_rgb_xy_gamma_unit(red, green, blue, 0.0, 0.0, 0); + color_rgb_from_ascii(&red, &green, &blue, "white"); + test_color_rgb_xy_gamma_unit(red, green, blue, 0.313, 0.329, 255); + color_rgb_from_ascii(&red, &green, &blue, "blue"); + test_color_rgb_xy_gamma_unit(red, green, blue, 0.157, 0.017, 5); + color_rgb_from_ascii(&red, &green, &blue, "green"); + test_color_rgb_xy_gamma_unit(red, green, blue, 0.115, 0.826, 40); + color_rgb_from_ascii(&red, &green, &blue, "red"); + test_color_rgb_xy_gamma_unit(red, green, blue, 0.735, 0.265, 59); + color_rgb_from_ascii(&red, &green, &blue, "maroon"); + test_color_rgb_xy_gamma_unit(red, green, blue, 0.735, 0.265, 12); } /** -* Unit Test for sRGB to CIE xy -*/ + * Unit Test for sRGB to CIE xy + */ #if defined(CONFIG_ZTEST_NEW_API) ZTEST(color_rgb_tests, test_color_rgb_ascii) #else @@ -85,8 +146,8 @@ static void test_color_rgb_ascii(void) for (unsigned i = 0; i < count; i++) { name = color_rgb_from_index(i, &red, &green, &blue); zassert_not_null(name, NULL); - test_index = color_rgb_from_ascii(&test_red, &test_green, &test_blue, - name); + test_index = + color_rgb_from_ascii(&test_red, &test_green, &test_blue, name); zassert_equal(i, test_index, NULL); zassert_equal(red, test_red, NULL); zassert_equal(green, test_green, NULL); @@ -100,16 +161,13 @@ static void test_color_rgb_ascii(void) * @} */ - #if defined(CONFIG_ZTEST_NEW_API) ZTEST_SUITE(color_rgb_tests, NULL, NULL, NULL, NULL, NULL); #else void test_main(void) { - ztest_test_suite(color_rgb_tests, - ztest_unit_test(test_color_rgb_ascii), - ztest_unit_test(test_color_rgb_xy) - ); + ztest_test_suite(color_rgb_tests, ztest_unit_test(test_color_rgb_ascii), + ztest_unit_test(test_color_rgb_xy)); ztest_run_test_suite(color_rgb_tests); } diff --git a/test/bacnet/basic/sys/linear/CMakeLists.txt b/test/bacnet/basic/sys/linear/CMakeLists.txt new file mode 100644 index 00000000..45f3797a --- /dev/null +++ b/test/bacnet/basic/sys/linear/CMakeLists.txt @@ -0,0 +1,44 @@ +# SPDX-License-Identifier: MIT + +cmake_minimum_required(VERSION 3.10 FATAL_ERROR) + +get_filename_component(basename ${CMAKE_CURRENT_SOURCE_DIR} NAME) +project(test_${basename} + VERSION 1.0.0 + LANGUAGES C) + + +string(REGEX REPLACE + "/test/bacnet/[a-zA-Z_/-]*$" + "/src" + SRC_DIR + ${CMAKE_CURRENT_SOURCE_DIR}) +string(REGEX REPLACE + "/test/bacnet/[a-zA-Z_/-]*$" + "/test" + TST_DIR + ${CMAKE_CURRENT_SOURCE_DIR}) +set(ZTST_DIR "${TST_DIR}/ztest/src") + +add_compile_definitions( + BIG_ENDIAN=0 + CONFIG_ZTEST=1 + ) + +include_directories( + ${SRC_DIR} + ${TST_DIR}/ztest/include + ) + +add_executable(${PROJECT_NAME} + # File(s) under test + ${SRC_DIR}/bacnet/basic/sys/linear.c + # Support files and stubs (pathname alphabetical) + # Test and test library files + ./src/main.c + ${ZTST_DIR}/ztest_mock.c + ${ZTST_DIR}/ztest.c + ) + +target_link_libraries(${PROJECT_NAME} PRIVATE + m) diff --git a/test/bacnet/basic/sys/linear/src/main.c b/test/bacnet/basic/sys/linear/src/main.c new file mode 100644 index 00000000..3c2e574a --- /dev/null +++ b/test/bacnet/basic/sys/linear/src/main.c @@ -0,0 +1,136 @@ +/** + * @file + * @brief test linear interpolation APIs + * @date 2010 + * + * @section LICENSE + * Copyright (c) 2010 Steve Karg + * + * SPDX-License-Identifier: MIT + */ +#include +#include + +/** + * @addtogroup bacnet_tests + * @{ + */ + +/** +* Unit Test for linear interpolation of floating point values, rounded +*/ +void testLinearInterpolateRound(void) +{ + uint16_t x2 = 0; + uint16_t y1 = 0; + uint16_t y2 = 0; + uint16_t y3 = 0; + uint16_t x2_test = 0; + + y2 = linear_interpolate_round(1, 1, 65535, 1, 100); + zassert_equal(y2, 1, NULL); + y2 = linear_interpolate_round(1, 1, 65535, 100, 1); + zassert_equal(y2, 100, NULL); + + y2 = linear_interpolate_round(1, 65535, 65535, 1, 100); + zassert_equal(y2, 100, NULL); + y2 = linear_interpolate_round(1, 65535, 65535, 100, 1); + zassert_equal(y2, 1, NULL); + + y2 = linear_interpolate_round(1, (65535 / 2), 65535, 1, 100); + zassert_equal(y2, 50, NULL); + + y2 = linear_interpolate_round(1, (65535 / 4), 65535, 1, 100); + zassert_equal(y2, 26, NULL); + + y2 = linear_interpolate_round(1, ((65535 * 3) / 4), 65535, 1, 100); + zassert_equal(y2, 75, NULL); + + y2 = linear_interpolate_round(1, 1, 100, 1, 65535); + zassert_equal(y2, 1, NULL); + + y2 = linear_interpolate_round(1, 100, 100, 1, 65535); + zassert_equal(y2, 65535, NULL); + + y2 = linear_interpolate_round(1, 100 / 2, 100, 1, 65535); + zassert_equal(y2, 32437, NULL); + + /* scaling from percent to steps and back */ + for (x2 = 1; x2 <= 100; x2++) { + y2 = linear_interpolate_round(1, x2, 100, 1, 65535); + x2_test = linear_interpolate_round(1, y2, 65535, 1, 100); + zassert_equal(x2, x2_test, NULL); + } + + /* test for low-trim, high-trim and scaling from percent to steps */ + for (x2 = 1; x2 <= 100; x2++) { + y1 = linear_interpolate_round(1, 20, 100, 1, 65535); + y3 = linear_interpolate_round(1, 80, 100, 1, 65535); + y2 = linear_interpolate_round(1, x2, 100, y1, y3); + x2_test = linear_interpolate_round(y1, y2, y3, 1, 100); + zassert_equal(x2, x2_test, "x2=%hu x2_test=%hu\n", x2, x2_test); + } + + y2 = linear_interpolate_round(1, 1, 65535, 20, 80); + zassert_equal(y2, 20, NULL); + y2 = linear_interpolate_round(1, 1, 65535, 80, 20); + zassert_equal(y2, 80, NULL); + y2 = linear_interpolate_round(1, 65535, 65535, 20, 80); + zassert_equal(y2, 80, NULL); + y2 = linear_interpolate_round(1, 65535, 65535, 80, 20); + zassert_equal(y2, 20, NULL); +} + +/** +* Unit Test for linear interpolation of integers +*/ +void testLinearInterpolateInt(void) +{ + uint16_t y2 = 0; + + y2 = linear_interpolate_int(1, 1, 65535, 1, 100); + zassert_equal(y2, 1, NULL); + y2 = linear_interpolate_int(1, 1, 65535, 100, 1); + zassert_equal(y2, 100, NULL); + + y2 = linear_interpolate_int(1, 65535, 65535, 1, 100); + zassert_equal(y2, 100, NULL); + y2 = linear_interpolate_int(1, 65535, 65535, 100, 1); + zassert_equal(y2, 1, NULL); + + y2 = linear_interpolate_int(1, (65535 / 4), 65535, 1, 100); + zassert_equal(y2, 25, NULL); + + y2 = linear_interpolate_int(1, (65535 / 2), 65535, 1, 100); + zassert_equal(y2, 50, NULL); + + y2 = linear_interpolate_int(1, ((65535 * 3) / 4), 65535, 1, 100); + zassert_equal(y2, 75, NULL); + + y2 = linear_interpolate_int(1, 1, 100, 1, 65535); + zassert_equal(y2, 1, NULL); + + y2 = linear_interpolate_int(1, 100, 100, 1, 65535); + zassert_equal(y2, 65535, NULL); + + y2 = linear_interpolate_int(1, 100 / 2, 100, 1, 65535); + zassert_equal(y2, 32437, NULL); +} + +/** + * @} + */ + +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST_SUITE(Linear_Interpolate, NULL, NULL, NULL, NULL, NULL); +#else +void test_main(void) +{ + ztest_test_suite(Linear_Interpolate, + ztest_unit_test(testLinearInterpolateRound), + ztest_unit_test(testLinearInterpolateInt) + ); + + ztest_run_test_suite(Linear_Interpolate); +} +#endif diff --git a/test/bacnet/lighting/CMakeLists.txt b/test/bacnet/lighting/CMakeLists.txt index b2645e44..2c650ed4 100644 --- a/test/bacnet/lighting/CMakeLists.txt +++ b/test/bacnet/lighting/CMakeLists.txt @@ -39,6 +39,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/bacreal.c ${SRC_DIR}/bacnet/bacstr.c ${SRC_DIR}/bacnet/basic/sys/bigend.c + ${SRC_DIR}/bacnet/bactext.c + ${SRC_DIR}/bacnet/indtext.c # Test and test library files ./src/main.c ${ZTST_DIR}/ztest_mock.c diff --git a/test/bacnet/lighting/src/main.c b/test/bacnet/lighting/src/main.c index 0e72b240..34db9af5 100644 --- a/test/bacnet/lighting/src/main.c +++ b/test/bacnet/lighting/src/main.c @@ -10,6 +10,8 @@ #include #include +#include +#include #include /** @@ -36,10 +38,16 @@ static void testBACnetLightingCommand(BACNET_LIGHTING_COMMAND *data) status = lighting_command_same(&test_data, data); zassert_true(status, NULL); len = lighting_command_encode(apdu, data); - apdu_len = lighting_command_decode(apdu, sizeof(apdu), &test_data); - zassert_true(len > 0, NULL); - zassert_true(apdu_len > 0, NULL); + apdu_len = lighting_command_decode(apdu, len, &test_data); + zassert_true(len > 0, "lighting-command[%s] failed to encode!", + bactext_lighting_operation_name(data->operation)); + zassert_true(apdu_len > 0, "lighting-command[%s] failed to decode!", + bactext_lighting_operation_name(data->operation)); status = lighting_command_same(&test_data, data); + while (len) { + len--; + apdu_len = lighting_command_decode(apdu, len, NULL); + } } #if defined(CONFIG_ZTEST_NEW_API) @@ -48,27 +56,58 @@ ZTEST(lighting_tests, testBACnetLightingCommandAll) static void testBACnetLightingCommandAll(void) #endif { - BACNET_LIGHTING_COMMAND data; + /* + BACNET_LIGHTING_OPERATION operation; + bool use_target_level:1; + bool use_ramp_rate:1; + bool use_step_increment:1; + bool use_fade_time:1; + bool use_priority:1; + float target_level; + float ramp_rate; + float step_increment; + uint32_t fade_time; + uint8_t priority; + */ + BACNET_LIGHTING_COMMAND test_data[] = { + { BACNET_LIGHTS_NONE, false, false, false, false, false, 0.0, 100.0, + 1.0, 100, 1 }, + { BACNET_LIGHTS_FADE_TO, true, false, false, true, true, 100.0, 100.0, + 1.0, 100, 1 }, + { BACNET_LIGHTS_FADE_TO, true, false, false, false, false, 0.0, 100.0, + 1.0, 100, 1 }, + { BACNET_LIGHTS_RAMP_TO, true, true, false, false, true, 0.0, 100.0, + 1.0, 100, 1 }, + { BACNET_LIGHTS_RAMP_TO, true, false, false, false, false, 100.0, 100.0, + 1.0, 100, 1 }, + { BACNET_LIGHTS_STEP_UP, false, false, true, false, true, 100.0, 100.0, + 1.0, 100, 1 }, + { BACNET_LIGHTS_STEP_UP, false, false, true, false, false, 100.0, 100.0, + 2.0, 100, 1 }, + { BACNET_LIGHTS_STEP_DOWN, false, false, true, false, true, 100.0, + 100.0, 1.0, 100, 1 }, + { BACNET_LIGHTS_STEP_DOWN, false, false, true, false, false, 100.0, + 100.0, 2.0, 100, 1 }, + { BACNET_LIGHTS_STEP_ON, false, false, true, false, true, 100.0, 100.0, + 1.0, 100, 1 }, + { BACNET_LIGHTS_STEP_ON, false, false, true, false, false, 100.0, 100.0, + 2.0, 100, 1 }, + { BACNET_LIGHTS_STEP_OFF, false, false, true, false, true, 100.0, 100.0, + 1.0, 100, 1 }, + { BACNET_LIGHTS_STEP_OFF, false, false, true, false, false, 100.0, + 100.0, 2.0, 100, 1 }, + { BACNET_LIGHTS_STOP, false, false, false, false, true, 100.0, 100.0, + 1.0, 100, 1 }, + { BACNET_LIGHTS_STOP, false, false, false, false, false, 100.0, 100.0, + 2.0, 100, 1 }, + }; + unsigned i; - data.operation = BACNET_LIGHTS_NONE; - data.use_target_level = false; - data.use_ramp_rate = false; - data.use_step_increment = false; - data.use_fade_time = false; - data.use_priority = false; - data.target_level = 0.0; - data.ramp_rate = 100.0; - data.step_increment = 1.0; - data.fade_time = 100; - data.priority = 1; - testBACnetLightingCommand(&data); - data.operation = BACNET_LIGHTS_STOP; - data.use_target_level = true; - data.use_ramp_rate = true; - data.use_step_increment = true; - data.use_fade_time = true; - data.use_priority = true; - testBACnetLightingCommand(&data); + for (i = 0; i < ARRAY_SIZE(test_data); i++) { + printf("test-lighting-command[%s]\n", + bactext_lighting_operation_name(test_data[i].operation)); + testBACnetLightingCommand(&test_data[i]); + } } /** * @} @@ -94,11 +133,16 @@ static void testBACnetColorCommand(BACNET_COLOR_COMMAND *data) status = color_command_same(&test_data, data); zassert_true(status, NULL); len = color_command_encode(apdu, data); - apdu_len = color_command_decode(apdu, sizeof(apdu), &error_code, - &test_data); - zassert_true(len > 0, NULL); - zassert_true(apdu_len > 0, NULL); + apdu_len = color_command_decode(apdu, len, &error_code, &test_data); + zassert_true(len > 0, "color-command[%s] failed to encode!", + bactext_color_operation_name(data->operation)); + zassert_true(apdu_len > 0, "color-command[%s] failed to decode!", + bactext_color_operation_name(data->operation)); status = color_command_same(&test_data, data); + while (len) { + len--; + apdu_len = color_command_decode(apdu, len, NULL, NULL); + } } #if defined(CONFIG_ZTEST_NEW_API) @@ -107,16 +151,51 @@ ZTEST(lighting_tests, testBACnetColorCommandAll) static void testBACnetColorCommandAll(void) #endif { - BACNET_COLOR_COMMAND data = { 0 }; + BACNET_COLOR_COMMAND test_data[] = { + { .operation = BACNET_COLOR_OPERATION_NONE, + .target.color_temperature = 0, + .transit.fade_time = 0 }, + { .operation = BACNET_COLOR_OPERATION_STOP, + .target.color_temperature = 0, + .transit.fade_time = 0 }, + { .operation = BACNET_COLOR_OPERATION_FADE_TO_COLOR, + .target.color.x_coordinate = 0.0, + .target.color.y_coordinate = 0.0, + .transit.fade_time = 0 }, + { .operation = BACNET_COLOR_OPERATION_FADE_TO_COLOR, + .target.color.x_coordinate = 0.0, + .target.color.y_coordinate = 0.0, + .transit.fade_time = 2000 }, + { .operation = BACNET_COLOR_OPERATION_FADE_TO_CCT, + .target.color_temperature = 1800, + .transit.fade_time = 0 }, + { .operation = BACNET_COLOR_OPERATION_FADE_TO_CCT, + .target.color_temperature = 1800, + .transit.fade_time = 2000 }, + { .operation = BACNET_COLOR_OPERATION_RAMP_TO_CCT, + .target.color_temperature = 1800, + .transit.ramp_rate = 0 }, + { .operation = BACNET_COLOR_OPERATION_RAMP_TO_CCT, + .target.color_temperature = 1800, + .transit.ramp_rate = 20 }, + { .operation = BACNET_COLOR_OPERATION_STEP_UP_CCT, + .target.color_temperature = 1800, + .transit.step_increment = 0 }, + { .operation = BACNET_COLOR_OPERATION_STEP_UP_CCT, + .target.color_temperature = 1800, + .transit.step_increment = 1 }, + { .operation = BACNET_COLOR_OPERATION_STEP_DOWN_CCT, + .target.color_temperature = 5000, + .transit.step_increment = 0 }, + { .operation = BACNET_COLOR_OPERATION_STEP_DOWN_CCT, + .target.color_temperature = 5000, + .transit.step_increment = 1 }, + }; + unsigned i; - data.operation = BACNET_COLOR_OPERATION_NONE; - data.target.color_temperature = 0; - data.transit.fade_time = 0; - testBACnetColorCommand(&data); - data.operation = BACNET_COLOR_OPERATION_STOP; - data.target.color_temperature = 0; - data.transit.fade_time = 0; - testBACnetColorCommand(&data); + for (i = 0; i < ARRAY_SIZE(test_data); i++) { + testBACnetColorCommand(&test_data[i]); + } } #if defined(CONFIG_ZTEST_NEW_API) @@ -146,11 +225,14 @@ static void testBACnetXYColor(void) null_len = xy_color_context_encode(NULL, tag_number, &value); len = xy_color_context_encode(apdu, tag_number, &value); zassert_equal(null_len, len, NULL); - test_len = xy_color_context_decode(apdu, sizeof(apdu), tag_number, - &test_value); + test_len = xy_color_context_decode(apdu, len, tag_number, &test_value); zassert_equal(test_len, len, NULL); status = xy_color_same(&value, &test_value); zassert_true(status, NULL); + while (len) { + len--; + test_len = xy_color_context_decode(apdu, len, tag_number, NULL); + } } #if defined(CONFIG_ZTEST_NEW_API) @@ -159,10 +241,9 @@ ZTEST_SUITE(lighting_tests, NULL, NULL, NULL, NULL, NULL); void test_main(void) { ztest_test_suite(lighting_tests, - ztest_unit_test(testBACnetLightingCommandAll), - ztest_unit_test(testBACnetColorCommandAll), - ztest_unit_test(testBACnetXYColor) - ); + ztest_unit_test(testBACnetLightingCommandAll), + ztest_unit_test(testBACnetColorCommandAll), + ztest_unit_test(testBACnetXYColor)); ztest_run_test_suite(lighting_tests); } diff --git a/zephyr/CMakeLists.txt b/zephyr/CMakeLists.txt index aedded9b..f2e376e9 100644 --- a/zephyr/CMakeLists.txt +++ b/zephyr/CMakeLists.txt @@ -223,6 +223,8 @@ set(BACNETSTACK_SRCS ${BACNETSTACK_SRC}/bacnet/basic/sys/key.h ${BACNETSTACK_SRC}/bacnet/basic/sys/keylist.c ${BACNETSTACK_SRC}/bacnet/basic/sys/keylist.h + ${BACNETSTACK_SRC}/bacnet/basic/sys/linear.c + ${BACNETSTACK_SRC}/bacnet/basic/sys/linear.h ${BACNETSTACK_SRC}/bacnet/basic/sys/mstimer.c ${BACNETSTACK_SRC}/bacnet/basic/sys/mstimer.h ${BACNETSTACK_SRC}/bacnet/basic/sys/ringbuf.c diff --git a/zephyr/tests/bacnet/basic/object/color_object/CMakeLists.txt b/zephyr/tests/bacnet/basic/object/color_object/CMakeLists.txt index 50b92210..d33319d9 100644 --- a/zephyr/tests/bacnet/basic/object/color_object/CMakeLists.txt +++ b/zephyr/tests/bacnet/basic/object/color_object/CMakeLists.txt @@ -44,6 +44,7 @@ if(BOARD STREQUAL unit_testing) ${BACNET_SRC}/timestamp.c ${BACNET_SRC}/basic/sys/days.c ${BACNET_SRC}/basic/sys/keylist.c + ${BACNET_SRC}/basic/sys/linear.c ${BACNET_SRC}/bacdevobjpropref.c ${BACNET_SRC}/bactext.c ${BACNET_SRC}/indtext.c diff --git a/zephyr/tests/bacnet/basic/object/color_temperature/CMakeLists.txt b/zephyr/tests/bacnet/basic/object/color_temperature/CMakeLists.txt index 78d23cad..4e181ea2 100644 --- a/zephyr/tests/bacnet/basic/object/color_temperature/CMakeLists.txt +++ b/zephyr/tests/bacnet/basic/object/color_temperature/CMakeLists.txt @@ -44,6 +44,7 @@ if(BOARD STREQUAL unit_testing) ${BACNET_SRC}/timestamp.c ${BACNET_SRC}/basic/sys/days.c ${BACNET_SRC}/basic/sys/keylist.c + ${BACNET_SRC}/basic/sys/linear.c ${BACNET_SRC}/bacdevobjpropref.c ${BACNET_SRC}/bactext.c ${BACNET_SRC}/indtext.c diff --git a/zephyr/tests/bacnet/basic/object/lo/CMakeLists.txt b/zephyr/tests/bacnet/basic/object/lo/CMakeLists.txt index da4f3355..062d0517 100644 --- a/zephyr/tests/bacnet/basic/object/lo/CMakeLists.txt +++ b/zephyr/tests/bacnet/basic/object/lo/CMakeLists.txt @@ -26,6 +26,7 @@ if(BOARD STREQUAL unit_testing) list(APPEND SOURCES ${BACNET_SRC_PATH}.c ${BACNET_TEST_PATH}/src/main.c + ${BACNET_TEST_PATH}/../mock/device_mock.c ) get_filename_component(BACNET_OBJECT_SRC ${BACNET_SRC_PATH} PATH) @@ -51,6 +52,8 @@ if(BOARD STREQUAL unit_testing) ${BACNET_SRC}/dailyschedule.c ${BACNET_SRC}/weeklyschedule.c ${BACNET_SRC}/basic/sys/bigend.c + ${BACNET_SRC}/basic/sys/linear.c + ${BACNET_SRC}/basic/sys/keylist.c ${BACNET_SRC}/bactimevalue.c ) diff --git a/zephyr/tests/bacnet/lighting/CMakeLists.txt b/zephyr/tests/bacnet/lighting/CMakeLists.txt index 41c510f6..4f1b6256 100644 --- a/zephyr/tests/bacnet/lighting/CMakeLists.txt +++ b/zephyr/tests/bacnet/lighting/CMakeLists.txt @@ -34,6 +34,8 @@ if(BOARD STREQUAL unit_testing) ${BACNET_SRC}/bacstr.c ${BACNET_SRC}/bacint.c ${BACNET_SRC}/bacreal.c + ${BACNET_SRC}/bactext.c + ${BACNET_SRC}/indtext.c ${BACNET_SRC}/datetime.c ${BACNET_SRC}/timestamp.c ${BACNET_SRC}/basic/sys/days.c