Mercurial > louis > mq > lightsd
changeset 217:d320d75c9ca1
start re-ordering patches
author | Louis Opter <kalessin@kalessin.fr> |
---|---|
date | Mon, 03 Aug 2015 01:18:55 -0700 |
parents | 1314ac3aafd4 |
children | 1bb9e30ce9ae |
files | add_command_pipe.patch add_daemon_module.patch add_daemonization_with_nice_proctitle.patch add_tag_and_untag.patch daemon_module.patch fix_crash_in_get_light_state_when_tag_does_not_exist.patch fix_lifx_wire_float_endian_functions_naming.patch fix_set_power_state.patch fix_usage_and_version.patch ignore_duplicate_listening_address.patch ignore_duplicated_listening_addresses.patch leftovers.patch series tag_untag.patch update_readme.patch |
diffstat | 15 files changed, 5620 insertions(+), 6159 deletions(-) [+] |
line wrap: on
line diff
--- a/add_command_pipe.patch Sun Aug 02 19:02:55 2015 -0700 +++ b/add_command_pipe.patch Mon Aug 03 01:18:55 2015 -0700 @@ -1,20 +1,31 @@ # HG changeset patch -# Parent 710a684b58e97eddc7d7a12d85b17b823d087a82 -Add a command pipe to easily do actions from a shell - -NOTE: the command pipe is write only, responses aren't returned. +# Parent b346d618c2526ae5de9814cdd8db56fff77b224b diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt -@@ -43,6 +43,8 @@ +@@ -42,6 +42,8 @@ + "-D_POSIX_C_SOURCE=200809L" "-D_BSD_SOURCE=1" "-D_DEFAULT_SOURCE=1" - ++ + "-D_DARWIN_C_SOURCE=1" -+ - "-DLGTD_BIG_ENDIAN_SYSTEM=${BIG_ENDIAN_SYSTEM}" - "-DLGTD_SIZEOF_VOID_P=${CMAKE_SIZEOF_VOID_P}" + ) + + IF (CMAKE_BUILD_TYPE MATCHES "DEBUG") +diff --git a/README.rst b/README.rst +--- a/README.rst ++++ b/README.rst +@@ -33,8 +33,8 @@ + tests); + - toggle (power on if off and vice-versa, coming up). + +-The JSON-RPC interface works on top on IPv4/v6, over a command (named) pipe +-(coming up) and Unix sockets (coming up). ++The JSON-RPC interface works on top on IPv4/v6, over a command (named) pipe and ++Unix sockets (coming up). + + lightsd can target single or multiple bulbs at once: diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt --- a/core/CMakeLists.txt @@ -26,7 +37,7 @@ + pipe.c proto.c router.c - stats.c + ) diff --git a/core/client.c b/core/client.c --- a/core/client.c +++ b/core/client.c @@ -105,15 +116,6 @@ diff --git a/core/jsonrpc.c b/core/jsonrpc.c --- a/core/jsonrpc.c +++ b/core/jsonrpc.c -@@ -129,7 +129,7 @@ - return c == '-' || (c >= '0' && c <= '9'); - } - --static bool __attribute__((unused)) -+static bool - lgtd_jsonrpc_type_bool(const jsmntok_t *t, const char *json) - { - if (t->type != JSMN_PRIMITIVE) { @@ -480,7 +480,7 @@ lgtd_jsonrpc_write_id(struct lgtd_client *client) { @@ -192,7 +194,7 @@ diff --git a/core/lightsd.c b/core/lightsd.c --- a/core/lightsd.c +++ b/core/lightsd.c -@@ -52,6 +52,7 @@ +@@ -45,6 +45,7 @@ #include "jsmn.h" #include "jsonrpc.h" #include "client.h" @@ -200,7 +202,7 @@ #include "listen.h" #include "lightsd.h" -@@ -69,6 +70,7 @@ +@@ -60,6 +61,7 @@ lgtd_cleanup(void) { lgtd_listen_close_all(); @@ -208,15 +210,15 @@ lgtd_client_close_all(); lgtd_lifx_timer_close(); lgtd_lifx_broadcast_close(); -@@ -135,10 +137,17 @@ +@@ -126,8 +128,14 @@ lgtd_usage(const char *progname) { printf( - "Usage: %s -l addr:port [-l ...] [-f] [-t] [-h] [-V] " - "[-v debug|info|warning|error]\n", + "Usage: %s ...\n\n" -+ " [-l,--listen addr:port [-l ...]]\n" -+ " [-c,--comand-pipe /command/fifo -[c ...]]\n" ++ " [-l,--listen addr:port [+]]\n" ++ " [-c,--comand-pipe /command/fifo [+]]\n" + " [-f,--foreground]\n" + " [-t,--no-timestamps]\n" + " [-h,--help]\n" @@ -224,11 +226,8 @@ + " [-v,--verbosity debug|info|warning|error]\n", progname ); -+ lgtd_cleanup(); exit(0); - } - -@@ -198,6 +207,7 @@ +@@ -141,6 +149,7 @@ static const struct option long_opts[] = { {"listen", required_argument, NULL, 'l'}, @@ -236,7 +235,7 @@ {"foreground", no_argument, NULL, 'f'}, {"no-timestamps", no_argument, NULL, 't'}, {"help", no_argument, NULL, 'h'}, -@@ -205,7 +215,7 @@ +@@ -148,7 +157,7 @@ {"version", no_argument, NULL, 'V'}, {NULL, 0, NULL, 0} }; @@ -245,20 +244,11 @@ for (int rv = getopt_long(argc, argv, short_opts, long_opts, NULL); rv != -1; -@@ -218,8 +228,7 @@ - lgtd_usage(argv[0]); +@@ -164,6 +173,12 @@ + if (!lgtd_listen_open(optarg, sep + 1)) { + exit(1); } - strncat(binds, optarg, LGTD_MIN( -- sizeof(binds) - strlen(binds) - 1, -- strlen(optarg) -+ sizeof(binds) - strlen(binds) - 1, strlen(optarg) - )); - strncat(binds, ", ", LGTD_MIN( - sizeof(binds) - strlen(binds) - 1, 2 -@@ -230,6 +239,11 @@ - } - *sep = ':'; - break; ++ break; + case 'c': + if (!lgtd_command_pipe_open(optarg)) { + exit(1); @@ -271,7 +261,7 @@ new file mode 100644 --- /dev/null +++ b/core/pipe.c -@@ -0,0 +1,247 @@ +@@ -0,0 +1,248 @@ +// Copyright (c) 2015, Louis Opter <kalessin@kalessin.fr> +// +// This file is part of lighstd. @@ -311,7 +301,7 @@ +#include "pipe.h" +#include "lightsd.h" + -+static struct lgtd_command_pipe_list lgtd_command_pipes = ++struct lgtd_command_pipe_list lgtd_command_pipes = + SLIST_HEAD_INITIALIZER(&lgtd_command_pipes); + +static void @@ -448,6 +438,7 @@ + return false; + } + ++ lgtd_client_open_from_pipe(&pipe->client); + pipe->path = path; + pipe->fd = -1; + @@ -523,7 +514,7 @@ new file mode 100644 --- /dev/null +++ b/core/pipe.h -@@ -0,0 +1,31 @@ +@@ -0,0 +1,33 @@ +// Copyright (c) 2015, Louis Opter <kalessin@kalessin.fr> +// +// This file is part of lighstd. @@ -553,6 +544,8 @@ +}; +SLIST_HEAD(lgtd_command_pipe_list, lgtd_command_pipe); + ++extern struct lgtd_command_pipe_list lgtd_command_pipes; ++ +bool lgtd_command_pipe_open(const char *); +void lgtd_command_pipe_close_all(void); diff --git a/core/proto.c b/core/proto.c @@ -567,7 +560,7 @@ struct lgtd_router_device *device; SLIST_FOREACH(device, devices, link) { struct lgtd_lifx_bulb *bulb = device->device; -@@ -204,15 +204,15 @@ +@@ -204,7 +204,7 @@ ); continue; } @@ -576,19 +569,8 @@ bool comma = false; int tag_id; - LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, bulb->state.tags) { - if (LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id) & bulb->gw->tag_ids) { -- LGTD_CLIENT_WRITE_STRING(client, comma ? ",\"" : "\""); -- LGTD_CLIENT_WRITE_STRING(client, bulb->gw->tags[tag_id]->label); -- LGTD_CLIENT_WRITE_STRING(client, "\""); -+ lgtd_client_write_string(client, comma ? ",\"" : "\""); -+ lgtd_client_write_string(client, bulb->gw->tags[tag_id]->label); -+ lgtd_client_write_string(client, "\""); - comma = true; - } else { - lgtd_warnx( -@@ -225,11 +225,11 @@ - } +@@ -215,11 +215,11 @@ + comma = true; } - LGTD_CLIENT_WRITE_STRING( @@ -620,120 +602,6 @@ +{ + bufferevent_write(client->io, buf, bufsz); +} -diff --git a/tests/core/mock_event2.h b/tests/core/mock_event2.h -new file mode 100644 ---- /dev/null -+++ b/tests/core/mock_event2.h -@@ -0,0 +1,109 @@ -+#pragma once -+ -+#ifndef MOCKED_EVBUFFER_DRAIN -+int -+evbuffer_drain(struct evbuffer *buf, size_t len) -+{ -+ (void)buf; -+ (void)len; -+ return 0; -+} -+#endif -+ -+#ifndef MOCKED_EVBUFFER_NEW -+struct evbuffer * -+evbuffer_new(void) -+{ -+ return NULL; -+} -+#endif -+ -+#ifndef MOCKED_EVENT_FREE -+void -+evbuffer_free(struct evbuffer *buf) -+{ -+ (void)buf; -+} -+#endif -+ -+#ifndef MOCKED_EVBUFFER_GET_LENGTH -+size_t -+evbuffer_get_length(const struct evbuffer *buf) -+{ -+ (void)buf; -+ return 0; -+} -+#endif -+ -+#ifndef MOCKED_EVBUFFER_PULLUP -+unsigned char * -+evbuffer_pullup(struct evbuffer *buf, ev_ssize_t size) -+{ -+ (void)buf; -+ (void)size; -+ return NULL; -+} -+#endif -+ -+#ifndef MOCKED_EVBUFFER_READ -+int -+evbuffer_read(struct evbuffer *buffer, evutil_socket_t fd, int howmuch) -+{ -+ (void)buffer; -+ (void)fd; -+ return howmuch; -+} -+#endif -+ -+#ifndef MOCKED_EVENT_ADD -+int -+event_add(struct event *ev, const struct timeval *timeout) -+{ -+ (void)ev; -+ (void)timeout; -+ return 0; -+} -+#endif -+ -+#ifndef MOCKED_EVENT_DEL -+int -+event_del(struct event *ev) -+{ -+ (void)ev; -+ return 0; -+} -+#endif -+ -+#ifndef MOCKED_EVENT_FREE -+void -+event_free(struct event *ev) -+{ -+ (void)ev; -+} -+#endif -+ -+#ifndef MOCKED_EVENT_NEW -+struct event * -+event_new(struct event_base *base, -+ evutil_socket_t fd, -+ short events, -+ event_callback_fn cb, -+ void *ctx) -+{ -+ (void)base; -+ (void)fd; -+ (void)events; -+ (void)cb; -+ (void)ctx; -+ return NULL; -+} -+#endif -+ -+#ifndef MOCKED_EVUTIL_MAKE_SOCKET_NONBLOCKING -+int -+evutil_make_socket_nonblocking(evutil_socket_t fd) -+{ -+ (void)fd; -+ return 0; -+} -+#endif diff --git a/tests/core/pipe/CMakeLists.txt b/tests/core/pipe/CMakeLists.txt new file mode 100644 --- /dev/null @@ -1952,10 +1820,102 @@ + (void)parsed; +} +#endif +diff --git a/tests/core/proto/test_proto_get_light_state.c b/tests/core/proto/test_proto_get_light_state.c +--- a/tests/core/proto/test_proto_get_light_state.c ++++ b/tests/core/proto/test_proto_get_light_state.c +@@ -76,7 +76,7 @@ + int + main(void) + { +- struct lgtd_client client; ++ struct lgtd_client client = { .io = FAKE_BUFFEREVENT }; + struct lgtd_proto_target_list *targets = (void *)0x2a; + + lgtd_proto_get_light_state(&client, targets); +diff --git a/tests/core/proto/test_proto_get_light_state_empty_device_list.c b/tests/core/proto/test_proto_get_light_state_empty_device_list.c +--- a/tests/core/proto/test_proto_get_light_state_empty_device_list.c ++++ b/tests/core/proto/test_proto_get_light_state_empty_device_list.c +@@ -35,7 +35,7 @@ + int + main(void) + { +- struct lgtd_client client; ++ struct lgtd_client client = { .io = FAKE_BUFFEREVENT }; + struct lgtd_proto_target_list *targets = (void *)0x2a; + + lgtd_proto_get_light_state(&client, targets); +diff --git a/tests/core/proto/test_proto_get_light_state_null_device_list.c b/tests/core/proto/test_proto_get_light_state_null_device_list.c +--- a/tests/core/proto/test_proto_get_light_state_null_device_list.c ++++ b/tests/core/proto/test_proto_get_light_state_null_device_list.c +@@ -45,7 +45,7 @@ + int + main(void) + { +- struct lgtd_client client; ++ struct lgtd_client client = { .io = FAKE_BUFFEREVENT }; + struct lgtd_proto_target_list *targets = (void *)0x2a; + + lgtd_proto_get_light_state(&client, targets); +diff --git a/tests/core/proto/test_proto_power_off.c b/tests/core/proto/test_proto_power_off.c +--- a/tests/core/proto/test_proto_power_off.c ++++ b/tests/core/proto/test_proto_power_off.c +@@ -52,7 +52,7 @@ + struct lgtd_proto_target_list *targets; + targets = lgtd_tests_build_target_list("*", NULL); + +- struct lgtd_client client; ++ struct lgtd_client client = { .io = FAKE_BUFFEREVENT }; + + lgtd_proto_power_off(&client, targets); + +diff --git a/tests/core/proto/test_proto_power_off_routing_error.c b/tests/core/proto/test_proto_power_off_routing_error.c +--- a/tests/core/proto/test_proto_power_off_routing_error.c ++++ b/tests/core/proto/test_proto_power_off_routing_error.c +@@ -52,7 +52,7 @@ + struct lgtd_proto_target_list *targets; + targets = lgtd_tests_build_target_list("*", NULL); + +- struct lgtd_client client; ++ struct lgtd_client client = { .io = FAKE_BUFFEREVENT }; + + lgtd_proto_power_off(&client, targets); + +diff --git a/tests/core/proto/test_proto_power_on.c b/tests/core/proto/test_proto_power_on.c +--- a/tests/core/proto/test_proto_power_on.c ++++ b/tests/core/proto/test_proto_power_on.c +@@ -52,7 +52,7 @@ + struct lgtd_proto_target_list *targets; + targets = lgtd_tests_build_target_list("*", NULL); + +- struct lgtd_client client; ++ struct lgtd_client client = { .io = FAKE_BUFFEREVENT }; + + lgtd_proto_power_on(&client, targets); + +diff --git a/tests/core/proto/test_proto_power_on_routing_error.c b/tests/core/proto/test_proto_power_on_routing_error.c +--- a/tests/core/proto/test_proto_power_on_routing_error.c ++++ b/tests/core/proto/test_proto_power_on_routing_error.c +@@ -52,7 +52,7 @@ + struct lgtd_proto_target_list *targets; + targets = lgtd_tests_build_target_list("*", NULL); + +- struct lgtd_client client; ++ struct lgtd_client client = { .io = FAKE_BUFFEREVENT }; + + lgtd_proto_power_on(&client, targets); + diff --git a/tests/core/proto/tests_proto_utils.h b/tests/core/proto/tests_proto_utils.h --- a/tests/core/proto/tests_proto_utils.h +++ b/tests/core/proto/tests_proto_utils.h -@@ -18,7 +18,7 @@ +@@ -1,5 +1,7 @@ + #pragma once + ++#define FAKE_BUFFEREVENT (void *)0xfeed ++ + void + lgtd_client_start_send_response(struct lgtd_client *client) + { +@@ -16,7 +18,7 @@ void lgtd_client_send_response(struct lgtd_client *client, const char *msg) { @@ -1964,18 +1924,6 @@ } #endif -diff --git a/tests/core/tests_shims.c b/tests/core/tests_shims.c ---- a/tests/core/tests_shims.c -+++ b/tests/core/tests_shims.c -@@ -24,7 +24,7 @@ - .verbosity = LGTD_DEBUG - }; - --struct event_base *lgtd_ev_base = NULL; -+struct event_base *lgtd_ev_base = (void *)0x1234; - - const char *lgtd_binds = NULL; - diff --git a/tests/core/tests_utils.c b/tests/core/tests_utils.c --- a/tests/core/tests_utils.c +++ b/tests/core/tests_utils.c @@ -1999,7 +1947,7 @@ #include <event2/util.h> @@ -112,3 +117,42 @@ - + LIST_INSERT_HEAD(&tag->sites, site, link); return site; } +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/add_daemon_module.patch Mon Aug 03 01:18:55 2015 -0700 @@ -0,0 +1,1315 @@ +# HG changeset patch +# Parent 98585a1c815188fb358356d2dc68c2b06bbf86b6 + +diff --git a/CMakeLists.txt b/CMakeLists.txt +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -1,4 +1,4 @@ +-CMAKE_MINIMUM_REQUIRED(VERSION 2.8) ++CMAKE_MINIMUM_REQUIRED(VERSION 2.8.11) # first version TARGET_INCLUDE_DIRECTORIES + + PROJECT(LIGHTSD C) + +@@ -22,28 +22,34 @@ + # TODO: we need at least 2.0.19-stable because of the logging defines + FIND_PACKAGE(Event2 REQUIRED COMPONENTS core) + FIND_PACKAGE(Endian REQUIRED) ++ ++INCLUDE(CheckFunctionExists) + INCLUDE(TestBigEndian) ++INCLUDE(CompatSetProctitle) + INCLUDE(CompatTimeMonotonic) + +-TEST_BIG_ENDIAN(LGTD_BIG_ENDIAN_SYSTEM) ++TEST_BIG_ENDIAN(BIG_ENDIAN_SYSTEM) + + ### Global definitions ######################################################### + + INCLUDE(AddAllSubdirectories) + INCLUDE(AddTestFromSources) + +-SET(CMAKE_C_FLAGS "-pipe -Wextra -Wall -Wstrict-prototypes -std=c99") ++SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pipe -Wextra -Wall -Wstrict-prototypes -std=c99") + +-ADD_DEFINITIONS("-DLGTD_SIZEOF_VOID_P=${CMAKE_SIZEOF_VOID_P}") +-ADD_DEFINITIONS("-DLGTD_BIG_ENDIAN_SYSTEM=${LGTD_BIG_ENDIAN_SYSTEM}") +- +-# Only relevant for the GNU libc: + ADD_DEFINITIONS( ++ # Only relevant for the GNU libc: + "-D_POSIX_C_SOURCE=200809L" + "-D_BSD_SOURCE=1" + "-D_DEFAULT_SOURCE=1" + + "-D_DARWIN_C_SOURCE=1" ++ ++ "-DLGTD_BIG_ENDIAN_SYSTEM=${BIG_ENDIAN_SYSTEM}" ++ "-DLGTD_SIZEOF_VOID_P=${CMAKE_SIZEOF_VOID_P}" ++ ++ "-DLGTD_HAVE_LIBBSD=${HAVE_LIBBSD}" ++ "-DLGTD_HAVE_PROCTITLE=${HAVE_PROCTITLE}" + ) + + IF (CMAKE_BUILD_TYPE MATCHES "DEBUG") +@@ -54,10 +60,11 @@ + ENDIF () + + INCLUDE_DIRECTORIES( +- ${LIGHTSD_SOURCE_DIR}/compat/generic + ${LIGHTSD_BINARY_DIR}/compat ++ ${LIGHTSD_BINARY_DIR}/compat/generic + ) + ++ADD_SUBDIRECTORY(compat) + ADD_SUBDIRECTORY(core) + ADD_SUBDIRECTORY(lifx) + ADD_SUBDIRECTORY(tests) +diff --git a/CMakeScripts/AddTestFromSources.cmake b/CMakeScripts/AddTestFromSources.cmake +--- a/CMakeScripts/AddTestFromSources.cmake ++++ b/CMakeScripts/AddTestFromSources.cmake +@@ -1,11 +1,11 @@ +-FUNCTION(ADD_TEST_FROM_C_SOURCES TEST_SOURCE TEST_LIB) ++FUNCTION(ADD_TEST_FROM_C_SOURCES TEST_SOURCE) + STRING(LENGTH ${TEST_SOURCE} TEST_NAME_LEN) + STRING(LENGTH "test_" PREFIX_LEN) + MATH(EXPR TEST_NAME_LEN "${TEST_NAME_LEN} - 2 - ${PREFIX_LEN}") + STRING(SUBSTRING ${ARGV0} ${PREFIX_LEN} ${TEST_NAME_LEN} TEST_NAME) +- ADD_EXECUTABLE(${TEST_NAME} ${TEST_SOURCE} ${ARGN}) +- IF (TEST_LIB) +- TARGET_LINK_LIBRARIES(${TEST_NAME} ${TEST_LIB}) ++ ADD_EXECUTABLE(${TEST_NAME} ${TEST_SOURCE}) ++ IF (ARGN) ++ TARGET_LINK_LIBRARIES(${TEST_NAME} ${ARGN}) + ENDIF () + ADD_TEST(test_${TEST_NAME} ${TEST_NAME}) + ENDFUNCTION() +diff --git a/CMakeScripts/CompatSetProctitle.cmake b/CMakeScripts/CompatSetProctitle.cmake +new file mode 100644 +--- /dev/null ++++ b/CMakeScripts/CompatSetProctitle.cmake +@@ -0,0 +1,20 @@ ++IF (NOT HAVE_PROCTITLE) ++ SET(CMAKE_REQUIRED_QUIET TRUE) ++ SET(HAVE_PROCTITLE 0 CACHE INTERNAL "setproctitle found in libbsd") ++ SET(HAVE_LIBBSD 0 CACHE INTERNAL "libbsd found") ++ MESSAGE(STATUS "Looking for setproctitle") ++ CHECK_FUNCTION_EXISTS("setproctitle" HAVE_PROCTITLE) ++ IF (NOT HAVE_PROCTITLE) ++ MESSAGE(STATUS "Looking for setproctitle - not found, falling back on libbsd") ++ FIND_PACKAGE(LibBSD) ++ IF (NOT LibBSD_FOUND) ++ MESSAGE(STATUS "Couldn't find setproctitle, no fancy report in the process list") ++ ELSE () ++ SET(HAVE_PROCTITLE 1 CACHE INTERNAL "setproctitle found in libbsd") ++ SET(HAVE_LIBBSD 1 CACHE INTERNAL "libbsd found") ++ ENDIF () ++ ELSE () ++ SET(HAVE_PROCTITLE 1 CACHE INTERNAL "setproctitle found on the system") ++ ENDIF () ++ UNSET(CMAKE_REQUIRED_QUIET) ++ENDIF () +diff --git a/CMakeScripts/CompatTimeMonotonic.cmake b/CMakeScripts/CompatTimeMonotonic.cmake +--- a/CMakeScripts/CompatTimeMonotonic.cmake ++++ b/CMakeScripts/CompatTimeMonotonic.cmake +@@ -1,5 +1,3 @@ +-INCLUDE(CheckFunctionExists) +- + IF (NOT TIME_MONOTONIC_LIBRARY) + SET(COMPAT_TIME_MONOTONIC_IMPL "${LIGHTSD_SOURCE_DIR}/compat/${CMAKE_SYSTEM_NAME}/time_monotonic.c") + SET(COMPAT_TIME_MONOTONIC_H "${LIGHTSD_SOURCE_DIR}/compat/${CMAKE_SYSTEM_NAME}/time_monotonic.h") +diff --git a/CMakeScripts/FindLibBSD.cmake b/CMakeScripts/FindLibBSD.cmake +new file mode 100644 +--- /dev/null ++++ b/CMakeScripts/FindLibBSD.cmake +@@ -0,0 +1,10 @@ ++FIND_PATH(LIBBSD_INCLUDE_DIR bsd.h PATH_SUFFIXES bsd) ++ ++FIND_LIBRARY(LIBBSD_LIBRARY bsd) ++IF(LIBBSD_LIBRARY) ++ SET(LibBSD_FOUND TRUE) ++ENDIF() ++ ++INCLUDE(FindPackageHandleStandardArgs) ++ ++FIND_PACKAGE_HANDLE_STANDARD_ARGS(LibBSD DEFAULT_MSG LIBBSD_LIBRARY LIBBSD_INCLUDE_DIR) +diff --git a/README.rst b/README.rst +--- a/README.rst ++++ b/README.rst +@@ -65,6 +65,9 @@ + - CMake ≥ 2.8; + - libevent ≥ 2.0.19. + ++lightsd optionally depends on libbsd ≥ 0.5.0 on platforms missing ++``setproctitle`` (pretty much any non-BSD system, including Mac OS X). ++ + lightsd is actively developed and tested from Arch Linux, Debian and Mac OS X; + both for 32/64 bits and little/big endian architectures. + +@@ -86,4 +89,19 @@ + + .../lightsd/build$ core/lightsd -v info -l ::1:1234 + ++lightsd forks in the background by default, display running processes and check ++how we are doing: ++ ++:: ++ ++ ps aux | grep lightsd ++ ++You can stop lightsd with: ++ ++:: ++ ++ pkill lightsd ++ ++Use the ``-f`` option to run lightsd in the foreground. ++ + .. vim: set tw=80 spelllang=en spell: +diff --git a/compat/CMakeLists.txt b/compat/CMakeLists.txt +new file mode 100644 +--- /dev/null ++++ b/compat/CMakeLists.txt +@@ -0,0 +1,1 @@ ++ADD_SUBDIRECTORY(generic) +diff --git a/compat/generic/CMakeLists.txt b/compat/generic/CMakeLists.txt +new file mode 100644 +--- /dev/null ++++ b/compat/generic/CMakeLists.txt +@@ -0,0 +1,1 @@ ++FILE(COPY sys DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) +diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt +--- a/core/CMakeLists.txt ++++ b/core/CMakeLists.txt +@@ -15,6 +15,7 @@ + ADD_EXECUTABLE( + lightsd + client.c ++ daemon.c + jsmn.c + jsonrpc.c + listen.c +@@ -23,6 +24,7 @@ + pipe.c + proto.c + router.c ++ stats.c + ) + + TARGET_LINK_LIBRARIES( +@@ -31,3 +33,7 @@ + ${EVENT2_CORE_LIBRARY} + ${TIME_MONOTONIC_LIBRARY} + ) ++ ++IF (HAVE_LIBBSD) ++ TARGET_LINK_LIBRARIES(lightsd ${LIBBSD_LIBRARY}) ++ENDIF (HAVE_LIBBSD) +diff --git a/core/client.c b/core/client.c +--- a/core/client.c ++++ b/core/client.c +@@ -34,6 +34,8 @@ + #include "jsonrpc.h" + #include "client.h" + #include "proto.h" ++#include "stats.h" ++#include "daemon.h" + #include "lightsd.h" + + struct lgtd_client_list lgtd_clients = LIST_HEAD_INITIALIZER(&lgtd_clients); +@@ -44,6 +46,8 @@ + assert(client); + assert(client->io); + ++ LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(clients, -1); ++ + LIST_REMOVE(client, link); + bufferevent_free(client->io); + free(client); +@@ -217,6 +221,8 @@ + + LIST_INSERT_HEAD(&lgtd_clients, client, link); + ++ LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(clients, 1); ++ + return client; + } + +diff --git a/core/daemon.c b/core/daemon.c +new file mode 100644 +--- /dev/null ++++ b/core/daemon.c +@@ -0,0 +1,155 @@ ++// Copyright (c) 2015, Louis Opter <kalessin@kalessin.fr> ++// ++// This file is part of lighstd. ++// ++// lighstd is free software: you can redistribute it and/or modify ++// it under the terms of the GNU General Public License as published by ++// the Free Software Foundation, either version 3 of the License, or ++// (at your option) any later version. ++// ++// lighstd is distributed in the hope that it will be useful, ++// but WITHOUT ANY WARRANTY; without even the implied warranty of ++// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++// GNU General Public License for more details. ++// ++// You should have received a copy of the GNU General Public License ++// along with lighstd. If not, see <http://www.gnu.org/licenses/>. ++ ++#include <sys/queue.h> ++#include <sys/tree.h> ++#include <sys/types.h> ++#include <endian.h> ++#include <fcntl.h> ++#include <stdbool.h> ++#include <stdint.h> ++#include <stdio.h> ++#include <stdlib.h> ++#include <string.h> ++#include <unistd.h> ++ ++#if LGTD_HAVE_LIBBSD ++#include <bsd/bsd.h> ++#endif ++ ++#include <event2/util.h> ++ ++#include "time_monotonic.h" ++#include "lifx/wire_proto.h" ++#include "lifx/bulb.h" ++#include "lifx/gateway.h" ++#include "jsmn.h" ++#include "jsonrpc.h" ++#include "client.h" ++#include "listen.h" ++#include "daemon.h" ++#include "pipe.h" ++#include "stats.h" ++#include "lightsd.h" ++ ++bool ++lgtd_daemon_unleash(void) ++{ ++ if (chdir("/")) { ++ return false; ++ } ++ ++ int null = open("/dev/null", O_RDWR); ++ if (null == -1) { ++ return false; ++ } ++ ++ const int fds[] = { STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO }; ++ for (int i = 0; i != LGTD_ARRAY_SIZE(fds); ++i) { ++ if (dup2(null, fds[i]) == -1) { ++ close(null); ++ return false; ++ } ++ } ++ close(null); ++ ++#define SUMMON() do { \ ++ switch (fork()) { \ ++ case 0: \ ++ break; \ ++ case -1: \ ++ return false; \ ++ default: \ ++ exit(0); \ ++ } \ ++} while (0) ++ ++ SUMMON(); // \_o< ! ++ setsid(); ++ ++ SUMMON(); // \_o< !! ++ ++ return true; // welcome to UNIX! ++} ++ ++void ++lgtd_daemon_setup_proctitle(int argc, char *argv[], char *envp[]) ++{ ++#if LGTD_HAVE_LIBBSD ++ setproctitle_init(argc, argv, envp); ++ lgtd_daemon_update_proctitle(); ++#else ++ (void)argc; ++ (void)argv; ++ (void)envp; ++#endif ++} ++ ++void ++lgtd_daemon_update_proctitle(void) ++{ ++#if LGTD_HAVE_PROCTITLE ++ char title[LGTD_DAEMON_TITLE_SIZE] = { 0 }; ++ int i = 0; ++ ++#define TITLE_APPEND(fmt, ...) do { \ ++ int n = snprintf((&title[i]), (sizeof(title) - i), (fmt), __VA_ARGS__); \ ++ i = LGTD_MIN(i + n, (int)sizeof(title)); \ ++} while (0) ++ ++#define PREFIX(fmt, ...) TITLE_APPEND( \ ++ "%s" fmt, (i && title[i - 1] == ')' ? "; " : ""), __VA_ARGS__ \ ++) ++ ++#define ADD_ITEM(fmt, ...) TITLE_APPEND( \ ++ "%s" fmt, (i && title[i - 1] != '(' ? ", " : ""), __VA_ARGS__ \ ++) ++#define LOOP(list_type, list, elem_type, prefix, ...) do { \ ++ if (!list_type ## _EMPTY(list)) { \ ++ PREFIX("%s(", prefix); \ ++ elem_type *it; \ ++ list_type ## _FOREACH(it, list, link) { \ ++ ADD_ITEM(__VA_ARGS__); \ ++ } \ ++ TITLE_APPEND("%s", ")"); \ ++ } \ ++} while (0) ++ ++ LOOP( ++ SLIST, &lgtd_listeners, struct lgtd_listen, ++ "listening_on", "%s:[%s]", it->addr, it->port ++ ); ++ ++ LOOP( ++ SLIST, &lgtd_command_pipes, struct lgtd_command_pipe, ++ "command_pipes", "%s", it->path ++ ); ++ ++ if (!LIST_EMPTY(&lgtd_lifx_gateways)) { ++ PREFIX("lifx_gateways(found=%d)", LGTD_STATS_GET(gateways)); ++ } ++ ++ PREFIX( ++ "bulbs(found=%d, on=%d)", ++ LGTD_STATS_GET(bulbs), LGTD_STATS_GET(bulbs_powered_on) ++ ); ++ ++ PREFIX("clients(connected=%d)", LGTD_STATS_GET(clients)); ++ ++ setproctitle("%s", title); ++#endif ++} +diff --git a/core/daemon.h b/core/daemon.h +new file mode 100644 +--- /dev/null ++++ b/core/daemon.h +@@ -0,0 +1,24 @@ ++// Copyright (c) 2015, Louis Opter <kalessin@kalessin.fr> ++// ++// This file is part of lighstd. ++// ++// lighstd is free software: you can redistribute it and/or modify ++// it under the terms of the GNU General Public License as published by ++// the Free Software Foundation, either version 3 of the License, or ++// (at your option) any later version. ++// ++// lighstd is distributed in the hope that it will be useful, ++// but WITHOUT ANY WARRANTY; without even the implied warranty of ++// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++// GNU General Public License for more details. ++// ++// You should have received a copy of the GNU General Public License ++// along with lighstd. If not, see <http://www.gnu.org/licenses/>. ++ ++#pragma once ++ ++enum { LGTD_DAEMON_TITLE_SIZE = 2048 }; ++ ++bool lgtd_daemon_unleash(void); // \_o< ++void lgtd_daemon_setup_proctitle(int, char *[], char *[]); ++void lgtd_daemon_update_proctitle(void); +diff --git a/core/lightsd.c b/core/lightsd.c +--- a/core/lightsd.c ++++ b/core/lightsd.c +@@ -47,6 +47,7 @@ + #include "client.h" + #include "pipe.h" + #include "listen.h" ++#include "daemon.h" + #include "lightsd.h" + + struct lgtd_opts lgtd_opts = { +@@ -142,8 +143,10 @@ + } + + int +-main(int argc, char *argv[]) ++main(int argc, char *argv[], char *envp[]) + { ++ lgtd_daemon_setup_proctitle(argc, argv, envp); ++ + lgtd_configure_libevent(); + lgtd_configure_signal_handling(); + +@@ -217,6 +220,10 @@ + lgtd_err(1, "can't setup lightsd"); + } + ++ if (!lgtd_opts.foreground && !lgtd_daemon_unleash()) { ++ lgtd_err(1, "can't fork to the background"); ++ } ++ + lgtd_lifx_timer_start_discovery(); + + event_base_dispatch(lgtd_ev_base); +diff --git a/core/listen.c b/core/listen.c +--- a/core/listen.c ++++ b/core/listen.c +@@ -30,6 +30,7 @@ + #include "jsonrpc.h" + #include "client.h" + #include "listen.h" ++#include "daemon.h" + #include "lightsd.h" + + struct lgtd_listen_list lgtd_listeners = +@@ -69,6 +70,8 @@ + evconnlistener_free(listener->evlistener); + free(listener); + } ++ ++ lgtd_daemon_update_proctitle(); + } + + bool +@@ -130,6 +133,8 @@ + + evutil_freeaddrinfo(res); + ++ lgtd_daemon_update_proctitle(); ++ + return true; + + error: +diff --git a/core/listen.h b/core/listen.h +--- a/core/listen.h ++++ b/core/listen.h +@@ -17,6 +17,8 @@ + + #pragma once + ++struct evconnlistener; ++ + struct lgtd_listen { + SLIST_ENTRY(lgtd_listen) link; + const char *addr; +@@ -25,5 +27,7 @@ + }; + SLIST_HEAD(lgtd_listen_list, lgtd_listen); + ++extern struct lgtd_listen_list lgtd_listeners; ++ + bool lgtd_listen_open(const char *, const char *); + void lgtd_listen_close_all(void); +diff --git a/core/log.c b/core/log.c +--- a/core/log.c ++++ b/core/log.c +@@ -28,9 +28,14 @@ + #include <stdio.h> + #include <time.h> + ++#if LGTD_HAVE_LIBBSD ++#include <bsd/unistd.h> ++#endif ++ + #include <event2/event.h> + + #include "lifx/wire_proto.h" ++#include "stats.h" + #include "lightsd.h" + + static void +diff --git a/core/stats.c b/core/stats.c +new file mode 100644 +--- /dev/null ++++ b/core/stats.c +@@ -0,0 +1,47 @@ ++// Copyright (c) 2015, Louis Opter <kalessin@kalessin.fr> ++// ++// This file is part of lighstd. ++// ++// lighstd is free software: you can redistribute it and/or modify ++// it under the terms of the GNU General Public License as published by ++// the Free Software Foundation, either version 3 of the License, or ++// (at your option) any later version. ++// ++// lighstd is distributed in the hope that it will be useful, ++// but WITHOUT ANY WARRANTY; without even the implied warranty of ++// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++// GNU General Public License for more details. ++// ++// You should have received a copy of the GNU General Public License ++// along with lighstd. If not, see <http://www.gnu.org/licenses/>. ++ ++#include <assert.h> ++#include <stdint.h> ++ ++#include "stats.h" ++ ++struct lgtd_stats lgtd_counters = { .gateways = 0 }; ++ ++void ++lgtd_stats_add(int offset, int value) ++{ ++ assert(offset >= 0); ++ assert(offset < (int)sizeof(lgtd_counters)); ++ assert(offset % sizeof(int) == 0); ++ ++ int *counter = (int *)((uint8_t *)&lgtd_counters + offset); ++ ++ assert(*counter + value >= 0); ++ ++ *counter += value; ++} ++ ++int ++lgtd_stats_get(int offset) ++{ ++ assert(offset >= 0); ++ assert(offset < (int)sizeof(lgtd_counters)); ++ assert(offset % sizeof(int) == 0); ++ ++ return *(int *)((uint8_t *)&lgtd_counters + offset); ++} +diff --git a/core/stats.h b/core/stats.h +new file mode 100644 +--- /dev/null ++++ b/core/stats.h +@@ -0,0 +1,35 @@ ++// Copyright (c) 2015, Louis Opter <kalessin@kalessin.fr> ++// ++// This file is part of lighstd. ++// ++// lighstd is free software: you can redistribute it and/or modify ++// it under the terms of the GNU General Public License as published by ++// the Free Software Foundation, either version 3 of the License, or ++// (at your option) any later version. ++// ++// lighstd is distributed in the hope that it will be useful, ++// but WITHOUT ANY WARRANTY; without even the implied warranty of ++// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++// GNU General Public License for more details. ++// ++// You should have received a copy of the GNU General Public License ++// along with lighstd. If not, see <http://www.gnu.org/licenses/>. ++ ++#pragma once ++ ++struct lgtd_stats { ++ int gateways; ++ int bulbs; ++ int bulbs_powered_on; ++ int clients; ++}; ++ ++void lgtd_stats_add(int, int); ++int lgtd_stats_get(int); ++ ++#define LGTD_STATS_GET(name) lgtd_stats_get(offsetof(struct lgtd_stats, name)) ++ ++#define LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(name, value) do { \ ++ lgtd_stats_add(offsetof(struct lgtd_stats, name), (value)); \ ++ lgtd_daemon_update_proctitle(); \ ++} while (0) +diff --git a/lifx/bulb.c b/lifx/bulb.c +--- a/lifx/bulb.c ++++ b/lifx/bulb.c +@@ -32,6 +32,8 @@ + #include "core/time_monotonic.h" + #include "bulb.h" + #include "gateway.h" ++#include "core/daemon.h" ++#include "core/stats.h" + #include "core/lightsd.h" + + struct lgtd_lifx_bulb_map lgtd_lifx_bulbs_table = +@@ -62,6 +64,7 @@ + bulb->gw = gw; + memcpy(bulb->addr, addr, sizeof(bulb->addr)); + RB_INSERT(lgtd_lifx_bulb_map, &lgtd_lifx_bulbs_table, bulb); ++ LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(bulbs, 1); + + bulb->last_light_state_at = lgtd_time_monotonic_msecs(); + +@@ -74,6 +77,10 @@ + assert(bulb); + assert(bulb->gw); + ++ LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(bulbs, -1); ++ if (bulb->state.power == LGTD_LIFX_POWER_ON) { ++ LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(bulbs_powered_on, -1); ++ } + RB_REMOVE(lgtd_lifx_bulb_map, &lgtd_lifx_bulbs_table, bulb); + SLIST_REMOVE(&bulb->gw->bulbs, bulb, lgtd_lifx_bulb, link_by_gw); + lgtd_info( +@@ -94,6 +101,13 @@ + { + assert(bulb); + assert(state); ++ ++ if (state->power != bulb->state.power) { ++ LGTD_STATS_ADD_AND_UPDATE_PROCTITLE( ++ bulbs_powered_on, state->power == LGTD_LIFX_POWER_ON ? 1 : -1 ++ ); ++ } ++ + bulb->last_light_state_at = received_at; + memcpy(&bulb->state, state, sizeof(bulb->state)); + } +@@ -102,5 +116,12 @@ + lgtd_lifx_bulb_set_power_state(struct lgtd_lifx_bulb *bulb, uint16_t power) + { + assert(bulb); ++ ++ if (power != bulb->state.power) { ++ LGTD_STATS_ADD_AND_UPDATE_PROCTITLE( ++ bulbs_powered_on, power == LGTD_LIFX_POWER_ON ? 1 : -1 ++ ); ++ } ++ + bulb->state.power = power; + } +diff --git a/lifx/gateway.c b/lifx/gateway.c +--- a/lifx/gateway.c ++++ b/lifx/gateway.c +@@ -44,6 +44,8 @@ + #include "core/client.h" + #include "core/proto.h" + #include "core/router.h" ++#include "core/stats.h" ++#include "core/daemon.h" + #include "core/lightsd.h" + + struct lgtd_lifx_gateway_list lgtd_lifx_gateways = +@@ -54,6 +56,7 @@ + { + assert(gw); + ++ LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(gateways, -1); + event_del(gw->refresh_ev); + event_del(gw->write_ev); + if (gw->socket != -1) { +@@ -284,6 +287,8 @@ + // will stop by itself: + lgtd_lifx_timer_start_watchdog(); + ++ LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(gateways, 1); ++ + return gw; + + error_allocate: +diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt +--- a/tests/CMakeLists.txt ++++ b/tests/CMakeLists.txt +@@ -1,1 +1,14 @@ ++FUNCTION(ADD_CORE_LIBRARY LIBNAME) ++ ADD_LIBRARY(${LIBNAME} ${ARGN}) ++ TARGET_LINK_LIBRARIES(${LIBNAME} ${TIME_MONOTONIC_LIBRARY}) ++ TARGET_INCLUDE_DIRECTORIES( ++ ${LIBNAME} PUBLIC ++ ${LIGHTSD_SOURCE_DIR}/core/ ++ ${LIGHTSD_BINARY_DIR}/core/ ++ ) ++ IF (HAVE_LIBBSD) ++ TARGET_LINK_LIBRARIES(${LIBNAME} ${LIBBSD_LIBRARY}) ++ ENDIF (HAVE_LIBBSD) ++ENDFUNCTION() ++ + ADD_ALL_SUBDIRECTORIES() +diff --git a/tests/core/CMakeLists.txt b/tests/core/CMakeLists.txt +--- a/tests/core/CMakeLists.txt ++++ b/tests/core/CMakeLists.txt +@@ -2,9 +2,11 @@ + ${LIGHTSD_SOURCE_DIR} + ${LIGHTSD_SOURCE_DIR}/core/ + ${CMAKE_CURRENT_SOURCE_DIR} ++ ${CMAKE_CURRENT_SOURCE_DIR}/../lifx + ${LIGHTSD_BINARY_DIR} + ${LIGHTSD_BINARY_DIR}/core/ + ${CMAKE_CURRENT_BINARY_DIR} ++ ${CMAKE_CURRENT_BINARY_DIR}/../lifx + ) + + ADD_ALL_SUBDIRECTORIES() +diff --git a/tests/core/daemon/CMakeLists.txt b/tests/core/daemon/CMakeLists.txt +new file mode 100644 +--- /dev/null ++++ b/tests/core/daemon/CMakeLists.txt +@@ -0,0 +1,24 @@ ++INCLUDE_DIRECTORIES( ++ ${CMAKE_CURRENT_SOURCE_DIR} ++ ${CMAKE_CURRENT_BINARY_DIR} ++) ++ ++ADD_CORE_LIBRARY( ++ test_core_daemon STATIC ++ ${LIGHTSD_SOURCE_DIR}/core/log.c ++ ${LIGHTSD_SOURCE_DIR}/core/stats.c ++ ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c ++ ${LIGHTSD_SOURCE_DIR}/lifx/tagging.c ++ ${LIGHTSD_SOURCE_DIR}/lifx/wire_proto.c ++ ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c ++ ${CMAKE_CURRENT_SOURCE_DIR}/../tests_utils.c ++) ++ ++FUNCTION(ADD_DAEMON_TEST TEST_SOURCE) ++ ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_core_daemon) ++ENDFUNCTION() ++ ++FILE(GLOB TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test_*.c") ++FOREACH(TEST ${TESTS}) ++ ADD_DAEMON_TEST(${TEST}) ++ENDFOREACH() +diff --git a/tests/core/daemon/mock_pipe.h b/tests/core/daemon/mock_pipe.h +new file mode 100644 +--- /dev/null ++++ b/tests/core/daemon/mock_pipe.h +@@ -0,0 +1,4 @@ ++#pragma once ++ ++struct lgtd_command_pipe_list lgtd_command_pipes = ++ SLIST_HEAD_INITIALIZER(&lgtd_command_pipes); +diff --git a/tests/core/daemon/test_daemon_update_proctitle.c b/tests/core/daemon/test_daemon_update_proctitle.c +new file mode 100644 +--- /dev/null ++++ b/tests/core/daemon/test_daemon_update_proctitle.c +@@ -0,0 +1,109 @@ ++void mock_setproctitle(const char *fmt, ...) ++ __attribute__((format(printf, 1, 2))); ++ ++#undef LGTD_HAVE_LIBBSD ++#undef LGTD_HAVE_PROCTITLE ++#define LGTD_HAVE_PROCTITLE 1 ++#define setproctitle mock_setproctitle ++#include "daemon.c" ++ ++#include <err.h> ++ ++#include "mock_gateway.h" ++#include "mock_pipe.h" ++ ++#include "tests_utils.h" ++ ++const char *expected = ""; ++int setproctitle_call_count = 0; ++ ++void ++mock_setproctitle(const char *fmt, ...) ++{ ++ if (strcmp(fmt, "%s")) { ++ errx(1, "unexepected format %s (expected %%s)", fmt); ++ } ++ ++ va_list ap; ++ va_start(ap, fmt); ++ const char *title = va_arg(ap, const char *); ++ va_end(ap); ++ ++ if (strcmp(title, expected)) { ++ errx(1, "unexpected title: %s (expected %s)", title, expected); ++ } ++ ++ setproctitle_call_count++; ++} ++ ++int ++main(void) ++{ ++ expected = "bulbs(found=0, on=0); clients(connected=0)"; ++ lgtd_daemon_update_proctitle(); ++ if (setproctitle_call_count != 1) { ++ errx(1, "setproctitle should have been called"); ++ } ++ ++ expected = ( ++ "lifx_gateways(found=1); " ++ "bulbs(found=0, on=0); " ++ "clients(connected=0)" ++ ); ++ struct lgtd_lifx_gateway *gw_1 = lgtd_tests_insert_mock_gateway(1); ++ if (setproctitle_call_count != 2) { ++ errx(1, "setproctitle should have been called"); ++ } ++ ++ expected = ( ++ "lifx_gateways(found=1); " ++ "bulbs(found=1, on=0); " ++ "clients(connected=0)" ++ ); ++ lgtd_tests_insert_mock_bulb(gw_1, 2); ++ expected = ( ++ "lifx_gateways(found=1); " ++ "bulbs(found=2, on=0); " ++ "clients(connected=0)" ++ ); ++ lgtd_tests_insert_mock_bulb(gw_1, 3); ++ if (setproctitle_call_count != 4) { ++ errx(1, "setproctitle should have been called"); ++ } ++ ++ expected = ( ++ "listening_on(foobar.com:[1234]); " ++ "lifx_gateways(found=1); " ++ "bulbs(found=2, on=0); " ++ "clients(connected=0)" ++ ); ++ lgtd_tests_insert_mock_listener("foobar.com", "1234"); ++ lgtd_daemon_update_proctitle(); ++ if (setproctitle_call_count != 5) { ++ errx(1, "setproctitle should have been called"); ++ } ++ ++ expected = ( ++ "listening_on(foobar.com:[1234]); " ++ "lifx_gateways(found=1); " ++ "bulbs(found=2, on=1); " ++ "clients(connected=0)" ++ ); ++ LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(bulbs_powered_on, 1); ++ if (setproctitle_call_count != 6) { ++ errx(1, "setproctitle should have been called"); ++ } ++ ++ expected = ( ++ "listening_on(foobar.com:[1234]); " ++ "lifx_gateways(found=1); " ++ "bulbs(found=2, on=1); " ++ "clients(connected=1)" ++ ); ++ LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(clients, 1); ++ if (setproctitle_call_count != 7) { ++ errx(1, "setproctitle should have been called"); ++ } ++ ++ return 0; ++} +diff --git a/tests/core/jsonrpc/CMakeLists.txt b/tests/core/jsonrpc/CMakeLists.txt +--- a/tests/core/jsonrpc/CMakeLists.txt ++++ b/tests/core/jsonrpc/CMakeLists.txt +@@ -3,16 +3,16 @@ + ${CMAKE_CURRENT_BINARY_DIR} + ) + +-ADD_LIBRARY( ++ADD_CORE_LIBRARY( + test_core_jsonrpc STATIC + ${LIGHTSD_SOURCE_DIR}/core/jsmn.c + ${LIGHTSD_SOURCE_DIR}/core/log.c ++ ${LIGHTSD_SOURCE_DIR}/core/stats.c + ${LIGHTSD_SOURCE_DIR}/lifx/wire_proto.c + ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c + ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c + ${CMAKE_CURRENT_SOURCE_DIR}/../tests_utils.c + ) +-TARGET_LINK_LIBRARIES(test_core_jsonrpc ${TIME_MONOTONIC_LIBRARY}) + + FUNCTION(ADD_JSONRPC_TEST TEST_SOURCE) + ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_core_jsonrpc) +diff --git a/tests/core/mock_daemon.h b/tests/core/mock_daemon.h +new file mode 100644 +--- /dev/null ++++ b/tests/core/mock_daemon.h +@@ -0,0 +1,8 @@ ++#pragma once ++ ++#ifndef MOCKED_DAEMON_UPDATE_PROCTITLE ++void ++lgtd_daemon_update_proctitle(void) ++{ ++} ++#endif +diff --git a/tests/core/proto/CMakeLists.txt b/tests/core/proto/CMakeLists.txt +--- a/tests/core/proto/CMakeLists.txt ++++ b/tests/core/proto/CMakeLists.txt +@@ -3,17 +3,17 @@ + ${CMAKE_CURRENT_BINARY_DIR} + ) + +-ADD_LIBRARY( ++ADD_CORE_LIBRARY( + test_core_proto STATIC + ${LIGHTSD_SOURCE_DIR}/core/log.c + ${LIGHTSD_SOURCE_DIR}/core/jsonrpc.c ++ ${LIGHTSD_SOURCE_DIR}/core/stats.c + ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c + ${LIGHTSD_SOURCE_DIR}/lifx/timer.c + ${LIGHTSD_SOURCE_DIR}/lifx/wire_proto.c + ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c + ${CMAKE_CURRENT_SOURCE_DIR}/../tests_utils.c + ) +-TARGET_LINK_LIBRARIES(test_core_proto ${TIME_MONOTONIC_LIBRARY}) + + FUNCTION(ADD_ROUTER_TEST TEST_SOURCE) + ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_core_proto) +diff --git a/tests/core/proto/test_proto_get_light_state.c b/tests/core/proto/test_proto_get_light_state.c +--- a/tests/core/proto/test_proto_get_light_state.c ++++ b/tests/core/proto/test_proto_get_light_state.c +@@ -1,6 +1,8 @@ + #include "proto.c" + + #include "mock_client_buf.h" ++#include "mock_daemon.h" ++#include "mock_gateway.h" + #include "tests_utils.h" + + #define MOCKED_ROUTER_TARGETS_TO_DEVICES +diff --git a/tests/core/proto/test_proto_get_light_state_empty_device_list.c b/tests/core/proto/test_proto_get_light_state_empty_device_list.c +--- a/tests/core/proto/test_proto_get_light_state_empty_device_list.c ++++ b/tests/core/proto/test_proto_get_light_state_empty_device_list.c +@@ -1,6 +1,7 @@ + #include "proto.c" + + #include "mock_client_buf.h" ++#include "mock_daemon.h" + #include "tests_utils.h" + + #define MOCKED_ROUTER_TARGETS_TO_DEVICES +diff --git a/tests/core/proto/test_proto_get_light_state_null_device_list.c b/tests/core/proto/test_proto_get_light_state_null_device_list.c +--- a/tests/core/proto/test_proto_get_light_state_null_device_list.c ++++ b/tests/core/proto/test_proto_get_light_state_null_device_list.c +@@ -1,6 +1,7 @@ + #include "proto.c" + + #include "mock_client_buf.h" ++#include "mock_daemon.h" + #include "tests_utils.h" + + #define MOCKED_ROUTER_TARGETS_TO_DEVICES +diff --git a/tests/core/proto/test_proto_power_off.c b/tests/core/proto/test_proto_power_off.c +--- a/tests/core/proto/test_proto_power_off.c ++++ b/tests/core/proto/test_proto_power_off.c +@@ -1,6 +1,7 @@ + #include "proto.c" + + #include "mock_client_buf.h" ++#include "mock_daemon.h" + #include "tests_utils.h" + + #define MOCKED_CLIENT_SEND_RESPONSE +@@ -38,7 +39,7 @@ + lgtd_client_send_response(struct lgtd_client *client, const char *msg) + { + if (!client) { +- errx(1, "client shouldn't ne NULL"); ++ errx(1, "client shouldn't be NULL"); + } + + if (strcmp(msg, "true")) { +diff --git a/tests/core/proto/test_proto_power_off_routing_error.c b/tests/core/proto/test_proto_power_off_routing_error.c +--- a/tests/core/proto/test_proto_power_off_routing_error.c ++++ b/tests/core/proto/test_proto_power_off_routing_error.c +@@ -1,6 +1,7 @@ + #include "proto.c" + + #include "mock_client_buf.h" ++#include "mock_daemon.h" + #include "tests_utils.h" + + #define MOCKED_CLIENT_SEND_RESPONSE +diff --git a/tests/core/proto/test_proto_power_on.c b/tests/core/proto/test_proto_power_on.c +--- a/tests/core/proto/test_proto_power_on.c ++++ b/tests/core/proto/test_proto_power_on.c +@@ -1,6 +1,7 @@ + #include "proto.c" + + #include "mock_client_buf.h" ++#include "mock_daemon.h" + #include "tests_utils.h" + + #define MOCKED_CLIENT_SEND_RESPONSE +diff --git a/tests/core/proto/test_proto_power_on_routing_error.c b/tests/core/proto/test_proto_power_on_routing_error.c +--- a/tests/core/proto/test_proto_power_on_routing_error.c ++++ b/tests/core/proto/test_proto_power_on_routing_error.c +@@ -1,6 +1,7 @@ + #include "proto.c" + + #include "mock_client_buf.h" ++#include "mock_daemon.h" + #include "tests_utils.h" + + #define MOCKED_CLIENT_SEND_RESPONSE +diff --git a/tests/core/proto/test_proto_set_light_from_hsbk.c b/tests/core/proto/test_proto_set_light_from_hsbk.c +--- a/tests/core/proto/test_proto_set_light_from_hsbk.c ++++ b/tests/core/proto/test_proto_set_light_from_hsbk.c +@@ -3,6 +3,7 @@ + #include "proto.c" + + #include "mock_client_buf.h" ++#include "mock_daemon.h" + #include "tests_utils.h" + + #define MOCKED_CLIENT_SEND_RESPONSE +diff --git a/tests/core/proto/test_proto_set_light_from_hsbk_on_routing_error.c b/tests/core/proto/test_proto_set_light_from_hsbk_on_routing_error.c +--- a/tests/core/proto/test_proto_set_light_from_hsbk_on_routing_error.c ++++ b/tests/core/proto/test_proto_set_light_from_hsbk_on_routing_error.c +@@ -3,6 +3,7 @@ + #include "proto.c" + + #include "mock_client_buf.h" ++#include "mock_daemon.h" + #include "tests_utils.h" + + #define MOCKED_CLIENT_SEND_RESPONSE +diff --git a/tests/core/proto/test_proto_set_waveform.c b/tests/core/proto/test_proto_set_waveform.c +--- a/tests/core/proto/test_proto_set_waveform.c ++++ b/tests/core/proto/test_proto_set_waveform.c +@@ -3,6 +3,7 @@ + #include "proto.c" + + #include "mock_client_buf.h" ++#include "mock_daemon.h" + #include "tests_utils.h" + + #define MOCKED_CLIENT_SEND_RESPONSE +diff --git a/tests/core/proto/test_proto_set_waveform_on_routing_error.c b/tests/core/proto/test_proto_set_waveform_on_routing_error.c +--- a/tests/core/proto/test_proto_set_waveform_on_routing_error.c ++++ b/tests/core/proto/test_proto_set_waveform_on_routing_error.c +@@ -3,6 +3,7 @@ + #include "proto.c" + + #include "mock_client_buf.h" ++#include "mock_daemon.h" + #include "tests_utils.h" + + #define MOCKED_CLIENT_SEND_RESPONSE +diff --git a/tests/core/router/CMakeLists.txt b/tests/core/router/CMakeLists.txt +--- a/tests/core/router/CMakeLists.txt ++++ b/tests/core/router/CMakeLists.txt +@@ -3,10 +3,11 @@ + ${CMAKE_CURRENT_BINARY_DIR} + ) + +-ADD_LIBRARY( ++ADD_CORE_LIBRARY( + test_core_router STATIC + ${LIGHTSD_SOURCE_DIR}/core/log.c + ${LIGHTSD_SOURCE_DIR}/core/proto.c ++ ${LIGHTSD_SOURCE_DIR}/core/stats.c + ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c + ${LIGHTSD_SOURCE_DIR}/lifx/timer.c + ${LIGHTSD_SOURCE_DIR}/lifx/wire_proto.c +@@ -14,11 +15,7 @@ + ${CMAKE_CURRENT_SOURCE_DIR}/../tests_utils.c + ) + +-TARGET_LINK_LIBRARIES( +- test_core_router +- ${EVENT2_CORE_LIBRARY} +- ${TIME_MONOTONIC_LIBRARY} +-) ++TARGET_LINK_LIBRARIES(test_core_router ${EVENT2_CORE_LIBRARY}) + + FUNCTION(ADD_ROUTER_TEST TEST_SOURCE) + ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_core_router) +diff --git a/tests/core/router/test_router_send_to_broadcast.c b/tests/core/router/test_router_send_to_broadcast.c +--- a/tests/core/router/test_router_send_to_broadcast.c ++++ b/tests/core/router/test_router_send_to_broadcast.c +@@ -1,6 +1,8 @@ + #include "router.c" + ++#include "mock_daemon.h" + #include "tests_utils.h" ++ + #include "tests_router_utils.h" + + int +diff --git a/tests/core/router/test_router_send_to_device.c b/tests/core/router/test_router_send_to_device.c +--- a/tests/core/router/test_router_send_to_device.c ++++ b/tests/core/router/test_router_send_to_device.c +@@ -1,5 +1,6 @@ + #include "router.c" + ++#include "mock_daemon.h" + #include "tests_utils.h" + #include "tests_router_utils.h" + +diff --git a/tests/core/router/test_router_send_to_invalid_targets.c b/tests/core/router/test_router_send_to_invalid_targets.c +--- a/tests/core/router/test_router_send_to_invalid_targets.c ++++ b/tests/core/router/test_router_send_to_invalid_targets.c +@@ -1,5 +1,6 @@ + #include "router.c" + ++#include "mock_daemon.h" + #include "tests_utils.h" + #include "tests_router_utils.h" + +diff --git a/tests/core/router/test_router_send_to_label.c b/tests/core/router/test_router_send_to_label.c +--- a/tests/core/router/test_router_send_to_label.c ++++ b/tests/core/router/test_router_send_to_label.c +@@ -1,5 +1,6 @@ + #include "router.c" + ++#include "mock_daemon.h" + #include "tests_utils.h" + #include "tests_router_utils.h" + +diff --git a/tests/core/router/test_router_send_to_tag.c b/tests/core/router/test_router_send_to_tag.c +--- a/tests/core/router/test_router_send_to_tag.c ++++ b/tests/core/router/test_router_send_to_tag.c +@@ -1,5 +1,6 @@ + #include "router.c" + ++#include "mock_daemon.h" + #include "tests_utils.h" + #include "tests_router_utils.h" + +diff --git a/tests/core/router/test_router_targets_to_devices.c b/tests/core/router/test_router_targets_to_devices.c +--- a/tests/core/router/test_router_targets_to_devices.c ++++ b/tests/core/router/test_router_targets_to_devices.c +@@ -1,5 +1,6 @@ + #include "router.c" + ++#include "mock_daemon.h" + #include "tests_utils.h" + #include "tests_router_utils.h" + +diff --git a/tests/core/tests_utils.c b/tests/core/tests_utils.c +--- a/tests/core/tests_utils.c ++++ b/tests/core/tests_utils.c +@@ -24,6 +24,9 @@ + #include "core/jsonrpc.h" + #include "core/client.h" + #include "core/proto.h" ++#include "core/listen.h" ++#include "core/daemon.h" ++#include "core/stats.h" + #include "lifx/bulb.h" + #include "lifx/gateway.h" + #include "tests_utils.h" +@@ -31,8 +34,8 @@ + struct lgtd_lifx_gateway_list lgtd_lifx_gateways = + LIST_HEAD_INITIALIZER(&lgtd_lifx_gateways); + +-struct lgtd_lifx_tag_list lgtd_lifx_tags = +- LIST_HEAD_INITIALIZER(&lgtd_lifx_tags); ++struct lgtd_listen_list lgtd_listeners = ++ SLIST_HEAD_INITIALIZER(&lgtd_listeners); + + struct lgtd_lifx_gateway * + lgtd_tests_insert_mock_gateway(int id) +@@ -44,6 +47,8 @@ + + LIST_INSERT_HEAD(&lgtd_lifx_gateways, gw, link); + ++ LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(gateways, 1); ++ + return gw; + } + +@@ -118,6 +123,17 @@ + return site; + } + ++struct lgtd_listen * ++lgtd_tests_insert_mock_listener(const char *addr, const char *port) ++{ ++ struct lgtd_listen *listener = calloc(1, sizeof(*listener)); ++ listener->addr = addr; ++ listener->port = port; ++ SLIST_INSERT_HEAD(&lgtd_listeners, listener, link); ++ ++ return listener; ++} ++ + char * + lgtd_tests_make_temp_dir(void) + { +diff --git a/tests/core/tests_utils.h b/tests/core/tests_utils.h +--- a/tests/core/tests_utils.h ++++ b/tests/core/tests_utils.h +@@ -40,3 +40,4 @@ + struct lgtd_lifx_site *lgtd_tests_add_tag_to_gw(struct lgtd_lifx_tag *, + struct lgtd_lifx_gateway *, + int); ++struct lgtd_listen *lgtd_tests_insert_mock_listener(const char *addr, const char *port); +diff --git a/tests/lifx/gateway/CMakeLists.txt b/tests/lifx/gateway/CMakeLists.txt +--- a/tests/lifx/gateway/CMakeLists.txt ++++ b/tests/lifx/gateway/CMakeLists.txt +@@ -3,21 +3,27 @@ + ${CMAKE_CURRENT_BINARY_DIR} + ) + +-ADD_LIBRARY( +- test_lifx_gateway STATIC ++ADD_CORE_LIBRARY( ++ test_lifx_gateway_core STATIC + ${LIGHTSD_SOURCE_DIR}/core/log.c + ${LIGHTSD_SOURCE_DIR}/core/proto.c + ${LIGHTSD_SOURCE_DIR}/core/router.c ++ ${LIGHTSD_SOURCE_DIR}/core/stats.c ++ ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c ++) ++ ++ADD_LIBRARY( ++ test_lifx_gateway STATIC + ${LIGHTSD_SOURCE_DIR}/lifx/broadcast.c + ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c + ${LIGHTSD_SOURCE_DIR}/lifx/timer.c + ${LIGHTSD_SOURCE_DIR}/lifx/wire_proto.c +- ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c + ) +-TARGET_LINK_LIBRARIES(test_lifx_gateway ${TIME_MONOTONIC_LIBRARY}) + + FUNCTION(ADD_GATEWAY_TEST TEST_SOURCE) +- ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_lifx_gateway) ++ ADD_TEST_FROM_C_SOURCES( ++ ${TEST_SOURCE} test_lifx_gateway_core test_lifx_gateway ++ ) + ENDFUNCTION() + + FILE(GLOB TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test_*.c") +diff --git a/tests/lifx/tagging/CMakeLists.txt b/tests/lifx/tagging/CMakeLists.txt +--- a/tests/lifx/tagging/CMakeLists.txt ++++ b/tests/lifx/tagging/CMakeLists.txt +@@ -6,8 +6,12 @@ + ADD_LIBRARY( + test_lifx_tagging STATIC + ${LIGHTSD_SOURCE_DIR}/core/log.c ++ ${LIGHTSD_SOURCE_DIR}/core/stats.c + ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c + ) ++IF (HAVE_LIBBSD) ++ TARGET_LINK_LIBRARIES(test_lifx_tagging ${LIBBSD_LIBRARY}) ++ENDIF (HAVE_LIBBSD) + + FUNCTION(ADD_TAGGING_TEST TEST_SOURCE) + ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_lifx_tagging) +diff --git a/tests/lifx/tests_shims.c b/tests/lifx/tests_shims.c +--- a/tests/lifx/tests_shims.c ++++ b/tests/lifx/tests_shims.c +@@ -35,3 +35,8 @@ + return ntohs(in6_peer->sin6_port); + } + } ++ ++void ++lgtd_daemon_update_proctitle(void) ++{ ++} +diff --git a/tests/lifx/wire_proto/CMakeLists.txt b/tests/lifx/wire_proto/CMakeLists.txt +--- a/tests/lifx/wire_proto/CMakeLists.txt ++++ b/tests/lifx/wire_proto/CMakeLists.txt +@@ -3,14 +3,15 @@ + ${CMAKE_CURRENT_BINARY_DIR} + ) + +-ADD_LIBRARY( +- test_lifx_wire_proto STATIC ++ADD_CORE_LIBRARY( ++ test_lifx_wire_proto_core STATIC + ${LIGHTSD_SOURCE_DIR}/core/log.c ++ ${LIGHTSD_SOURCE_DIR}/core/stats.c + ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c + ) + + FUNCTION(ADD_WIRE_PROTO_TEST TEST_SOURCE) +- ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_lifx_wire_proto) ++ ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_lifx_wire_proto_core) + ENDFUNCTION() + + FILE(GLOB TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test_*.c")
--- a/add_daemonization_with_nice_proctitle.patch Sun Aug 02 19:02:55 2015 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,746 +0,0 @@ -# HG changeset patch -# Parent 33ec84c490e97e4958a011ba16b113561be2aa19 -Add daemonization with a nice process title - -NOTE: the -f argument is now actually required to start lightsd in the - foreground. - -diff --git a/CMakeLists.txt b/CMakeLists.txt ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -22,10 +22,13 @@ - # TODO: we need at least 2.0.19-stable because of the logging defines - FIND_PACKAGE(Event2 REQUIRED COMPONENTS core) - FIND_PACKAGE(Endian REQUIRED) -+ -+INCLUDE(CheckFunctionExists) - INCLUDE(TestBigEndian) -+INCLUDE(CompatSetProctitle) - INCLUDE(CompatTimeMonotonic) - --TEST_BIG_ENDIAN(LGTD_BIG_ENDIAN_SYSTEM) -+TEST_BIG_ENDIAN(BIG_ENDIAN_SYSTEM) - - ### Global definitions ######################################################### - -@@ -34,14 +37,17 @@ - - SET(CMAKE_C_FLAGS "-pipe -Wextra -Wall -Wstrict-prototypes -std=c99") - --ADD_DEFINITIONS("-DLGTD_SIZEOF_VOID_P=${CMAKE_SIZEOF_VOID_P}") --ADD_DEFINITIONS("-DLGTD_BIG_ENDIAN_SYSTEM=${LGTD_BIG_ENDIAN_SYSTEM}") -- --# Only relevant for the GNU libc: - ADD_DEFINITIONS( -+ # Only relevant for the GNU libc: - "-D_POSIX_C_SOURCE=200809L" - "-D_BSD_SOURCE=1" - "-D_DEFAULT_SOURCE=1" -+ -+ "-DLGTD_BIG_ENDIAN_SYSTEM=${BIG_ENDIAN_SYSTEM}" -+ "-DLGTD_SIZEOF_VOID_P=${CMAKE_SIZEOF_VOID_P}" -+ -+ "-DLGTD_HAVE_LIBBSD=${HAVE_LIBBSD}" -+ "-DLGTD_HAVE_PROCTITLE=${HAVE_PROCTITLE}" - ) - - IF (CMAKE_BUILD_TYPE MATCHES "DEBUG") -diff --git a/CMakeScripts/CompatSetProctitle.cmake b/CMakeScripts/CompatSetProctitle.cmake -new file mode 100644 ---- /dev/null -+++ b/CMakeScripts/CompatSetProctitle.cmake -@@ -0,0 +1,20 @@ -+IF (NOT HAVE_PROCTITLE) -+ SET(CMAKE_REQUIRED_QUIET TRUE) -+ SET(HAVE_PROCTITLE 0 CACHE INTERNAL "setproctitle found in libbsd") -+ SET(HAVE_LIBBSD 0 CACHE INTERNAL "libbsd found") -+ MESSAGE(STATUS "Looking for setproctitle") -+ CHECK_FUNCTION_EXISTS("setproctitle" HAVE_PROCTITLE) -+ IF (NOT HAVE_PROCTITLE) -+ MESSAGE(STATUS "Looking for setproctitle - not found, falling back on libbsd") -+ FIND_PACKAGE(LibBSD) -+ IF (NOT LibBSD_FOUND) -+ MESSAGE(STATUS "Couldn't find setproctitle, no fancy report in the process list") -+ ELSE () -+ SET(HAVE_PROCTITLE 1 CACHE INTERNAL "setproctitle found in libbsd") -+ SET(HAVE_LIBBSD 1 CACHE INTERNAL "libbsd found") -+ ENDIF () -+ ELSE () -+ SET(HAVE_PROCTITLE 1 CACHE INTERNAL "setproctitle found on the system") -+ ENDIF () -+ UNSET(CMAKE_REQUIRED_QUIET) -+ENDIF () -diff --git a/CMakeScripts/CompatTimeMonotonic.cmake b/CMakeScripts/CompatTimeMonotonic.cmake ---- a/CMakeScripts/CompatTimeMonotonic.cmake -+++ b/CMakeScripts/CompatTimeMonotonic.cmake -@@ -1,5 +1,3 @@ --INCLUDE(CheckFunctionExists) -- - IF (NOT TIME_MONOTONIC_LIBRARY) - SET(COMPAT_TIME_MONOTONIC_IMPL "${LIGHTSD_SOURCE_DIR}/compat/${CMAKE_SYSTEM_NAME}/time_monotonic.c") - SET(COMPAT_TIME_MONOTONIC_H "${LIGHTSD_SOURCE_DIR}/compat/${CMAKE_SYSTEM_NAME}/time_monotonic.h") -diff --git a/CMakeScripts/FindLibBSD.cmake b/CMakeScripts/FindLibBSD.cmake -new file mode 100644 ---- /dev/null -+++ b/CMakeScripts/FindLibBSD.cmake -@@ -0,0 +1,10 @@ -+FIND_PATH(LIBBSD_INCLUDE_DIR bsd.h PATH_SUFFIXES bsd) -+ -+FIND_LIBRARY(LIBBSD_LIBRARY bsd) -+IF(LIBBSD_LIBRARY) -+ SET(LibBSD_FOUND TRUE) -+ENDIF() -+ -+INCLUDE(FindPackageHandleStandardArgs) -+ -+FIND_PACKAGE_HANDLE_STANDARD_ARGS(LibBSD DEFAULT_MSG LIBBSD_LIBRARY LIBBSD_INCLUDE_DIR) -diff --git a/README.rst b/README.rst ---- a/README.rst -+++ b/README.rst -@@ -65,6 +65,9 @@ - - CMake ≥ 2.8; - - libevent ≥ 2.0.19. - -+lightsd optionally depends on libbsd ≥ 0.5.0 on platforms missing -+``setproctitle`` (pretty much any non-BSD system, including Mac OS X). -+ - lightsd is actively developed and tested from Arch Linux, Debian and Mac OS X; - both for 32/64 bits and little/big endian architectures. - -@@ -86,4 +89,19 @@ - - .../lightsd/build$ core/lightsd -v info -l ::1:1234 - -+lightsd forks in the background by default, display running processes and check -+how we are doing: -+ -+:: -+ -+ ps aux | grep lightsd -+ -+You can stop lightsd with: -+ -+:: -+ -+ pkill lightsd -+ -+Use the ``-f`` option to run lightsd in the foreground. -+ - .. vim: set tw=80 spelllang=en spell: -diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt ---- a/core/CMakeLists.txt -+++ b/core/CMakeLists.txt -@@ -22,6 +22,7 @@ - log.c - proto.c - router.c -+ stats.c - ) - - TARGET_LINK_LIBRARIES( -@@ -30,3 +31,7 @@ - ${EVENT2_CORE_LIBRARY} - ${TIME_MONOTONIC_LIBRARY} - ) -+ -+IF (HAVE_LIBBSD) -+ TARGET_LINK_LIBRARIES(lightsd ${LIBBSD_LIBRARY}) -+ENDIF (HAVE_LIBBSD) -diff --git a/core/lightsd.c b/core/lightsd.c ---- a/core/lightsd.c -+++ b/core/lightsd.c -@@ -17,11 +17,13 @@ - - #include <sys/queue.h> - #include <sys/tree.h> -+#include <sys/types.h> - #include <arpa/inet.h> - #include <assert.h> - #include <endian.h> - #include <err.h> - #include <errno.h> -+#include <fcntl.h> - #include <getopt.h> - #include <signal.h> - #include <stdarg.h> -@@ -31,6 +33,11 @@ - #include <stdlib.h> - #include <string.h> - #include <strings.h> -+#include <unistd.h> -+ -+#if LGTD_HAVE_LIBBSD -+#include <bsd/bsd.h> -+#endif - - #include <event2/event.h> - #include <event2/event_struct.h> -@@ -56,6 +63,8 @@ - - struct event_base *lgtd_ev_base = NULL; - -+const char *lgtd_binds; -+ - void - lgtd_cleanup(void) - { -@@ -133,9 +142,57 @@ - exit(0); - } - -+static bool -+lgtd_daemonize(void) -+{ -+ if (chdir("/")) { -+ return false; -+ } -+ -+ int null = open("/dev/null", O_RDWR); -+ if (null == -1) { -+ return false; -+ } -+ -+ const int fds[] = { STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO }; -+ for (int i = 0; i != LGTD_ARRAY_SIZE(fds); ++i) { -+ if (dup2(null, fds[i]) == -1) { -+ close(null); -+ return false; -+ } -+ } -+ close(null); -+ -+#define FORK() do { \ -+ switch (fork()) { \ -+ case 0: \ -+ break; \ -+ case -1: \ -+ return false; \ -+ default: \ -+ exit(0); \ -+ } \ -+} while (0) -+ -+ FORK(); -+ setsid(); -+ -+ FORK(); -+ -+ return true; -+} -+ - int --main(int argc, char *argv[]) -+main(int argc, char *argv[], char *envp[]) - { -+ char binds[512] = { 0 }; -+ lgtd_binds = binds; -+ -+#if LGTD_HAVE_LIBBSD -+ setproctitle_init(argc, argv, envp); -+#else -+ (void)envp; -+#endif - lgtd_configure_libevent(); - lgtd_configure_signal_handling(); - -@@ -160,10 +217,19 @@ - if (!sep || !sep[1]) { - lgtd_usage(argv[0]); - } -+ strncat(binds, optarg, LGTD_MIN( -+ sizeof(binds) - strlen(binds) - 1, -+ strlen(optarg) -+ )); -+ strncat(binds, ", ", LGTD_MIN( -+ sizeof(binds) - strlen(binds) - 1, 2 -+ )); - *sep = '\0'; - if (!lgtd_listen_open(optarg, sep + 1)) { - exit(1); - } -+ *sep = ':'; -+ break; - case 'f': - lgtd_opts.foreground = true; - break; -@@ -194,6 +260,12 @@ - } - } - -+ binds[LGTD_MAX(strlen(binds) - 1, 0)] = '\0'; -+ binds[LGTD_MAX(strlen(binds) - 2, 0)] = '\0'; -+ if (binds[sizeof(binds) - 2]) { -+ memset(binds + sizeof(binds) - 4, '.', 3); -+ } -+ - argc -= optind; - argv += optind; - -@@ -202,6 +274,10 @@ - lgtd_err(1, "can't setup lightsd"); - } - -+ if (!lgtd_opts.foreground && !lgtd_daemonize()) { -+ lgtd_err(1, "can't fork to the background"); -+ } -+ - lgtd_lifx_timer_start_discovery(); - - event_base_dispatch(lgtd_ev_base); -diff --git a/core/lightsd.h b/core/lightsd.h ---- a/core/lightsd.h -+++ b/core/lightsd.h -@@ -17,12 +17,15 @@ - - #pragma once - -+struct sockaddr_storage; -+ - #ifndef __attribute__ - # define __atttribute__(e) - #endif - - #define LGTD_ABS(v) ((v) >= 0 ? (v) : (v) * -1) - #define LGTD_MIN(a, b) ((a) < (b) ? (a) : (b)) -+#define LGTD_MAX(a, b) ((a) > (b) ? (a) : (b)) - #define LGTD_ARRAY_SIZE(a) (sizeof(a) / sizeof(a[0])) - #define LGTD_MSECS_TO_TIMEVAL(v) { \ - .tv_sec = (v) / 1000, \ -@@ -44,6 +47,7 @@ - enum lgtd_verbosity verbosity; - }; - -+extern const char *lgtd_binds; - extern struct lgtd_opts lgtd_opts; - extern struct event_base *lgtd_ev_base; - -@@ -62,5 +66,6 @@ - void lgtd_info(const char *, ...) __attribute__((format(printf, 1, 2))); - void lgtd_debug(const char *, ...) __attribute__((format(printf, 1, 2))); - void lgtd_libevent_log(int, const char *); -+void lgtd_update_proctitle(void); - - void lgtd_cleanup(void); -diff --git a/core/log.c b/core/log.c ---- a/core/log.c -+++ b/core/log.c -@@ -17,6 +17,7 @@ - - #include <sys/tree.h> - #include <sys/time.h> -+#include <sys/types.h> - #include <arpa/inet.h> - #include <assert.h> - #include <endian.h> -@@ -26,11 +27,17 @@ - #include <stdbool.h> - #include <stdint.h> - #include <stdio.h> -+#include <stdlib.h> - #include <time.h> - -+#if LGTD_HAVE_LIBBSD -+#include <bsd/unistd.h> -+#endif -+ - #include <event2/event.h> - - #include "lifx/wire_proto.h" -+#include "stats.h" - #include "lightsd.h" - - static void -@@ -170,6 +177,20 @@ - case EVENT_LOG_MSG: lgtd_info("%s", msg); break; - case EVENT_LOG_WARN: lgtd_warnx("%s", msg) break; - case EVENT_LOG_ERR: lgtd_warnx("%s", msg); break; -- default: break; -+ default: break; - } - } -+ -+void -+lgtd_update_proctitle(void) -+{ -+#if LGTD_HAVE_PROCTITLE -+ setproctitle( -+ "listening_on(%s); lifx_gateways(found=%d); bulbs(found=%d, on=%d)", -+ lgtd_binds, -+ LGTD_STATS_GET(gateways), -+ LGTD_STATS_GET(bulbs), -+ LGTD_STATS_GET(bulbs_powered_on) -+ ); -+#endif -+} -diff --git a/core/stats.c b/core/stats.c -new file mode 100644 ---- /dev/null -+++ b/core/stats.c -@@ -0,0 +1,47 @@ -+// Copyright (c) 2015, Louis Opter <kalessin@kalessin.fr> -+// -+// This file is part of lighstd. -+// -+// lighstd is free software: you can redistribute it and/or modify -+// it under the terms of the GNU General Public License as published by -+// the Free Software Foundation, either version 3 of the License, or -+// (at your option) any later version. -+// -+// lighstd is distributed in the hope that it will be useful, -+// but WITHOUT ANY WARRANTY; without even the implied warranty of -+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -+// GNU General Public License for more details. -+// -+// You should have received a copy of the GNU General Public License -+// along with lighstd. If not, see <http://www.gnu.org/licenses/>. -+ -+#include <assert.h> -+#include <stdint.h> -+ -+#include "stats.h" -+ -+struct lgtd_stats lgtd_counters = { .gateways = 0 }; -+ -+void -+lgtd_stats_add(int offset, int value) -+{ -+ assert(offset >= 0); -+ assert(offset < (int)sizeof(lgtd_counters)); -+ assert(offset % sizeof(int) == 0); -+ -+ int *counter = (int *)((uint8_t *)&lgtd_counters + offset); -+ -+ assert(*counter + value >= 0); -+ -+ *counter += value; -+} -+ -+int -+lgtd_stats_get(int offset) -+{ -+ assert(offset >= 0); -+ assert(offset < (int)sizeof(lgtd_counters)); -+ assert(offset % sizeof(int) == 0); -+ -+ return *(int *)((uint8_t *)&lgtd_counters + offset); -+} -diff --git a/core/stats.h b/core/stats.h -new file mode 100644 ---- /dev/null -+++ b/core/stats.h -@@ -0,0 +1,33 @@ -+// Copyright (c) 2015, Louis Opter <kalessin@kalessin.fr> -+// -+// This file is part of lighstd. -+// -+// lighstd is free software: you can redistribute it and/or modify -+// it under the terms of the GNU General Public License as published by -+// the Free Software Foundation, either version 3 of the License, or -+// (at your option) any later version. -+// -+// lighstd is distributed in the hope that it will be useful, -+// but WITHOUT ANY WARRANTY; without even the implied warranty of -+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -+// GNU General Public License for more details. -+// -+// You should have received a copy of the GNU General Public License -+// along with lighstd. If not, see <http://www.gnu.org/licenses/>. -+ -+#pragma once -+ -+struct lgtd_stats { -+ int gateways; -+ int bulbs; -+ int bulbs_powered_on; -+}; -+ -+void lgtd_stats_add(int, int); -+int lgtd_stats_get(int); -+ -+#define LGTD_STATS_GET(name) lgtd_stats_get(offsetof(struct lgtd_stats, name)) -+#define LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(name, value) do { \ -+ lgtd_stats_add(offsetof(struct lgtd_stats, name), (value)); \ -+ lgtd_update_proctitle(); \ -+} while (0) -diff --git a/lifx/bulb.c b/lifx/bulb.c ---- a/lifx/bulb.c -+++ b/lifx/bulb.c -@@ -32,6 +32,7 @@ - #include "core/time_monotonic.h" - #include "bulb.h" - #include "gateway.h" -+#include "core/stats.h" - #include "core/lightsd.h" - - struct lgtd_lifx_bulb_map lgtd_lifx_bulbs_table = -@@ -62,6 +63,7 @@ - bulb->gw = gw; - memcpy(bulb->addr, addr, sizeof(bulb->addr)); - RB_INSERT(lgtd_lifx_bulb_map, &lgtd_lifx_bulbs_table, bulb); -+ LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(bulbs, 1); - - bulb->last_light_state_at = lgtd_time_monotonic_msecs(); - -@@ -74,6 +76,10 @@ - assert(bulb); - assert(bulb->gw); - -+ LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(bulbs, -1); -+ if (bulb->state.power == LGTD_LIFX_POWER_ON) { -+ LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(bulbs_powered_on, -1); -+ } - RB_REMOVE(lgtd_lifx_bulb_map, &lgtd_lifx_bulbs_table, bulb); - SLIST_REMOVE(&bulb->gw->bulbs, bulb, lgtd_lifx_bulb, link_by_gw); - lgtd_info( -@@ -94,6 +100,13 @@ - { - assert(bulb); - assert(state); -+ -+ if (state->power != bulb->state.power) { -+ LGTD_STATS_ADD_AND_UPDATE_PROCTITLE( -+ bulbs_powered_on, state->power == LGTD_LIFX_POWER_ON ? 1 : -1 -+ ); -+ } -+ - bulb->last_light_state_at = received_at; - memcpy(&bulb->state, state, sizeof(bulb->state)); - } -@@ -102,5 +115,12 @@ - lgtd_lifx_bulb_set_power_state(struct lgtd_lifx_bulb *bulb, uint16_t power) - { - assert(bulb); -+ -+ if (power != bulb->state.power) { -+ LGTD_STATS_ADD_AND_UPDATE_PROCTITLE( -+ bulbs_powered_on, power == LGTD_LIFX_POWER_ON ? 1 : -1 -+ ); -+ } -+ - bulb->state.power = power; - } -diff --git a/lifx/gateway.c b/lifx/gateway.c ---- a/lifx/gateway.c -+++ b/lifx/gateway.c -@@ -44,6 +44,7 @@ - #include "core/client.h" - #include "core/proto.h" - #include "core/router.h" -+#include "core/stats.h" - #include "core/lightsd.h" - - struct lgtd_lifx_gateway_list lgtd_lifx_gateways = -@@ -54,6 +55,7 @@ - { - assert(gw); - -+ LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(gateways, -1); - event_del(gw->refresh_ev); - event_del(gw->write_ev); - if (gw->socket != -1) { -@@ -284,6 +286,8 @@ - // will stop by itself: - lgtd_lifx_timer_start_watchdog(); - -+ LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(gateways, 1); -+ - return gw; - - error_allocate: -diff --git a/lifx/wire_proto.c b/lifx/wire_proto.c ---- a/lifx/wire_proto.c -+++ b/lifx/wire_proto.c -@@ -317,16 +317,22 @@ - pkt->brightness = le16toh(pkt->brightness); - pkt->kelvin = le16toh(pkt->kelvin); - pkt->dim = le16toh(pkt->dim); -- pkt->power = le16toh(pkt->power); -+ // The bulbs actually return power values between 0 and 0xffff, not sure -+ // what the intermediate values mean, let's pull them down to 0: -+ if (pkt->power != LGTD_LIFX_POWER_ON) { -+ pkt->power = LGTD_LIFX_POWER_OFF; -+ } - pkt->tags = le64toh(pkt->tags); - } - - void - lgtd_lifx_wire_decode_power_state(struct lgtd_lifx_packet_power_state *pkt) - { -- (void)pkt; -+ assert(pkt); - -- assert(pkt); -+ if (pkt->power != LGTD_LIFX_POWER_ON) { -+ pkt->power = LGTD_LIFX_POWER_OFF; -+ } - } - - void -diff --git a/tests/core/jsonrpc/CMakeLists.txt b/tests/core/jsonrpc/CMakeLists.txt ---- a/tests/core/jsonrpc/CMakeLists.txt -+++ b/tests/core/jsonrpc/CMakeLists.txt -@@ -7,12 +7,16 @@ - test_core_jsonrpc STATIC - ${LIGHTSD_SOURCE_DIR}/core/jsmn.c - ${LIGHTSD_SOURCE_DIR}/core/log.c -+ ${LIGHTSD_SOURCE_DIR}/core/stats.c - ${LIGHTSD_SOURCE_DIR}/lifx/wire_proto.c - ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c - ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c - ${CMAKE_CURRENT_SOURCE_DIR}/../tests_utils.c - ) - TARGET_LINK_LIBRARIES(test_core_jsonrpc ${TIME_MONOTONIC_LIBRARY}) -+IF (HAVE_LIBBSD) -+ TARGET_LINK_LIBRARIES(test_core_jsonrpc ${LIBBSD_LIBRARY}) -+ENDIF (HAVE_LIBBSD) - - FUNCTION(ADD_JSONRPC_TEST TEST_SOURCE) - ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_core_jsonrpc) -diff --git a/tests/core/proto/CMakeLists.txt b/tests/core/proto/CMakeLists.txt ---- a/tests/core/proto/CMakeLists.txt -+++ b/tests/core/proto/CMakeLists.txt -@@ -7,6 +7,7 @@ - test_core_proto STATIC - ${LIGHTSD_SOURCE_DIR}/core/log.c - ${LIGHTSD_SOURCE_DIR}/core/jsonrpc.c -+ ${LIGHTSD_SOURCE_DIR}/core/stats.c - ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c - ${LIGHTSD_SOURCE_DIR}/lifx/timer.c - ${LIGHTSD_SOURCE_DIR}/lifx/wire_proto.c -@@ -14,6 +15,9 @@ - ${CMAKE_CURRENT_SOURCE_DIR}/../tests_utils.c - ) - TARGET_LINK_LIBRARIES(test_core_proto ${TIME_MONOTONIC_LIBRARY}) -+IF (HAVE_LIBBSD) -+ TARGET_LINK_LIBRARIES(test_core_proto ${LIBBSD_LIBRARY}) -+ENDIF (HAVE_LIBBSD) - - FUNCTION(ADD_ROUTER_TEST TEST_SOURCE) - ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_core_proto) -diff --git a/tests/core/proto/test_proto_get_light_state.c b/tests/core/proto/test_proto_get_light_state.c ---- a/tests/core/proto/test_proto_get_light_state.c -+++ b/tests/core/proto/test_proto_get_light_state.c -@@ -103,7 +103,7 @@ - 1, - "%d bytes written, expected %lu " - "(got %.*s instead of %s)", -- client_write_buf_idx, sizeof(expected) - 1, -+ client_write_buf_idx, sizeof(expected) - 1UL, - client_write_buf_idx, client_write_buf, expected - ); - } -diff --git a/tests/core/proto/test_proto_get_light_state_empty_device_list.c b/tests/core/proto/test_proto_get_light_state_empty_device_list.c ---- a/tests/core/proto/test_proto_get_light_state_empty_device_list.c -+++ b/tests/core/proto/test_proto_get_light_state_empty_device_list.c -@@ -45,7 +45,7 @@ - if (client_write_buf_idx != sizeof(expected) - 1) { - lgtd_errx( - 1, "%d bytes written, expected %lu", -- client_write_buf_idx, sizeof(expected) - 1 -+ client_write_buf_idx, sizeof(expected) - 1UL - ); - } - -diff --git a/tests/core/router/CMakeLists.txt b/tests/core/router/CMakeLists.txt ---- a/tests/core/router/CMakeLists.txt -+++ b/tests/core/router/CMakeLists.txt -@@ -7,6 +7,7 @@ - test_core_router STATIC - ${LIGHTSD_SOURCE_DIR}/core/log.c - ${LIGHTSD_SOURCE_DIR}/core/proto.c -+ ${LIGHTSD_SOURCE_DIR}/core/stats.c - ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c - ${LIGHTSD_SOURCE_DIR}/lifx/timer.c - ${LIGHTSD_SOURCE_DIR}/lifx/wire_proto.c -@@ -15,10 +16,11 @@ - ) - - TARGET_LINK_LIBRARIES( -- test_core_router -- ${EVENT2_CORE_LIBRARY} -- ${TIME_MONOTONIC_LIBRARY} -+ test_core_router ${EVENT2_CORE_LIBRARY} ${TIME_MONOTONIC_LIBRARY} - ) -+IF (HAVE_LIBBSD) -+ TARGET_LINK_LIBRARIES(test_core_router ${LIBBSD_LIBRARY}) -+ENDIF (HAVE_LIBBSD) - - FUNCTION(ADD_ROUTER_TEST TEST_SOURCE) - ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_core_router) -diff --git a/tests/core/tests_shims.c b/tests/core/tests_shims.c ---- a/tests/core/tests_shims.c -+++ b/tests/core/tests_shims.c -@@ -26,6 +26,8 @@ - - struct event_base *lgtd_ev_base = NULL; - -+const char *lgtd_binds = NULL; -+ - void - lgtd_cleanup(void) - { -diff --git a/tests/lifx/gateway/CMakeLists.txt b/tests/lifx/gateway/CMakeLists.txt ---- a/tests/lifx/gateway/CMakeLists.txt -+++ b/tests/lifx/gateway/CMakeLists.txt -@@ -8,6 +8,7 @@ - ${LIGHTSD_SOURCE_DIR}/core/log.c - ${LIGHTSD_SOURCE_DIR}/core/proto.c - ${LIGHTSD_SOURCE_DIR}/core/router.c -+ ${LIGHTSD_SOURCE_DIR}/core/stats.c - ${LIGHTSD_SOURCE_DIR}/lifx/broadcast.c - ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c - ${LIGHTSD_SOURCE_DIR}/lifx/timer.c -@@ -15,6 +16,9 @@ - ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c - ) - TARGET_LINK_LIBRARIES(test_lifx_gateway ${TIME_MONOTONIC_LIBRARY}) -+IF (HAVE_LIBBSD) -+ TARGET_LINK_LIBRARIES(test_lifx_gateway ${LIBBSD_LIBRARY}) -+ENDIF (HAVE_LIBBSD) - - FUNCTION(ADD_GATEWAY_TEST TEST_SOURCE) - ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_lifx_gateway) -diff --git a/tests/lifx/tagging/CMakeLists.txt b/tests/lifx/tagging/CMakeLists.txt ---- a/tests/lifx/tagging/CMakeLists.txt -+++ b/tests/lifx/tagging/CMakeLists.txt -@@ -6,8 +6,12 @@ - ADD_LIBRARY( - test_lifx_tagging STATIC - ${LIGHTSD_SOURCE_DIR}/core/log.c -+ ${LIGHTSD_SOURCE_DIR}/core/stats.c - ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c - ) -+IF (HAVE_LIBBSD) -+ TARGET_LINK_LIBRARIES(test_lifx_tagging ${LIBBSD_LIBRARY}) -+ENDIF (HAVE_LIBBSD) - - FUNCTION(ADD_TAGGING_TEST TEST_SOURCE) - ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_lifx_tagging) -diff --git a/tests/lifx/tests_shims.c b/tests/lifx/tests_shims.c ---- a/tests/lifx/tests_shims.c -+++ b/tests/lifx/tests_shims.c -@@ -15,6 +15,8 @@ - - struct event_base *lgtd_ev_base = NULL; - -+const char *lgtd_binds = NULL; -+ - void - lgtd_cleanup(void) - { -diff --git a/tests/lifx/wire_proto/CMakeLists.txt b/tests/lifx/wire_proto/CMakeLists.txt ---- a/tests/lifx/wire_proto/CMakeLists.txt -+++ b/tests/lifx/wire_proto/CMakeLists.txt -@@ -6,8 +6,12 @@ - ADD_LIBRARY( - test_lifx_wire_proto STATIC - ${LIGHTSD_SOURCE_DIR}/core/log.c -+ ${LIGHTSD_SOURCE_DIR}/core/stats.c - ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c - ) -+IF (HAVE_LIBBSD) -+ TARGET_LINK_LIBRARIES(test_lifx_wire_proto ${LIBBSD_LIBRARY}) -+ENDIF (HAVE_LIBBSD) - - FUNCTION(ADD_WIRE_PROTO_TEST TEST_SOURCE) - ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_lifx_wire_proto)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/add_tag_and_untag.patch Mon Aug 03 01:18:55 2015 -0700 @@ -0,0 +1,3738 @@ +# HG changeset patch +# Parent e6a5c70e43bf38915cfe7bf4614b7aa71be843e9 + +diff --git a/README.rst b/README.rst +--- a/README.rst ++++ b/README.rst +@@ -29,8 +29,7 @@ + - set_light_from_hsbk; + - set_waveform (change the light according to a function like SAW or SINE); + - get_light_state; +-- tag/untag (group/ungroup bulbs together, coming up: need unit & regression +- tests); ++- tag/untag (group/ungroup bulbs together); + - toggle (power on if off and vice-versa, coming up). + + The JSON-RPC interface works on top on IPv4/v6, over a command (named) pipe and +diff --git a/core/jsonrpc.c b/core/jsonrpc.c +--- a/core/jsonrpc.c ++++ b/core/jsonrpc.c +@@ -977,6 +977,90 @@ + lgtd_proto_target_list_clear(&targets); + } + ++static void ++lgtd_jsonrpc_check_and_call_proto_tag_or_untag(struct lgtd_client *client, ++ void (*lgtd_proto_fn)(struct lgtd_client *, ++ const struct lgtd_proto_target_list *, ++ const char *)) ++ ++{ ++ struct lgtd_jsonrpc_target_args { ++ const jsmntok_t *target; ++ int target_ntokens; ++ const jsmntok_t *tag; ++ } params = { NULL, 0, NULL }; ++ static const struct lgtd_jsonrpc_node schema[] = { ++ LGTD_JSONRPC_NODE( ++ "target", ++ offsetof(struct lgtd_jsonrpc_target_args, target), ++ offsetof(struct lgtd_jsonrpc_target_args, target_ntokens), ++ lgtd_jsonrpc_type_string_number_or_array, ++ false ++ ), ++ LGTD_JSONRPC_NODE( ++ "tag", ++ offsetof(struct lgtd_jsonrpc_target_args, tag), ++ -1, ++ lgtd_jsonrpc_type_string, ++ false ++ ) ++ }; ++ ++ struct lgtd_jsonrpc_request *req = client->current_request; ++ bool ok = lgtd_jsonrpc_extract_and_validate_params_against_schema( ++ ¶ms, ++ schema, ++ LGTD_ARRAY_SIZE(schema), ++ req->params, ++ req->params_ntokens, ++ client->json ++ ); ++ if (!ok) { ++ lgtd_jsonrpc_send_error( ++ client, LGTD_JSONRPC_INVALID_PARAMS, "Invalid parameters" ++ ); ++ return; ++ } ++ ++ struct lgtd_proto_target_list targets = SLIST_HEAD_INITIALIZER(&targets); ++ ok = lgtd_jsonrpc_build_target_list( ++ &targets, client, params.target, params.target_ntokens ++ ); ++ if (!ok) { ++ return; ++ } ++ ++ char *tag = strndup( ++ &client->json[params.tag->start], LGTD_JSONRPC_TOKEN_LEN(params.tag) ++ ); ++ if (!tag) { ++ lgtd_warn("can't allocate a tag"); ++ lgtd_jsonrpc_send_error( ++ client, LGTD_JSONRPC_INTERNAL_ERROR, "Can't allocate memory" ++ ); ++ goto error_strdup; ++ } ++ ++ lgtd_proto_fn(client, &targets, tag); ++ ++ free(tag); ++ ++error_strdup: ++ lgtd_proto_target_list_clear(&targets); ++} ++ ++static void ++lgtd_jsonrpc_check_and_call_tag(struct lgtd_client *client) ++{ ++ return lgtd_jsonrpc_check_and_call_proto_tag_or_untag(client, lgtd_proto_tag); ++} ++ ++static void ++lgtd_jsonrpc_check_and_call_untag(struct lgtd_client *client) ++{ ++ return lgtd_jsonrpc_check_and_call_proto_tag_or_untag(client, lgtd_proto_untag); ++} ++ + void + lgtd_jsonrpc_dispatch_request(struct lgtd_client *client, int parsed) + { +@@ -1001,6 +1085,14 @@ + LGTD_JSONRPC_METHOD( + "get_light_state", 1, // t + lgtd_jsonrpc_check_and_call_get_light_state ++ ), ++ LGTD_JSONRPC_METHOD( ++ "tag", 2, // t, tag ++ lgtd_jsonrpc_check_and_call_tag ++ ), ++ LGTD_JSONRPC_METHOD( ++ "untag", 2, // t, tag ++ lgtd_jsonrpc_check_and_call_untag + ) + }; + +diff --git a/core/proto.c b/core/proto.c +--- a/core/proto.c ++++ b/core/proto.c +@@ -224,3 +224,145 @@ + + lgtd_router_device_list_free(devices); + } ++ ++void ++lgtd_proto_tag(struct lgtd_client *client, ++ const struct lgtd_proto_target_list *targets, ++ const char *tag_label) ++{ ++ assert(client); ++ assert(targets); ++ assert(tag_label); ++ ++ struct lgtd_router_device_list *devices; ++ devices = lgtd_router_targets_to_devices(targets); ++ if (!devices) { ++ goto error_tag_alloc; ++ } ++ ++ struct lgtd_lifx_tag *tag = lgtd_lifx_tagging_find_tag(tag_label); ++ if (!tag) { ++ tag = lgtd_lifx_tagging_allocate_tag(tag_label); ++ if (!tag) { ++ goto error_tag_alloc; ++ } ++ lgtd_info("created tag [%s]", tag_label); ++ } ++ ++ struct lgtd_router_device *device; ++ struct lgtd_lifx_site *site; ++ ++ // Loop over the devices and do allocations first, this makes error ++ // handling easier (since you can't rollback enqueued packets) and build ++ // the list of affected gateways so we can do SET_TAG_LABELS: ++ SLIST_FOREACH(device, devices, link) { ++ struct lgtd_lifx_gateway *gw = device->device->gw; ++ int tag_id = lgtd_lifx_gateway_get_tag_id(gw, tag); ++ if (tag_id == -1) { ++ tag_id = lgtd_lifx_gateway_allocate_tag_id(gw, -1, tag_label); ++ if (tag_id == -1) { ++ goto error_site_alloc; ++ } ++ } ++ } ++ ++ // SET_TAG_LABELS, this is idempotent, do it everytime so we can recover ++ // from any bad state: ++ LIST_FOREACH(site, &tag->sites, link) { ++ int tag_id = site->tag_id; ++ assert(tag_id > -1 && tag_id < LGTD_LIFX_GATEWAY_MAX_TAGS); ++ struct lgtd_lifx_packet_tag_labels pkt = { .tags = 0 }; ++ pkt.tags = LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id); ++ strncpy(pkt.label, tag_label, sizeof(pkt.label) - 1); ++ lgtd_lifx_wire_encode_tag_labels(&pkt); ++ bool enqueued = lgtd_lifx_gateway_send_to_site( ++ site->gw, LGTD_LIFX_SET_TAG_LABELS, &pkt ++ ); ++ if (!enqueued) { ++ goto error_site_alloc; ++ } ++ lgtd_info( ++ "created tag [%s] with id %d on gw [%s]:%hu", ++ tag_label, tag_id, site->gw->ip_addr, site->gw->port ++ ); ++ } ++ ++ // Finally SET_TAGS on the devices: ++ SLIST_FOREACH(device, devices, link) { ++ struct lgtd_lifx_bulb *bulb = device->device; ++ int tag_id = lgtd_lifx_gateway_get_tag_id(bulb->gw, tag); ++ assert(tag_id > -1 && tag_id < LGTD_LIFX_GATEWAY_MAX_TAGS); ++ int tag_value = LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id); ++ if (!(bulb->state.tags & tag_value)) { ++ struct lgtd_lifx_packet_tags pkt; ++ pkt.tags = bulb->state.tags | tag_value; ++ lgtd_lifx_wire_encode_tags(&pkt); ++ lgtd_router_send_to_device(bulb, LGTD_LIFX_SET_TAGS, &pkt); ++ } ++ } ++ ++ SEND_RESULT(client, true); ++ goto fini; ++ ++error_site_alloc: ++ if (LIST_EMPTY(&tag->sites)) { ++ lgtd_lifx_tagging_deallocate_tag(tag); ++ } else { // tagging_decref will deallocate the tag for us: ++ struct lgtd_lifx_site *next_site; ++ LIST_FOREACH_SAFE(site, &tag->sites, link, next_site) { ++ lgtd_lifx_gateway_deallocate_tag_id(site->gw, site->tag_id); ++ } ++ } ++error_tag_alloc: ++ lgtd_client_send_error( ++ client, LGTD_CLIENT_INTERNAL_ERROR, "couldn't allocate new tag" ++ ); ++fini: ++ lgtd_router_device_list_free(devices); ++ return; ++} ++ ++void ++lgtd_proto_untag(struct lgtd_client *client, ++ const struct lgtd_proto_target_list *targets, ++ const char *tag_label) ++{ ++ assert(client); ++ assert(targets); ++ assert(tag_label); ++ ++ struct lgtd_lifx_tag *tag = lgtd_lifx_tagging_find_tag(tag_label); ++ if (!tag) { ++ SEND_RESULT(client, true); ++ return; ++ } ++ ++ struct lgtd_router_device_list *devices = NULL; ++ devices = lgtd_router_targets_to_devices(targets); ++ if (!devices) { ++ lgtd_client_send_error( ++ client, LGTD_CLIENT_INTERNAL_ERROR, "couldn't allocate memory" ++ ); ++ return; ++ } ++ ++ struct lgtd_router_device *device; ++ SLIST_FOREACH(device, devices, link) { ++ struct lgtd_lifx_bulb *bulb = device->device; ++ struct lgtd_lifx_gateway *gw = bulb->gw; ++ int tag_id = lgtd_lifx_gateway_get_tag_id(gw, tag); ++ if (tag_id != -1) { ++ int tag_value = LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id); ++ if (bulb->state.tags & tag_value) { ++ struct lgtd_lifx_packet_tags pkt; ++ pkt.tags = bulb->state.tags & ~tag_value; ++ lgtd_lifx_wire_encode_tags(&pkt); ++ lgtd_router_send_to_device(bulb, LGTD_LIFX_SET_TAGS, &pkt); ++ } ++ } ++ } ++ ++ SEND_RESULT(client, true); ++ ++ lgtd_router_device_list_free(devices); ++} +diff --git a/core/proto.h b/core/proto.h +--- a/core/proto.h ++++ b/core/proto.h +@@ -39,3 +39,5 @@ + void lgtd_proto_power_on(struct lgtd_client *, const struct lgtd_proto_target_list *); + void lgtd_proto_power_off(struct lgtd_client *, const struct lgtd_proto_target_list *); + void lgtd_proto_get_light_state(struct lgtd_client *, const struct lgtd_proto_target_list *); ++void lgtd_proto_tag(struct lgtd_client *, const struct lgtd_proto_target_list *, const char *); ++void lgtd_proto_untag(struct lgtd_client *, const struct lgtd_proto_target_list *, const char *); +diff --git a/lifx/bulb.c b/lifx/bulb.c +--- a/lifx/bulb.c ++++ b/lifx/bulb.c +@@ -77,12 +77,29 @@ + assert(bulb); + assert(bulb->gw); + ++#ifndef NDEBUG ++ // FIXME: Yeah, so an unit test lgtd_lifx_gateway_remove_and_close_bulb ++ // would be better because it can be automated, but this looks so much ++ // easier to do and this code path is often exercised: ++ int tag_id; ++ LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, bulb->state.tags) { ++ int n = 0; ++ struct lgtd_lifx_bulb *gw_bulb; ++ SLIST_FOREACH(gw_bulb, &bulb->gw->bulbs, link_by_gw) { ++ assert(gw_bulb != bulb); ++ if (LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id) & gw_bulb->state.tags) { ++ n++; ++ } ++ } ++ assert(bulb->gw->tag_refcounts[tag_id] == n); ++ } ++#endif ++ + LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(bulbs, -1); + if (bulb->state.power == LGTD_LIFX_POWER_ON) { + LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(bulbs_powered_on, -1); + } + RB_REMOVE(lgtd_lifx_bulb_map, &lgtd_lifx_bulbs_table, bulb); +- SLIST_REMOVE(&bulb->gw->bulbs, bulb, lgtd_lifx_bulb, link_by_gw); + lgtd_info( + "closed bulb \"%.*s\" (%s) on [%s]:%hu", + LGTD_LIFX_LABEL_SIZE, +@@ -108,6 +125,8 @@ + ); + } + ++ lgtd_lifx_gateway_update_tag_refcounts(bulb->gw, bulb->state.tags, state->tags); ++ + bulb->last_light_state_at = received_at; + memcpy(&bulb->state, state, sizeof(bulb->state)); + } +@@ -125,3 +144,13 @@ + + bulb->state.power = power; + } ++ ++void ++lgtd_lifx_bulb_set_tags(struct lgtd_lifx_bulb *bulb, uint64_t tags) ++{ ++ assert(bulb); ++ ++ lgtd_lifx_gateway_update_tag_refcounts(bulb->gw, bulb->state.tags, tags); ++ ++ bulb->state.tags = tags; ++} +diff --git a/lifx/bulb.h b/lifx/bulb.h +--- a/lifx/bulb.h ++++ b/lifx/bulb.h +@@ -68,3 +68,4 @@ + const struct lgtd_lifx_light_state *, + lgtd_time_mono_t); + void lgtd_lifx_bulb_set_power_state(struct lgtd_lifx_bulb *, uint16_t); ++void lgtd_lifx_bulb_set_tags(struct lgtd_lifx_bulb *, uint64_t); +diff --git a/lifx/gateway.c b/lifx/gateway.c +--- a/lifx/gateway.c ++++ b/lifx/gateway.c +@@ -71,9 +71,9 @@ + lgtd_lifx_tagging_decref(gw->tags[i], gw); + } + } +- struct lgtd_lifx_bulb *bulb, *next_bulb; +- SLIST_FOREACH_SAFE(bulb, &gw->bulbs, link_by_gw, next_bulb) { +- lgtd_lifx_bulb_close(bulb); ++ while (!SLIST_EMPTY(&gw->bulbs)) { ++ struct lgtd_lifx_bulb *bulb = SLIST_FIRST(&gw->bulbs); ++ lgtd_lifx_gateway_remove_and_close_bulb(gw, bulb); + } + + lgtd_info( +@@ -83,6 +83,23 @@ + free(gw); + } + ++void ++lgtd_lifx_gateway_remove_and_close_bulb(struct lgtd_lifx_gateway *gw, ++ struct lgtd_lifx_bulb *bulb) ++{ ++ assert(gw); ++ assert(bulb); ++ ++ int tag_id; ++ LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, bulb->state.tags) { ++ assert(gw->tag_refcounts[tag_id] > 0); ++ gw->tag_refcounts[tag_id]--; ++ } ++ SLIST_REMOVE(&gw->bulbs, bulb, lgtd_lifx_bulb, link_by_gw); ++ ++ lgtd_lifx_bulb_close(bulb); ++} ++ + static void + lgtd_lifx_gateway_write_callback(evutil_socket_t socket, + short events, void *ctx) +@@ -151,36 +168,77 @@ + } + } + ++static bool ++lgtd_lifx_gateway_send_to_site_impl(struct lgtd_lifx_gateway *gw, ++ enum lgtd_lifx_packet_type pkt_type, ++ const void *pkt, ++ const struct lgtd_lifx_packet_infos **pkt_infos) ++{ ++ assert(gw); ++ assert(pkt_infos); ++ ++ struct lgtd_lifx_packet_header hdr; ++ union lgtd_lifx_target target = { .addr = gw->site.as_array }; ++ *pkt_infos = lgtd_lifx_wire_setup_header( ++ &hdr, ++ LGTD_LIFX_TARGET_SITE, ++ target, ++ gw->site.as_array, ++ pkt_type ++ ); ++ assert(*pkt_infos); ++ ++ lgtd_lifx_gateway_enqueue_packet(gw, &hdr, pkt_type, pkt, (*pkt_infos)->size); ++ ++ return true; // FIXME, have real return values on the send paths... ++} ++ ++static bool ++lgtd_lifx_gateway_send_to_site_quiet(struct lgtd_lifx_gateway *gw, ++ enum lgtd_lifx_packet_type pkt_type, ++ const void *pkt) ++{ ++ ++ const struct lgtd_lifx_packet_infos *pkt_infos; ++ bool rv = lgtd_lifx_gateway_send_to_site_impl( ++ gw, pkt_type, pkt, &pkt_infos ++ ); ++ ++ lgtd_debug( ++ "sending %s to site %s", ++ pkt_infos->name, lgtd_addrtoa(gw->site.as_array) ++ ); ++ ++ return rv; // FIXME, have real return values on the send paths... ++} ++ ++bool ++lgtd_lifx_gateway_send_to_site(struct lgtd_lifx_gateway *gw, ++ enum lgtd_lifx_packet_type pkt_type, ++ const void *pkt) ++{ ++ const struct lgtd_lifx_packet_infos *pkt_infos; ++ bool rv = lgtd_lifx_gateway_send_to_site_impl( ++ gw, pkt_type, pkt, &pkt_infos ++ ); ++ ++ lgtd_info( ++ "sending %s to site %s", ++ pkt_infos->name, lgtd_addrtoa(gw->site.as_array) ++ ); ++ ++ return rv; // FIXME, have real return values on the send paths... ++} ++ + static void + lgtd_lifx_gateway_send_get_all_light_state(struct lgtd_lifx_gateway *gw) + { + assert(gw); + +- struct lgtd_lifx_packet_header hdr; +- union lgtd_lifx_target target = { .addr = gw->site.as_array }; ++ lgtd_lifx_gateway_send_to_site_quiet(gw, LGTD_LIFX_GET_LIGHT_STATE, NULL); + +- lgtd_lifx_wire_setup_header( +- &hdr, +- LGTD_LIFX_TARGET_SITE, +- target, +- gw->site.as_array, +- LGTD_LIFX_GET_LIGHT_STATE +- ); +- lgtd_lifx_gateway_enqueue_packet( +- gw, &hdr, LGTD_LIFX_GET_LIGHT_STATE, NULL, 0 +- ); +- +- struct lgtd_lifx_packet_get_tag_labels pkt = { .tags = LGTD_LIFX_ALL_TAGS }; +- lgtd_lifx_wire_setup_header( +- &hdr, +- LGTD_LIFX_TARGET_SITE, +- target, +- gw->site.as_array, +- LGTD_LIFX_GET_TAG_LABELS +- ); +- lgtd_lifx_gateway_enqueue_packet( +- gw, &hdr, LGTD_LIFX_GET_TAG_LABELS, &pkt, sizeof(pkt) +- ); ++ struct lgtd_lifx_packet_tags pkt = { .tags = LGTD_LIFX_ALL_TAGS }; ++ lgtd_lifx_gateway_send_to_site_quiet(gw, LGTD_LIFX_GET_TAG_LABELS, &pkt); + + gw->pending_refresh_req = true; + } +@@ -371,19 +429,55 @@ + } + + void ++lgtd_lifx_gateway_update_tag_refcounts(struct lgtd_lifx_gateway *gw, ++ uint64_t bulb_tags, ++ uint64_t pkt_tags) ++{ ++ uint64_t changes = bulb_tags ^ pkt_tags; ++ uint64_t added_tags = changes & pkt_tags; ++ uint64_t removed_tags = changes & bulb_tags; ++ int tag_id; ++ ++ LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, added_tags) { ++ if (gw->tag_refcounts[tag_id] != UINT8_MAX) { ++ gw->tag_refcounts[tag_id]++; ++ } else { ++ lgtd_warnx( ++ "reached refcount limit (%u) for tag [%s] (%d) on gw [%s]:%hu", ++ UINT8_MAX, gw->tags[tag_id] ? gw->tags[tag_id]->label : NULL, ++ tag_id, gw->ip_addr, gw->port ++ ); ++ } ++ } ++ ++ LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, removed_tags) { ++ assert(gw->tag_refcounts[tag_id] > 0); ++ if (--gw->tag_refcounts[tag_id] == 0) { ++ lgtd_info( ++ "deleting unused tag [%s] (%d) from gw [%s]:%hu (site %s)", ++ gw->tags[tag_id] ? gw->tags[tag_id]->label : NULL, tag_id, ++ gw->ip_addr, gw->port, lgtd_addrtoa(gw->site.as_array) ++ ); ++ struct lgtd_lifx_packet_tag_labels pkt = { ++ .tags = ~(gw->tag_ids & ~LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id)) ++ }; ++ lgtd_lifx_wire_encode_tag_labels(&pkt); ++ lgtd_lifx_gateway_send_to_site(gw, LGTD_LIFX_SET_TAG_LABELS, &pkt); ++ } ++ } ++} ++ ++void + lgtd_lifx_gateway_handle_pan_gateway(struct lgtd_lifx_gateway *gw, + const struct lgtd_lifx_packet_header *hdr, + const struct lgtd_lifx_packet_pan_gateway *pkt) + { +- (void)pkt; +- + assert(gw && hdr && pkt); + + lgtd_debug( +- "SET_PAN_GATEWAY <-- [%s]:%hu - %s site=%s", +- gw->ip_addr, gw->port, +- lgtd_addrtoa(hdr->target.device_addr), +- lgtd_addrtoa(hdr->site) ++ "SET_PAN_GATEWAY <-- [%s]:%hu - %s site=%s, service_type=%d", ++ gw->ip_addr, gw->port, lgtd_addrtoa(hdr->target.device_addr), ++ lgtd_addrtoa(hdr->site), pkt->service_type + ); + } + +@@ -485,16 +579,44 @@ + } + + int ++lgtd_lifx_gateway_get_tag_id(const struct lgtd_lifx_gateway *gw, ++ const struct lgtd_lifx_tag *tag) ++{ ++ assert(gw); ++ assert(tag); ++ ++ int tag_id; ++ LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, gw->tag_ids) { ++ if (gw->tags[tag_id] == tag) { ++ return tag_id; ++ } ++ } ++ ++ return -1; ++} ++ ++int + lgtd_lifx_gateway_allocate_tag_id(struct lgtd_lifx_gateway *gw, + int tag_id, + const char *tag_label) + { + assert(gw); + assert(tag_label); +- // allocating a new tag_id (tag_id == -1) isn't supported yet: +- assert(tag_id >= 0); ++ assert(tag_id >= -1); + assert(tag_id < LGTD_LIFX_GATEWAY_MAX_TAGS); + ++ if (tag_id == -1) { ++ tag_id = lgtd_lifx_wire_bitscan64_forward(~gw->tag_ids); ++ if (tag_id == -1) { ++ lgtd_warnx( ++ "no tag_id left for new tag [%s] on gw [%s]:%hu (site %s)", ++ tag_label, gw->ip_addr, gw->port, ++ lgtd_addrtoa(gw->site.as_array) ++ ); ++ return -1; ++ } ++ } ++ + if (!(gw->tag_ids & LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id))) { + struct lgtd_lifx_tag *tag; + tag = lgtd_lifx_tagging_incref(tag_label, gw, tag_id); +@@ -545,9 +667,9 @@ + assert(gw && hdr && pkt); + + lgtd_debug( +- "SET_TAG_LABELS <-- [%s]:%hu - %s label=%s, tags=%jx", ++ "SET_TAG_LABELS <-- [%s]:%hu - %s label=%.*s, tags=%jx", + gw->ip_addr, gw->port, lgtd_addrtoa(hdr->target.device_addr), +- pkt->label, (uintmax_t)pkt->tags ++ LGTD_LIFX_LABEL_SIZE, pkt->label, (uintmax_t)pkt->tags + ); + + int tag_id; +@@ -559,3 +681,38 @@ + } + } + } ++ ++void lgtd_lifx_gateway_handle_tags(struct lgtd_lifx_gateway *gw, ++ const struct lgtd_lifx_packet_header *hdr, ++ const struct lgtd_lifx_packet_tags *pkt) ++{ ++ assert(gw && hdr && pkt); ++ ++ lgtd_debug( ++ "SET_TAGS <-- [%s]:%hu - %s tags=%#jx", ++ gw->ip_addr, gw->port, lgtd_addrtoa(hdr->target.device_addr), ++ (uintmax_t)pkt->tags ++ ); ++ ++ struct lgtd_lifx_bulb *b = lgtd_lifx_gateway_get_or_open_bulb( ++ gw, hdr->target.device_addr ++ ); ++ if (!b) { ++ return; ++ } ++ ++ int tag_id; ++ LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, pkt->tags) { ++ if (!(gw->tag_ids & LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id))) { ++ lgtd_warnx( ++ "trying to set unknown tag_id %d (%#jx) " ++ "on bulb %s (%.*s), gw [%s]:%hu (site %s)", ++ tag_id, LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id), ++ lgtd_addrtoa(b->addr), LGTD_LIFX_LABEL_SIZE, b->state.label, ++ gw->ip_addr, gw->port, lgtd_addrtoa(gw->site.as_array) ++ ); ++ } ++ } ++ ++ lgtd_lifx_bulb_set_tags(b, pkt->tags); ++} +diff --git a/lifx/gateway.h b/lifx/gateway.h +--- a/lifx/gateway.h ++++ b/lifx/gateway.h +@@ -51,6 +51,7 @@ + } site; + uint64_t tag_ids; + struct lgtd_lifx_tag *tags[LGTD_LIFX_GATEWAY_MAX_TAGS]; ++ uint8_t tag_refcounts[LGTD_LIFX_GATEWAY_MAX_TAGS]; + evutil_socket_t socket; + // Those three timers let us measure the latency of the gateway. If we + // aren't the only client on the network then this won't be accurate since +@@ -84,6 +85,7 @@ + + void lgtd_lifx_gateway_close(struct lgtd_lifx_gateway *); + void lgtd_lifx_gateway_close_all(void); ++void lgtd_lifx_gateway_remove_and_close_bulb(struct lgtd_lifx_gateway *, struct lgtd_lifx_bulb *); + + void lgtd_lifx_gateway_force_refresh(struct lgtd_lifx_gateway *); + +@@ -92,7 +94,14 @@ + enum lgtd_lifx_packet_type, + const void *, + int); ++// This could be on router but it's LIFX specific so I'd rather keep it here: ++bool lgtd_lifx_gateway_send_to_site(struct lgtd_lifx_gateway *, ++ enum lgtd_lifx_packet_type, ++ const void *); + ++void lgtd_lifx_gateway_update_tag_refcounts(struct lgtd_lifx_gateway *, uint64_t, uint64_t); ++ ++int lgtd_lifx_gateway_get_tag_id(const struct lgtd_lifx_gateway *, const struct lgtd_lifx_tag *); + int lgtd_lifx_gateway_allocate_tag_id(struct lgtd_lifx_gateway *, int, const char *); + void lgtd_lifx_gateway_deallocate_tag_id(struct lgtd_lifx_gateway *, int); + +@@ -108,3 +117,6 @@ + void lgtd_lifx_gateway_handle_tag_labels(struct lgtd_lifx_gateway *, + const struct lgtd_lifx_packet_header *, + const struct lgtd_lifx_packet_tag_labels *); ++void lgtd_lifx_gateway_handle_tags(struct lgtd_lifx_gateway *, ++ const struct lgtd_lifx_packet_header *, ++ const struct lgtd_lifx_packet_tags *); +diff --git a/lifx/tagging.c b/lifx/tagging.c +--- a/lifx/tagging.c ++++ b/lifx/tagging.c +@@ -66,6 +66,32 @@ + } + + struct lgtd_lifx_tag * ++lgtd_lifx_tagging_allocate_tag(const char *tag_label) ++{ ++ assert(tag_label); ++ assert(strlen(tag_label) < LGTD_LIFX_LABEL_SIZE); ++ ++ struct lgtd_lifx_tag *tag = calloc(1, sizeof(*tag)); ++ if (!tag) { ++ return NULL; ++ } ++ ++ strncpy(tag->label, tag_label, sizeof(tag->label) - 1); ++ LIST_INSERT_HEAD(&lgtd_lifx_tags, tag, link); ++ return tag; ++} ++ ++void ++lgtd_lifx_tagging_deallocate_tag(struct lgtd_lifx_tag *tag) ++{ ++ assert(tag); ++ assert(LIST_EMPTY(&tag->sites)); ++ ++ LIST_REMOVE(tag, link); ++ free(tag); ++} ++ ++struct lgtd_lifx_tag * + lgtd_lifx_tagging_incref(const char *tag_label, + struct lgtd_lifx_gateway *gw, + int tag_id) +@@ -77,12 +103,10 @@ + bool dealloc_tag = false; + struct lgtd_lifx_tag *tag = lgtd_lifx_tagging_find_tag(tag_label); + if (!tag) { +- tag = calloc(1, sizeof(*tag)); ++ tag = lgtd_lifx_tagging_allocate_tag(tag_label); + if (!tag) { + return NULL; + } +- strncpy(tag->label, tag_label, sizeof(tag->label) - 1); +- LIST_INSERT_HEAD(&lgtd_lifx_tags, tag, link); + dealloc_tag = true; + } + +@@ -91,8 +115,7 @@ + site = calloc(1, sizeof(*site)); + if (!site) { + if (dealloc_tag) { +- LIST_REMOVE(tag, link); +- free(tag); ++ lgtd_lifx_tagging_deallocate_tag(tag); + } + errno = ENOMEM; + return NULL; +@@ -100,9 +123,10 @@ + if (dealloc_tag) { + lgtd_info("discovered tag [%s]", tag_label); + } +- lgtd_debug( +- "tag [%s] added to gw [%s]:%hu (site %s)", +- tag_label, gw->ip_addr, gw->port, lgtd_addrtoa(gw->site.as_array) ++ lgtd_info( ++ "tag [%s] added to gw [%s]:%hu (site %s) with tag_id %d", ++ tag_label, gw->ip_addr, gw->port, ++ lgtd_addrtoa(gw->site.as_array), tag_id + ); + site->gw = gw; + site->tag_id = tag_id; +@@ -132,8 +156,7 @@ + free(site); + } + if (LIST_EMPTY(&tag->sites)) { +- LIST_REMOVE(tag, link); + lgtd_info("forgetting unused tag [%s]", tag->label); +- free(tag); ++ lgtd_lifx_tagging_deallocate_tag(tag); + } + } +diff --git a/lifx/tagging.h b/lifx/tagging.h +--- a/lifx/tagging.h ++++ b/lifx/tagging.h +@@ -39,3 +39,6 @@ + void lgtd_lifx_tagging_decref(struct lgtd_lifx_tag *, struct lgtd_lifx_gateway *); + + struct lgtd_lifx_tag *lgtd_lifx_tagging_find_tag(const char *); ++struct lgtd_lifx_tag *lgtd_lifx_tagging_allocate_tag(const char *); ++ ++void lgtd_lifx_tagging_deallocate_tag(struct lgtd_lifx_tag *); +diff --git a/lifx/timer.c b/lifx/timer.c +--- a/lifx/timer.c ++++ b/lifx/timer.c +@@ -95,7 +95,7 @@ + "closing bulb \"%.*s\" that hasn't been updated for %dms", + LGTD_LIFX_LABEL_SIZE, bulb->state.label, light_state_lag + ); +- lgtd_lifx_bulb_close(bulb); ++ lgtd_lifx_gateway_remove_and_close_bulb(bulb->gw, bulb); + start_discovery = true; + continue; + } +diff --git a/lifx/wire_proto.c b/lifx/wire_proto.c +--- a/lifx/wire_proto.c ++++ b/lifx/wire_proto.c +@@ -92,6 +92,7 @@ + .handle = lgtd_lifx_wire_null_packet_handler + + static struct lgtd_lifx_packet_infos packet_table[] = { ++ // Gateway packets: + { + REQUEST_ONLY, + NO_PAYLOAD, +@@ -108,6 +109,43 @@ + }, + { + REQUEST_ONLY, ++ .name = "SET_TAG_LABELS", ++ .type = LGTD_LIFX_SET_TAG_LABELS, ++ .size = sizeof(struct lgtd_lifx_packet_tag_labels), ++ .encode = ENCODER(lgtd_lifx_wire_encode_tag_labels) ++ }, ++ { ++ REQUEST_ONLY, ++ .name = "GET_TAG_LABELS", ++ .type = LGTD_LIFX_GET_TAG_LABELS, ++ .size = sizeof(struct lgtd_lifx_packet_tags), ++ .encode = ENCODER(lgtd_lifx_wire_encode_tags) ++ }, ++ { ++ RESPONSE_ONLY, ++ .name = "TAG_LABELS", ++ .type = LGTD_LIFX_TAG_LABELS, ++ .size = sizeof(struct lgtd_lifx_packet_tag_labels), ++ .decode = DECODER(lgtd_lifx_wire_decode_tag_labels), ++ .handle = HANDLER(lgtd_lifx_gateway_handle_tag_labels) ++ }, ++ // Bulb packets: ++ { ++ REQUEST_ONLY, ++ .name = "SET_LIGHT_COLOR", ++ .type = LGTD_LIFX_SET_LIGHT_COLOR, ++ .size = sizeof(struct lgtd_lifx_packet_light_color), ++ .encode = ENCODER(lgtd_lifx_wire_encode_light_color) ++ }, ++ { ++ REQUEST_ONLY, ++ .name = "SET_WAVEFORM", ++ .type = LGTD_LIFX_SET_WAVEFORM, ++ .size = sizeof(struct lgtd_lifx_packet_waveform), ++ .encode = ENCODER(lgtd_lifx_wire_encode_waveform) ++ }, ++ { ++ REQUEST_ONLY, + NO_PAYLOAD, + .name = "GET_LIGHT_STATUS", + .type = LGTD_LIFX_GET_LIGHT_STATE +@@ -128,6 +166,7 @@ + .type = LGTD_LIFX_SET_POWER_STATE, + }, + { ++ RESPONSE_ONLY, + .name = "POWER_STATE", + .type = LGTD_LIFX_POWER_STATE, + .size = sizeof(struct lgtd_lifx_packet_power_state), +@@ -136,32 +175,18 @@ + }, + { + REQUEST_ONLY, +- .name = "SET_LIGHT_COLOR", +- .type = LGTD_LIFX_SET_LIGHT_COLOR, +- .size = sizeof(struct lgtd_lifx_packet_light_color), +- .encode = ENCODER(lgtd_lifx_wire_encode_light_color) +- }, +- { +- REQUEST_ONLY, +- .name = "SET_WAVEFORM", +- .type = LGTD_LIFX_SET_WAVEFORM, +- .size = sizeof(struct lgtd_lifx_packet_waveform), +- .encode = ENCODER(lgtd_lifx_wire_encode_waveform) +- }, +- { +- REQUEST_ONLY, +- .name = "GET_TAG_LABELS", +- .type = LGTD_LIFX_GET_TAG_LABELS, +- .size = sizeof(struct lgtd_lifx_packet_get_tag_labels), +- .encode = lgtd_lifx_wire_null_packet_encoder_decoder ++ .name = "SET_TAGS", ++ .type = LGTD_LIFX_SET_TAGS, ++ .size = sizeof(struct lgtd_lifx_packet_tags), ++ .encode = ENCODER(lgtd_lifx_wire_encode_tags) + }, + { + RESPONSE_ONLY, +- .name = "TAG_LABELS", +- .type = LGTD_LIFX_TAG_LABELS, +- .size = sizeof(struct lgtd_lifx_packet_tag_labels), +- .decode = DECODER(lgtd_lifx_wire_decode_tag_labels), +- .handle = HANDLER(lgtd_lifx_gateway_handle_tag_labels) ++ .name = "TAGS", ++ .type = LGTD_LIFX_TAGS, ++ .size = sizeof(struct lgtd_lifx_packet_tags), ++ .decode = DECODER(lgtd_lifx_wire_decode_tags), ++ .handle = HANDLER(lgtd_lifx_gateway_handle_tags) + } + }; + +@@ -356,6 +381,14 @@ + } + + void ++lgtd_lifx_wire_encode_tag_labels(struct lgtd_lifx_packet_tag_labels *pkt) ++{ ++ assert(pkt); ++ ++ pkt->tags = htole64(pkt->tags); ++} ++ ++void + lgtd_lifx_wire_decode_tag_labels(struct lgtd_lifx_packet_tag_labels *pkt) + { + assert(pkt); +@@ -363,3 +396,19 @@ + pkt->label[sizeof(pkt->label) - 1] = '\0'; + pkt->tags = le64toh(pkt->tags); + } ++ ++void ++lgtd_lifx_wire_encode_tags(struct lgtd_lifx_packet_tags *pkt) ++{ ++ assert(pkt); ++ ++ pkt->tags = htole64(pkt->tags); ++} ++ ++void ++lgtd_lifx_wire_decode_tags(struct lgtd_lifx_packet_tags *pkt) ++{ ++ assert(pkt); ++ ++ pkt->tags = le64toh(pkt->tags); ++} +diff --git a/lifx/wire_proto.h b/lifx/wire_proto.h +--- a/lifx/wire_proto.h ++++ b/lifx/wire_proto.h +@@ -238,7 +238,7 @@ + }; + + enum { LGTD_LIFX_ALL_TAGS = ~0 }; +-struct lgtd_lifx_packet_get_tag_labels { ++struct lgtd_lifx_packet_tags { + uint64le_t tags; + }; + +@@ -350,4 +350,7 @@ + void lgtd_lifx_wire_encode_light_color(struct lgtd_lifx_packet_light_color *); + void lgtd_lifx_wire_encode_waveform(struct lgtd_lifx_packet_waveform *); + ++void lgtd_lifx_wire_encode_tags(struct lgtd_lifx_packet_tags *); ++void lgtd_lifx_wire_decode_tags(struct lgtd_lifx_packet_tags *); ++void lgtd_lifx_wire_encode_tag_labels(struct lgtd_lifx_packet_tag_labels *); + void lgtd_lifx_wire_decode_tag_labels(struct lgtd_lifx_packet_tag_labels *); +diff --git a/tests/core/jsonrpc/test_jsonrpc_build_target_list.c b/tests/core/jsonrpc/test_jsonrpc_build_target_list.c +--- a/tests/core/jsonrpc/test_jsonrpc_build_target_list.c ++++ b/tests/core/jsonrpc/test_jsonrpc_build_target_list.c +@@ -1,6 +1,7 @@ + #include "jsonrpc.c" + + #include "mock_client_buf.h" ++#include "mock_gateway.h" + #include "test_jsonrpc_utils.h" + + static void +diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off.c +--- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off.c ++++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off.c +@@ -1,6 +1,7 @@ + #include "jsonrpc.c" + + #include "mock_client_buf.h" ++#include "mock_gateway.h" + + #define LGTD_TESTING_POWER_OFF + #include "test_jsonrpc_utils.h" +diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off_missing_target.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off_missing_target.c +--- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off_missing_target.c ++++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off_missing_target.c +@@ -1,6 +1,7 @@ + #include "jsonrpc.c" + + #include "mock_client_buf.h" ++#include "mock_gateway.h" + + #define LGTD_TESTING_POWER_OFF + #include "test_jsonrpc_utils.h" +diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on.c +--- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on.c ++++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on.c +@@ -1,6 +1,7 @@ + #include "jsonrpc.c" + + #include "mock_client_buf.h" ++#include "mock_gateway.h" + + #define LGTD_TESTING_POWER_ON + #include "test_jsonrpc_utils.h" +diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on_missing_target.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on_missing_target.c +--- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on_missing_target.c ++++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on_missing_target.c +@@ -1,6 +1,7 @@ + #include "jsonrpc.c" + + #include "mock_client_buf.h" ++#include "mock_gateway.h" + + #define LGTD_TESTING_POWER_ON + #include "test_jsonrpc_utils.h" +diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk.c +--- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk.c ++++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk.c +@@ -1,6 +1,7 @@ + #include "jsonrpc.c" + + #include "mock_client_buf.h" ++#include "mock_gateway.h" + + #define LGTD_TESTING_SET_LIGHT_FROM_HSBK + #include "test_jsonrpc_utils.h" +diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_from_array.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_from_array.c +--- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_from_array.c ++++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_from_array.c +@@ -1,6 +1,7 @@ + #include "jsonrpc.c" + + #include "mock_client_buf.h" ++#include "mock_gateway.h" + + #define LGTD_TESTING_SET_LIGHT_FROM_HSBK + #include "test_jsonrpc_utils.h" +diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_invalid_params.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_invalid_params.c +--- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_invalid_params.c ++++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_invalid_params.c +@@ -1,6 +1,7 @@ + #include "jsonrpc.c" + + #include "mock_client_buf.h" ++#include "mock_gateway.h" + + #define LGTD_TESTING_SET_LIGHT_FROM_HSBK + #include "test_jsonrpc_utils.h" +diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform.c +--- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform.c ++++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform.c +@@ -1,6 +1,7 @@ + #include "jsonrpc.c" + + #include "mock_client_buf.h" ++#include "mock_gateway.h" + + #define LGTD_TESTING_SET_WAVEFORM + #include "test_jsonrpc_utils.h" +diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform_invalid_params.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform_invalid_params.c +--- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform_invalid_params.c ++++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform_invalid_params.c +@@ -1,6 +1,7 @@ + #include "jsonrpc.c" + + #include "mock_client_buf.h" ++#include "mock_gateway.h" + + #define LGTD_TESTING_SET_WAVEFORM + #include "test_jsonrpc_utils.h" +diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_tag.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_tag.c +new file mode 100644 +--- /dev/null ++++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_tag.c +@@ -0,0 +1,65 @@ ++#include "jsonrpc.c" ++ ++#include "mock_client_buf.h" ++#include "mock_gateway.h" ++ ++#define MOCKED_LGTD_TAG ++#include "test_jsonrpc_utils.h" ++ ++static bool tag_called = false; ++ ++void ++lgtd_proto_tag(struct lgtd_client *client, ++ const struct lgtd_proto_target_list *targets, ++ const char *tag) ++{ ++ if (!client) { ++ errx(1, "missing client!"); ++ } ++ ++ if (strcmp(SLIST_FIRST(targets)->target, "*")) { ++ errx( ++ 1, "Invalid target [%s] (expected=[*])", ++ SLIST_FIRST(targets)->target ++ ); ++ } ++ ++ if (strcmp(tag, "suspensions")) { ++ errx(1, "Invalid tag [%s] (expected=[suspensions])", tag); ++ } ++ ++ tag_called = true; ++} ++ ++int ++main(void) ++{ ++ jsmntok_t tokens[32]; ++ const char json[] = ("{" ++ "\"jsonrpc\": \"2.0\"," ++ "\"method\": \"tag\"," ++ "\"params\": {\"target\": \"*\", \"tag\": \"suspensions\"}," ++ "\"id\": \"42\"" ++ "}"); ++ int parsed = parse_json( ++ tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json) ++ ); ++ ++ bool ok; ++ struct lgtd_jsonrpc_request req = TEST_REQUEST_INITIALIZER; ++ struct lgtd_client client = { ++ .io = NULL, .current_request = &req, .json = json ++ }; ++ ok = lgtd_jsonrpc_check_and_extract_request(&req, tokens, parsed, json); ++ if (!ok) { ++ errx(1, "can't parse request"); ++ } ++ ++ lgtd_jsonrpc_check_and_call_proto_tag_or_untag(&client, lgtd_proto_tag); ++ ++ if (!tag_called) { ++ errx(1, "lgtd_proto_tag wasn't called"); ++ } ++ ++ return 0; ++} +diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_tag_missing_params.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_tag_missing_params.c +new file mode 100644 +--- /dev/null ++++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_tag_missing_params.c +@@ -0,0 +1,53 @@ ++#include "jsonrpc.c" ++ ++#include "mock_client_buf.h" ++#include "mock_gateway.h" ++ ++#define MOCKED_LGTD_TAG ++#include "test_jsonrpc_utils.h" ++ ++static bool tag_called = false; ++ ++void ++lgtd_proto_tag(struct lgtd_client *client, ++ const struct lgtd_proto_target_list *targets, ++ const char *tag) ++{ ++ (void)client; ++ (void)targets; ++ (void)tag; ++ tag_called = true; ++} ++ ++int ++main(void) ++{ ++ jsmntok_t tokens[32]; ++ const char json[] = ("{" ++ "\"jsonrpc\": \"2.0\"," ++ "\"method\": \"tag\"," ++ "\"params\": {\"tag\": \"suspensions\"}," ++ "\"id\": \"42\"" ++ "}"); ++ int parsed = parse_json( ++ tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json) ++ ); ++ ++ bool ok; ++ struct lgtd_jsonrpc_request req = TEST_REQUEST_INITIALIZER; ++ struct lgtd_client client = { ++ .io = NULL, .current_request = &req, .json = json ++ }; ++ ok = lgtd_jsonrpc_check_and_extract_request(&req, tokens, parsed, json); ++ if (!ok) { ++ errx(1, "can't parse request"); ++ } ++ ++ lgtd_jsonrpc_check_and_call_proto_tag_or_untag(&client, lgtd_proto_tag); ++ ++ if (tag_called) { ++ errx(1, "lgtd_proto_tag was called"); ++ } ++ ++ return 0; ++} +diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_untag.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_untag.c +new file mode 100644 +--- /dev/null ++++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_untag.c +@@ -0,0 +1,65 @@ ++#include "jsonrpc.c" ++ ++#include "mock_client_buf.h" ++#include "mock_gateway.h" ++ ++#define MOCKED_LGTD_UNTAG ++#include "test_jsonrpc_utils.h" ++ ++static bool untag_called = false; ++ ++void ++lgtd_proto_untag(struct lgtd_client *client, ++ const struct lgtd_proto_target_list *targets, ++ const char *tag) ++{ ++ if (!client) { ++ errx(1, "missing client!"); ++ } ++ ++ if (strcmp(SLIST_FIRST(targets)->target, "#suspensions")) { ++ errx( ++ 1, "Invalid target [%s] (expected=[#suspensions])", ++ SLIST_FIRST(targets)->target ++ ); ++ } ++ ++ if (strcmp(tag, "suspensions")) { ++ errx(1, "Invalid tag [%s] (expected=[suspensions])", tag); ++ } ++ ++ untag_called = true; ++} ++ ++int ++main(void) ++{ ++ jsmntok_t tokens[32]; ++ const char json[] = ("{" ++ "\"jsonrpc\": \"2.0\"," ++ "\"method\": \"tag\"," ++ "\"params\": [[\"#suspensions\"], \"suspensions\"]," ++ "\"id\": \"42\"" ++ "}"); ++ int parsed = parse_json( ++ tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json) ++ ); ++ ++ bool ok; ++ struct lgtd_jsonrpc_request req = TEST_REQUEST_INITIALIZER; ++ struct lgtd_client client = { ++ .io = NULL, .current_request = &req, .json = json ++ }; ++ ok = lgtd_jsonrpc_check_and_extract_request(&req, tokens, parsed, json); ++ if (!ok) { ++ errx(1, "can't parse request"); ++ } ++ ++ lgtd_jsonrpc_check_and_call_proto_tag_or_untag(&client, lgtd_proto_untag); ++ ++ if (!untag_called) { ++ errx(1, "lgtd_proto_tag wasn't called"); ++ } ++ ++ return 0; ++} +diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_untag_invalid_params.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_untag_invalid_params.c +new file mode 100644 +--- /dev/null ++++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_untag_invalid_params.c +@@ -0,0 +1,53 @@ ++#include "jsonrpc.c" ++ ++#include "mock_client_buf.h" ++#include "mock_gateway.h" ++ ++#define MOCKED_LGTD_UNTAG ++#include "test_jsonrpc_utils.h" ++ ++static bool untag_called = false; ++ ++void ++lgtd_proto_untag(struct lgtd_client *client, ++ const struct lgtd_proto_target_list *targets, ++ const char *tag) ++{ ++ (void)client; ++ (void)targets; ++ (void)tag; ++ untag_called = true; ++} ++ ++int ++main(void) ++{ ++ jsmntok_t tokens[32]; ++ const char json[] = ("{" ++ "\"jsonrpc\": \"2.0\"," ++ "\"method\": \"tag\"," ++ "\"params\": [[\"#suspensions\"], [\"suspensions\"]]," ++ "\"id\": \"42\"" ++ "}"); ++ int parsed = parse_json( ++ tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json) ++ ); ++ ++ bool ok; ++ struct lgtd_jsonrpc_request req = TEST_REQUEST_INITIALIZER; ++ struct lgtd_client client = { ++ .io = NULL, .current_request = &req, .json = json ++ }; ++ ok = lgtd_jsonrpc_check_and_extract_request(&req, tokens, parsed, json); ++ if (!ok) { ++ errx(1, "can't parse request"); ++ } ++ ++ lgtd_jsonrpc_check_and_call_proto_tag_or_untag(&client, lgtd_proto_untag); ++ ++ if (untag_called) { ++ errx(1, "lgtd_proto_tag was called"); ++ } ++ ++ return 0; ++} +diff --git a/tests/core/jsonrpc/test_jsonrpc_extract_request_no_params.c b/tests/core/jsonrpc/test_jsonrpc_extract_request_no_params.c +--- a/tests/core/jsonrpc/test_jsonrpc_extract_request_no_params.c ++++ b/tests/core/jsonrpc/test_jsonrpc_extract_request_no_params.c +@@ -1,6 +1,7 @@ + #include "jsonrpc.c" + + #include "mock_client_buf.h" ++#include "mock_gateway.h" + #include "test_jsonrpc_utils.h" + + int +diff --git a/tests/core/jsonrpc/test_jsonrpc_extract_request_notification_no_params.c b/tests/core/jsonrpc/test_jsonrpc_extract_request_notification_no_params.c +--- a/tests/core/jsonrpc/test_jsonrpc_extract_request_notification_no_params.c ++++ b/tests/core/jsonrpc/test_jsonrpc_extract_request_notification_no_params.c +@@ -1,6 +1,7 @@ + #include "jsonrpc.c" + + #include "mock_client_buf.h" ++#include "mock_gateway.h" + #include "test_jsonrpc_utils.h" + + int +diff --git a/tests/core/jsonrpc/test_jsonrpc_extract_request_params_array.c b/tests/core/jsonrpc/test_jsonrpc_extract_request_params_array.c +--- a/tests/core/jsonrpc/test_jsonrpc_extract_request_params_array.c ++++ b/tests/core/jsonrpc/test_jsonrpc_extract_request_params_array.c +@@ -1,6 +1,7 @@ + #include "jsonrpc.c" + + #include "mock_client_buf.h" ++#include "mock_gateway.h" + #include "test_jsonrpc_utils.h" + + int +diff --git a/tests/core/jsonrpc/test_jsonrpc_extract_request_params_obj.c b/tests/core/jsonrpc/test_jsonrpc_extract_request_params_obj.c +--- a/tests/core/jsonrpc/test_jsonrpc_extract_request_params_obj.c ++++ b/tests/core/jsonrpc/test_jsonrpc_extract_request_params_obj.c +@@ -1,6 +1,7 @@ + #include "jsonrpc.c" + + #include "mock_client_buf.h" ++#include "mock_gateway.h" + #include "test_jsonrpc_utils.h" + + int +diff --git a/tests/core/jsonrpc/test_jsonrpc_extract_request_valid_notification.c b/tests/core/jsonrpc/test_jsonrpc_extract_request_valid_notification.c +--- a/tests/core/jsonrpc/test_jsonrpc_extract_request_valid_notification.c ++++ b/tests/core/jsonrpc/test_jsonrpc_extract_request_valid_notification.c +@@ -1,6 +1,7 @@ + #include "jsonrpc.c" + + #include "mock_client_buf.h" ++#include "mock_gateway.h" + #include "test_jsonrpc_utils.h" + + int +diff --git a/tests/core/jsonrpc/test_jsonrpc_send_error.c b/tests/core/jsonrpc/test_jsonrpc_send_error.c +--- a/tests/core/jsonrpc/test_jsonrpc_send_error.c ++++ b/tests/core/jsonrpc/test_jsonrpc_send_error.c +@@ -1,6 +1,7 @@ + #include "jsonrpc.c" + + #include "mock_client_buf.h" ++#include "mock_gateway.h" + #include "test_jsonrpc_utils.h" + + int +diff --git a/tests/core/jsonrpc/test_jsonrpc_send_response.c b/tests/core/jsonrpc/test_jsonrpc_send_response.c +--- a/tests/core/jsonrpc/test_jsonrpc_send_response.c ++++ b/tests/core/jsonrpc/test_jsonrpc_send_response.c +@@ -1,6 +1,7 @@ + #include "jsonrpc.c" + + #include "mock_client_buf.h" ++#include "mock_gateway.h" + #include "test_jsonrpc_utils.h" + + int +diff --git a/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_invalid.c b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_invalid.c +--- a/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_invalid.c ++++ b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_invalid.c +@@ -1,6 +1,7 @@ + #include "jsonrpc.c" + + #include "mock_client_buf.h" ++#include "mock_gateway.h" + #include "test_jsonrpc_utils.h" + + static void +diff --git a/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_valid.c b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_valid.c +--- a/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_valid.c ++++ b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_valid.c +@@ -1,6 +1,7 @@ + #include "jsonrpc.c" + + #include "mock_client_buf.h" ++#include "mock_gateway.h" + #include "test_jsonrpc_utils.h" + + static void +diff --git a/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_invalid.c b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_invalid.c +--- a/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_invalid.c ++++ b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_invalid.c +@@ -1,6 +1,7 @@ + #include "jsonrpc.c" + + #include "mock_client_buf.h" ++#include "mock_gateway.h" + #include "test_jsonrpc_utils.h" + + static void +diff --git a/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_valid.c b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_valid.c +--- a/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_valid.c ++++ b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_valid.c +@@ -1,6 +1,7 @@ + #include "jsonrpc.c" + + #include "mock_client_buf.h" ++#include "mock_gateway.h" + #include "test_jsonrpc_utils.h" + + static void +diff --git a/tests/core/jsonrpc/test_jsonrpc_type_integer.c b/tests/core/jsonrpc/test_jsonrpc_type_integer.c +--- a/tests/core/jsonrpc/test_jsonrpc_type_integer.c ++++ b/tests/core/jsonrpc/test_jsonrpc_type_integer.c +@@ -1,6 +1,7 @@ + #include "jsonrpc.c" + + #include "mock_client_buf.h" ++#include "mock_gateway.h" + #include "test_jsonrpc_utils.h" + + int +diff --git a/tests/core/jsonrpc/test_jsonrpc_type_integer_invalid_characters.c b/tests/core/jsonrpc/test_jsonrpc_type_integer_invalid_characters.c +--- a/tests/core/jsonrpc/test_jsonrpc_type_integer_invalid_characters.c ++++ b/tests/core/jsonrpc/test_jsonrpc_type_integer_invalid_characters.c +@@ -1,6 +1,7 @@ + #include "jsonrpc.c" + + #include "mock_client_buf.h" ++#include "mock_gateway.h" + #include "test_jsonrpc_utils.h" + + int +diff --git a/tests/core/jsonrpc/test_jsonrpc_type_integer_too_big.c b/tests/core/jsonrpc/test_jsonrpc_type_integer_too_big.c +--- a/tests/core/jsonrpc/test_jsonrpc_type_integer_too_big.c ++++ b/tests/core/jsonrpc/test_jsonrpc_type_integer_too_big.c +@@ -1,6 +1,7 @@ + #include "jsonrpc.c" + + #include "mock_client_buf.h" ++#include "mock_gateway.h" + #include "test_jsonrpc_utils.h" + + int +diff --git a/tests/core/jsonrpc/test_jsonrpc_type_integer_too_small.c b/tests/core/jsonrpc/test_jsonrpc_type_integer_too_small.c +--- a/tests/core/jsonrpc/test_jsonrpc_type_integer_too_small.c ++++ b/tests/core/jsonrpc/test_jsonrpc_type_integer_too_small.c +@@ -1,6 +1,7 @@ + #include "jsonrpc.c" + + #include "mock_client_buf.h" ++#include "mock_gateway.h" + #include "test_jsonrpc_utils.h" + + int +diff --git a/tests/core/jsonrpc/test_jsonrpc_uint16_range_to_float_string.c b/tests/core/jsonrpc/test_jsonrpc_uint16_range_to_float_string.c +--- a/tests/core/jsonrpc/test_jsonrpc_uint16_range_to_float_string.c ++++ b/tests/core/jsonrpc/test_jsonrpc_uint16_range_to_float_string.c +@@ -3,6 +3,7 @@ + #include "jsonrpc.c" + + #include "mock_client_buf.h" ++#include "mock_gateway.h" + #include "test_jsonrpc_utils.h" + + int +diff --git a/tests/core/jsonrpc/test_jsonrpc_utils.h b/tests/core/jsonrpc/test_jsonrpc_utils.h +--- a/tests/core/jsonrpc/test_jsonrpc_utils.h ++++ b/tests/core/jsonrpc/test_jsonrpc_utils.h +@@ -95,3 +95,27 @@ + (void)targets; + } + #endif ++ ++#ifndef MOCKED_LGTD_TAG ++void ++lgtd_proto_tag(struct lgtd_client *client, ++ const struct lgtd_proto_target_list *targets, ++ const char *tag_label) ++{ ++ (void)client; ++ (void)targets; ++ (void)tag_label; ++} ++#endif ++ ++#ifndef MOCKED_LGTD_UNTAG ++void ++lgtd_proto_untag(struct lgtd_client *client, ++ const struct lgtd_proto_target_list *targets, ++ const char *tag_label) ++{ ++ (void)client; ++ (void)targets; ++ (void)tag_label; ++} ++#endif +diff --git a/tests/core/mock_event2.h b/tests/core/mock_event2.h +new file mode 100644 +--- /dev/null ++++ b/tests/core/mock_event2.h +@@ -0,0 +1,109 @@ ++#pragma once ++ ++#ifndef MOCKED_EVBUFFER_DRAIN ++int ++evbuffer_drain(struct evbuffer *buf, size_t len) ++{ ++ (void)buf; ++ (void)len; ++ return 0; ++} ++#endif ++ ++#ifndef MOCKED_EVBUFFER_NEW ++struct evbuffer * ++evbuffer_new(void) ++{ ++ return NULL; ++} ++#endif ++ ++#ifndef MOCKED_EVENT_FREE ++void ++evbuffer_free(struct evbuffer *buf) ++{ ++ (void)buf; ++} ++#endif ++ ++#ifndef MOCKED_EVBUFFER_GET_LENGTH ++size_t ++evbuffer_get_length(const struct evbuffer *buf) ++{ ++ (void)buf; ++ return 0; ++} ++#endif ++ ++#ifndef MOCKED_EVBUFFER_PULLUP ++unsigned char * ++evbuffer_pullup(struct evbuffer *buf, ev_ssize_t size) ++{ ++ (void)buf; ++ (void)size; ++ return NULL; ++} ++#endif ++ ++#ifndef MOCKED_EVBUFFER_READ ++int ++evbuffer_read(struct evbuffer *buffer, evutil_socket_t fd, int howmuch) ++{ ++ (void)buffer; ++ (void)fd; ++ return howmuch; ++} ++#endif ++ ++#ifndef MOCKED_EVENT_ADD ++int ++event_add(struct event *ev, const struct timeval *timeout) ++{ ++ (void)ev; ++ (void)timeout; ++ return 0; ++} ++#endif ++ ++#ifndef MOCKED_EVENT_DEL ++int ++event_del(struct event *ev) ++{ ++ (void)ev; ++ return 0; ++} ++#endif ++ ++#ifndef MOCKED_EVENT_FREE ++void ++event_free(struct event *ev) ++{ ++ (void)ev; ++} ++#endif ++ ++#ifndef MOCKED_EVENT_NEW ++struct event * ++event_new(struct event_base *base, ++ evutil_socket_t fd, ++ short events, ++ event_callback_fn cb, ++ void *ctx) ++{ ++ (void)base; ++ (void)fd; ++ (void)events; ++ (void)cb; ++ (void)ctx; ++ return NULL; ++} ++#endif ++ ++#ifndef MOCKED_EVUTIL_MAKE_SOCKET_NONBLOCKING ++int ++evutil_make_socket_nonblocking(evutil_socket_t fd) ++{ ++ (void)fd; ++ return 0; ++} ++#endif +diff --git a/tests/core/proto/CMakeLists.txt b/tests/core/proto/CMakeLists.txt +--- a/tests/core/proto/CMakeLists.txt ++++ b/tests/core/proto/CMakeLists.txt +@@ -9,6 +9,7 @@ + ${LIGHTSD_SOURCE_DIR}/core/jsonrpc.c + ${LIGHTSD_SOURCE_DIR}/core/stats.c + ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c ++ ${LIGHTSD_SOURCE_DIR}/lifx/tagging.c + ${LIGHTSD_SOURCE_DIR}/lifx/timer.c + ${LIGHTSD_SOURCE_DIR}/lifx/wire_proto.c + ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c +diff --git a/tests/core/proto/test_proto_get_light_state_empty_device_list.c b/tests/core/proto/test_proto_get_light_state_empty_device_list.c +--- a/tests/core/proto/test_proto_get_light_state_empty_device_list.c ++++ b/tests/core/proto/test_proto_get_light_state_empty_device_list.c +@@ -2,6 +2,7 @@ + + #include "mock_client_buf.h" + #include "mock_daemon.h" ++#include "mock_gateway.h" + #include "tests_utils.h" + + #define MOCKED_ROUTER_TARGETS_TO_DEVICES +diff --git a/tests/core/proto/test_proto_get_light_state_null_device_list.c b/tests/core/proto/test_proto_get_light_state_null_device_list.c +--- a/tests/core/proto/test_proto_get_light_state_null_device_list.c ++++ b/tests/core/proto/test_proto_get_light_state_null_device_list.c +@@ -2,6 +2,7 @@ + + #include "mock_client_buf.h" + #include "mock_daemon.h" ++#include "mock_gateway.h" + #include "tests_utils.h" + + #define MOCKED_ROUTER_TARGETS_TO_DEVICES +diff --git a/tests/core/proto/test_proto_power_off.c b/tests/core/proto/test_proto_power_off.c +--- a/tests/core/proto/test_proto_power_off.c ++++ b/tests/core/proto/test_proto_power_off.c +@@ -2,6 +2,7 @@ + + #include "mock_client_buf.h" + #include "mock_daemon.h" ++#include "mock_gateway.h" + #include "tests_utils.h" + + #define MOCKED_CLIENT_SEND_RESPONSE +diff --git a/tests/core/proto/test_proto_power_off_routing_error.c b/tests/core/proto/test_proto_power_off_routing_error.c +--- a/tests/core/proto/test_proto_power_off_routing_error.c ++++ b/tests/core/proto/test_proto_power_off_routing_error.c +@@ -2,6 +2,7 @@ + + #include "mock_client_buf.h" + #include "mock_daemon.h" ++#include "mock_gateway.h" + #include "tests_utils.h" + + #define MOCKED_CLIENT_SEND_RESPONSE +diff --git a/tests/core/proto/test_proto_power_on.c b/tests/core/proto/test_proto_power_on.c +--- a/tests/core/proto/test_proto_power_on.c ++++ b/tests/core/proto/test_proto_power_on.c +@@ -2,6 +2,7 @@ + + #include "mock_client_buf.h" + #include "mock_daemon.h" ++#include "mock_gateway.h" + #include "tests_utils.h" + + #define MOCKED_CLIENT_SEND_RESPONSE +diff --git a/tests/core/proto/test_proto_power_on_routing_error.c b/tests/core/proto/test_proto_power_on_routing_error.c +--- a/tests/core/proto/test_proto_power_on_routing_error.c ++++ b/tests/core/proto/test_proto_power_on_routing_error.c +@@ -2,6 +2,7 @@ + + #include "mock_client_buf.h" + #include "mock_daemon.h" ++#include "mock_gateway.h" + #include "tests_utils.h" + + #define MOCKED_CLIENT_SEND_RESPONSE +diff --git a/tests/core/proto/test_proto_set_light_from_hsbk.c b/tests/core/proto/test_proto_set_light_from_hsbk.c +--- a/tests/core/proto/test_proto_set_light_from_hsbk.c ++++ b/tests/core/proto/test_proto_set_light_from_hsbk.c +@@ -4,6 +4,7 @@ + + #include "mock_client_buf.h" + #include "mock_daemon.h" ++#include "mock_gateway.h" + #include "tests_utils.h" + + #define MOCKED_CLIENT_SEND_RESPONSE +diff --git a/tests/core/proto/test_proto_set_light_from_hsbk_on_routing_error.c b/tests/core/proto/test_proto_set_light_from_hsbk_on_routing_error.c +--- a/tests/core/proto/test_proto_set_light_from_hsbk_on_routing_error.c ++++ b/tests/core/proto/test_proto_set_light_from_hsbk_on_routing_error.c +@@ -4,6 +4,7 @@ + + #include "mock_client_buf.h" + #include "mock_daemon.h" ++#include "mock_gateway.h" + #include "tests_utils.h" + + #define MOCKED_CLIENT_SEND_RESPONSE +diff --git a/tests/core/proto/test_proto_set_waveform.c b/tests/core/proto/test_proto_set_waveform.c +--- a/tests/core/proto/test_proto_set_waveform.c ++++ b/tests/core/proto/test_proto_set_waveform.c +@@ -4,6 +4,7 @@ + + #include "mock_client_buf.h" + #include "mock_daemon.h" ++#include "mock_gateway.h" + #include "tests_utils.h" + + #define MOCKED_CLIENT_SEND_RESPONSE +diff --git a/tests/core/proto/test_proto_set_waveform_on_routing_error.c b/tests/core/proto/test_proto_set_waveform_on_routing_error.c +--- a/tests/core/proto/test_proto_set_waveform_on_routing_error.c ++++ b/tests/core/proto/test_proto_set_waveform_on_routing_error.c +@@ -4,6 +4,7 @@ + + #include "mock_client_buf.h" + #include "mock_daemon.h" ++#include "mock_gateway.h" + #include "tests_utils.h" + + #define MOCKED_CLIENT_SEND_RESPONSE +diff --git a/tests/core/proto/test_proto_tag_create.c b/tests/core/proto/test_proto_tag_create.c +new file mode 100644 +--- /dev/null ++++ b/tests/core/proto/test_proto_tag_create.c +@@ -0,0 +1,253 @@ ++#include "proto.c" ++ ++#include "mock_client_buf.h" ++#include "mock_daemon.h" ++#define MOCKED_LIFX_GATEWAY_SEND_TO_SITE ++#define MOCKED_LIFX_GATEWAY_ALLOCATE_TAG_ID ++#include "mock_gateway.h" ++#include "tests_utils.h" ++ ++#define MOCKED_ROUTER_TARGETS_TO_DEVICES ++#define MOCKED_ROUTER_SEND_TO_DEVICE ++#define MOCKED_ROUTER_DEVICE_LIST_FREE ++#include "tests_proto_utils.h" ++ ++#define FAKE_TARGET_LIST (void *)0x2a ++ ++static struct lgtd_router_device_list devices = ++ SLIST_HEAD_INITIALIZER(&devices); ++static struct lgtd_router_device_list device_1_only = ++ SLIST_HEAD_INITIALIZER(&device_1_only); ++ ++static bool send_to_device_called = false; ++ ++void ++lgtd_router_send_to_device(struct lgtd_lifx_bulb *bulb, ++ enum lgtd_lifx_packet_type pkt_type, ++ void *pkt) ++{ ++ if (!bulb) { ++ errx(1, "lgtd_router_send_to_device must be called with a bulb"); ++ } ++ ++ uint8_t expected_addr[LGTD_LIFX_ADDR_LENGTH] = { 1, 2, 3, 4, 5 }; ++ if (memcmp(bulb->addr, expected_addr, LGTD_LIFX_ADDR_LENGTH)) { ++ errx( ++ 1, "got bulb with addr %s (expected %s)", ++ lgtd_addrtoa(bulb->addr), lgtd_addrtoa(expected_addr) ++ ); ++ } ++ ++ if (pkt_type != LGTD_LIFX_SET_TAGS) { ++ errx( ++ 1, "got packet type %d (expected %d)", pkt_type, LGTD_LIFX_SET_TAGS ++ ); ++ } ++ ++ if (!pkt) { ++ errx(1, "missing SET_TAGS payload"); ++ } ++ ++ const struct lgtd_lifx_packet_tags *pkt_tags = pkt; ++ uint64_t tags = le64toh(pkt_tags->tags); ++ if (tags != 0x1) { ++ errx( ++ 1, "invalid SET_TAGS payload=%#jx (expected %#x)", ++ (uintmax_t)tags, 0x1 ++ ); ++ } ++ ++ send_to_device_called = true; ++} ++ ++static bool gateway_send_to_site_called = false; ++ ++bool ++lgtd_lifx_gateway_send_to_site(struct lgtd_lifx_gateway *gw, ++ enum lgtd_lifx_packet_type pkt_type, ++ const void *pkt) ++{ ++ if (!gw) { ++ errx(1, "missing gateway"); ++ } ++ ++ if (pkt_type != LGTD_LIFX_SET_TAG_LABELS) { ++ errx( ++ 1, "got packet type %#x (expected %#x)", ++ pkt_type, LGTD_LIFX_SET_TAG_LABELS ++ ); ++ } ++ ++ const struct lgtd_lifx_packet_tag_labels *pkt_tag_labels = pkt; ++ uint64_t tags = le64toh(pkt_tag_labels->tags); ++ if (tags != 0x1) { ++ errx(1, "got tags %#jx (expected %#x)", (uintmax_t)tags, 0x1); ++ } ++ ++ if (strcmp(pkt_tag_labels->label, "dub")) { ++ errx(1, "got label %s (expected dub)", pkt_tag_labels->label); ++ } ++ ++ gateway_send_to_site_called = true; ++ ++ return true; ++} ++ ++static bool gateway_allocate_tag_id_called = false; ++ ++int ++lgtd_lifx_gateway_allocate_tag_id(struct lgtd_lifx_gateway *gw, ++ int tag_id, ++ const char *tag_label) ++{ ++ if (gateway_allocate_tag_id_called) { ++ errx( ++ 1, "lgtd_lifx_gateway_allocate_tag_id " ++ "should have been called once only" ++ ); ++ } ++ ++ if (tag_id != -1) { ++ errx( ++ 1, "lgtd_lifx_gateway_allocate_tag_id " ++ "tag_id %d (expected -1)", tag_id ++ ); ++ } ++ ++ if (!gw) { ++ errx( ++ 1, "lgtd_lifx_gateway_allocate_tag_id " ++ "must be called with gateway" ++ ); ++ } ++ ++ if (!tag_label) { ++ errx( ++ 1, "lgtd_lifx_gateway_allocate_tag_id " ++ "must be called with a tag_label" ++ ); ++ } ++ ++ tag_id = 0; ++ ++ struct lgtd_lifx_tag *tag = lgtd_lifx_tagging_find_tag(tag_label); ++ if (!tag) { ++ errx(1, "tag %s wasn't found", tag_label); ++ } ++ lgtd_tests_add_tag_to_gw(tag, gw, tag_id); ++ ++ gateway_allocate_tag_id_called = true; ++ ++ return tag_id; ++} ++ ++static bool device_list_free_called = false; ++ ++void ++lgtd_router_device_list_free(struct lgtd_router_device_list *devices) ++{ ++ if (!devices) { ++ lgtd_errx(1, "the device list must be passed"); ++ } ++ ++ device_list_free_called = true; ++} ++ ++struct lgtd_router_device_list * ++lgtd_router_targets_to_devices(const struct lgtd_proto_target_list *targets) ++{ ++ if (targets != FAKE_TARGET_LIST) { ++ lgtd_errx(1, "unexpected targets list"); ++ } ++ ++ return &device_1_only; ++} ++ ++static void ++setup_devices(void) ++{ ++ static struct lgtd_lifx_gateway gw_bulb_1 = { ++ .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_1.bulbs) ++ }; ++ static struct lgtd_lifx_bulb bulb_1 = { ++ .addr = { 1, 2, 3, 4, 5 }, ++ .state = { ++ .hue = 0xaaaa, ++ .saturation = 0xffff, ++ .brightness = 0xbbbb, ++ .kelvin = 3600, ++ .label = "wave", ++ .power = LGTD_LIFX_POWER_ON, ++ .tags = 0 ++ }, ++ .gw = &gw_bulb_1 ++ }; ++ static struct lgtd_router_device device_1 = { .device = &bulb_1 }; ++ SLIST_INSERT_HEAD(&devices, &device_1, link); ++ SLIST_INSERT_HEAD(&device_1_only, &device_1, link); ++ ++ struct lgtd_lifx_tag *gw_2_tag_1 = lgtd_tests_insert_mock_tag("vapor"); ++ struct lgtd_lifx_tag *gw_2_tag_2 = lgtd_tests_insert_mock_tag("d^-^b"); ++ struct lgtd_lifx_tag *gw_2_tag_3 = lgtd_tests_insert_mock_tag("wave~"); ++ static struct lgtd_lifx_gateway gw_bulb_2 = { ++ .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_2.bulbs), ++ .tag_ids = 0x7 ++ }; ++ lgtd_tests_add_tag_to_gw(gw_2_tag_1, &gw_bulb_2, 0); ++ lgtd_tests_add_tag_to_gw(gw_2_tag_2, &gw_bulb_2, 1); ++ lgtd_tests_add_tag_to_gw(gw_2_tag_3, &gw_bulb_2, 2); ++ static struct lgtd_lifx_bulb bulb_2 = { ++ .addr = { 5, 4, 3, 2, 1 }, ++ .state = { ++ .hue = 0x0000, ++ .saturation = 0x0000, ++ .brightness = 0xffff, ++ .kelvin = 4000, ++ .label = "", ++ .power = LGTD_LIFX_POWER_OFF, ++ .tags = 0x3 ++ }, ++ .gw = &gw_bulb_2 ++ }; ++ static struct lgtd_router_device device_2 = { .device = &bulb_2 }; ++ SLIST_INSERT_HEAD(&devices, &device_2, link); ++} ++ ++int ++main(void) ++{ ++ struct lgtd_client client = { .io = FAKE_BUFFEREVENT }; ++ ++ setup_devices(); ++ ++ lgtd_proto_tag(&client, FAKE_TARGET_LIST, "dub"); ++ ++ const char expected[] = "true"; ++ if (client_write_buf_idx != sizeof(expected) - 1) { ++ lgtd_errx( ++ 1, ++ "%d bytes written, expected %lu " ++ "(got %.*s instead of %s)", ++ client_write_buf_idx, sizeof(expected) - 1UL, ++ client_write_buf_idx, client_write_buf, expected ++ ); ++ } ++ if (memcmp(expected, client_write_buf, sizeof(expected) - 1)) { ++ lgtd_errx( ++ 1, "got %.*s instead of %s", ++ client_write_buf_idx, client_write_buf, expected ++ ); ++ } ++ ++ if (!gateway_send_to_site_called) { ++ lgtd_errx(1, "SET_TAG_LABELS wasn't sent"); ++ } ++ if (!device_list_free_called) { ++ lgtd_errx(1, "the list of devices hasn't been freed"); ++ } ++ if (!send_to_device_called) { ++ lgtd_errx(1, "SET_TAGS wasn't send to any device"); ++ } ++ ++ return 0; ++} +diff --git a/tests/core/proto/test_proto_tag_create_lifx_gw_tag_ids_full.c b/tests/core/proto/test_proto_tag_create_lifx_gw_tag_ids_full.c +new file mode 100644 +--- /dev/null ++++ b/tests/core/proto/test_proto_tag_create_lifx_gw_tag_ids_full.c +@@ -0,0 +1,209 @@ ++#include "proto.c" ++ ++#include "mock_client_buf.h" ++#include "mock_daemon.h" ++#define MOCKED_LIFX_GATEWAY_SEND_TO_SITE ++#define MOCKED_LIFX_GATEWAY_ALLOCATE_TAG_ID ++#include "mock_gateway.h" ++#include "tests_utils.h" ++ ++#define MOCKED_CLIENT_SEND_ERROR ++#define MOCKED_ROUTER_TARGETS_TO_DEVICES ++#define MOCKED_ROUTER_SEND_TO_DEVICE ++#define MOCKED_ROUTER_DEVICE_LIST_FREE ++#include "tests_proto_utils.h" ++ ++#define FAKE_TARGET_LIST (void *)0x2a ++ ++static struct lgtd_router_device_list devices = ++ SLIST_HEAD_INITIALIZER(&devices); ++static struct lgtd_router_device_list device_1_only = ++ SLIST_HEAD_INITIALIZER(&device_1_only); ++ ++static bool client_send_error_called = false; ++ ++void ++lgtd_client_send_error(struct lgtd_client *client, ++ enum lgtd_client_error_code error, ++ const char *msg) ++{ ++ if (!client) { ++ errx(1, "client_send_error called without a client"); ++ } ++ ++ if (!error) { ++ errx(1, "client_send_error called without an error code"); ++ } ++ ++ if (!msg) { ++ errx(1, "client_send_error called without an error message"); ++ } ++ ++ client_send_error_called = true; ++} ++ ++static bool send_to_device_called = false; ++ ++void ++lgtd_router_send_to_device(struct lgtd_lifx_bulb *bulb, ++ enum lgtd_lifx_packet_type pkt_type, ++ void *pkt) ++{ ++ (void)bulb; ++ (void)pkt_type; ++ (void)pkt; ++ ++ send_to_device_called = true; ++} ++ ++static bool gateway_send_to_site_called = false; ++ ++bool ++lgtd_lifx_gateway_send_to_site(struct lgtd_lifx_gateway *gw, ++ enum lgtd_lifx_packet_type pkt_type, ++ const void *pkt) ++{ ++ (void)gw; ++ (void)pkt_type; ++ (void)pkt; ++ ++ gateway_send_to_site_called = true; ++ ++ return true; ++} ++ ++static bool gateway_allocate_tag_id_called = false; ++ ++int ++lgtd_lifx_gateway_allocate_tag_id(struct lgtd_lifx_gateway *gw, ++ int tag_id, ++ const char *tag_label) ++{ ++ if (gateway_allocate_tag_id_called) { ++ errx( ++ 1, "lgtd_lifx_gateway_allocate_tag_id " ++ "should have been called once only" ++ ); ++ } ++ ++ if (tag_id != -1) { ++ errx( ++ 1, "lgtd_lifx_gateway_allocate_tag_id " ++ "tag_id %d (expected -1)", tag_id ++ ); ++ } ++ ++ if (!gw) { ++ errx( ++ 1, "lgtd_lifx_gateway_allocate_tag_id " ++ "must be called with gateway" ++ ); ++ } ++ ++ if (!tag_label) { ++ errx( ++ 1, "lgtd_lifx_gateway_allocate_tag_id " ++ "must be called with a tag_label" ++ ); ++ } ++ ++ return -1; // no more tag id available ++} ++ ++static bool device_list_free_called = false; ++ ++void ++lgtd_router_device_list_free(struct lgtd_router_device_list *devices) ++{ ++ if (!devices) { ++ lgtd_errx(1, "the device list must be passed"); ++ } ++ ++ device_list_free_called = true; ++} ++ ++struct lgtd_router_device_list * ++lgtd_router_targets_to_devices(const struct lgtd_proto_target_list *targets) ++{ ++ if (targets != FAKE_TARGET_LIST) { ++ lgtd_errx(1, "unexpected targets list"); ++ } ++ ++ return &device_1_only; ++} ++ ++static void ++setup_devices(void) ++{ ++ static struct lgtd_lifx_gateway gw_bulb_1 = { ++ .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_1.bulbs) ++ }; ++ static struct lgtd_lifx_bulb bulb_1 = { ++ .addr = { 1, 2, 3, 4, 5 }, ++ .state = { ++ .hue = 0xaaaa, ++ .saturation = 0xffff, ++ .brightness = 0xbbbb, ++ .kelvin = 3600, ++ .label = "wave", ++ .power = LGTD_LIFX_POWER_ON, ++ .tags = 0 ++ }, ++ .gw = &gw_bulb_1 ++ }; ++ static struct lgtd_router_device device_1 = { .device = &bulb_1 }; ++ SLIST_INSERT_HEAD(&devices, &device_1, link); ++ SLIST_INSERT_HEAD(&device_1_only, &device_1, link); ++ ++ struct lgtd_lifx_tag *gw_2_tag_1 = lgtd_tests_insert_mock_tag("vapor"); ++ struct lgtd_lifx_tag *gw_2_tag_2 = lgtd_tests_insert_mock_tag("d^-^b"); ++ struct lgtd_lifx_tag *gw_2_tag_3 = lgtd_tests_insert_mock_tag("wave~"); ++ static struct lgtd_lifx_gateway gw_bulb_2 = { ++ .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_2.bulbs), ++ .tag_ids = 0x7 ++ }; ++ lgtd_tests_add_tag_to_gw(gw_2_tag_1, &gw_bulb_2, 0); ++ lgtd_tests_add_tag_to_gw(gw_2_tag_2, &gw_bulb_2, 1); ++ lgtd_tests_add_tag_to_gw(gw_2_tag_3, &gw_bulb_2, 2); ++ static struct lgtd_lifx_bulb bulb_2 = { ++ .addr = { 5, 4, 3, 2, 1 }, ++ .state = { ++ .hue = 0x0000, ++ .saturation = 0x0000, ++ .brightness = 0xffff, ++ .kelvin = 4000, ++ .label = "", ++ .power = LGTD_LIFX_POWER_OFF, ++ .tags = 0x3 ++ }, ++ .gw = &gw_bulb_2 ++ }; ++ static struct lgtd_router_device device_2 = { .device = &bulb_2 }; ++ SLIST_INSERT_HEAD(&devices, &device_2, link); ++} ++ ++int ++main(void) ++{ ++ struct lgtd_client client = { .io = FAKE_BUFFEREVENT }; ++ ++ setup_devices(); ++ ++ lgtd_proto_tag(&client, FAKE_TARGET_LIST, "dub"); ++ ++ ++ if (gateway_send_to_site_called) { ++ lgtd_errx(1, "SET_TAG_LABELS shouldn't have been sent"); ++ } ++ if (!device_list_free_called) { ++ lgtd_errx(1, "the list of devices hasn't been freed"); ++ } ++ if (send_to_device_called) { ++ lgtd_errx(1, "SET_TAGS shouldn't have been to any device"); ++ } ++ if (!client_send_error_called) { ++ lgtd_errx(1, "client_send_error should have been called"); ++ } ++ ++ return 0; ++} +diff --git a/tests/core/proto/test_proto_tag_update.c b/tests/core/proto/test_proto_tag_update.c +new file mode 100644 +--- /dev/null ++++ b/tests/core/proto/test_proto_tag_update.c +@@ -0,0 +1,283 @@ ++#include "proto.c" ++ ++#include "mock_client_buf.h" ++#include "mock_daemon.h" ++#define MOCKED_LIFX_GATEWAY_SEND_TO_SITE ++#define MOCKED_LIFX_GATEWAY_ALLOCATE_TAG_ID ++#include "mock_gateway.h" ++#include "tests_utils.h" ++ ++#define MOCKED_ROUTER_TARGETS_TO_DEVICES ++#define MOCKED_ROUTER_SEND_TO_DEVICE ++#define MOCKED_ROUTER_DEVICE_LIST_FREE ++#include "tests_proto_utils.h" ++ ++#define FAKE_TARGET_LIST (void *)0x2a ++ ++static struct lgtd_router_device_list devices = ++ SLIST_HEAD_INITIALIZER(&devices); ++ ++static bool send_to_device_called = false; ++ ++void ++lgtd_router_send_to_device(struct lgtd_lifx_bulb *bulb, ++ enum lgtd_lifx_packet_type pkt_type, ++ void *pkt) ++{ ++ if (send_to_device_called) { ++ errx(1, "lgtd_router_send_to_device should have been called once only"); ++ } ++ ++ if (!bulb) { ++ errx(1, "lgtd_router_send_to_device must be called with a bulb"); ++ } ++ ++ uint8_t expected_addr[LGTD_LIFX_ADDR_LENGTH] = { 5, 4, 3, 2, 1 }; ++ if (memcmp(bulb->addr, expected_addr, LGTD_LIFX_ADDR_LENGTH)) { ++ errx( ++ 1, "got bulb with addr %s (expected %s)", ++ lgtd_addrtoa(bulb->addr), lgtd_addrtoa(expected_addr) ++ ); ++ } ++ ++ if (pkt_type != LGTD_LIFX_SET_TAGS) { ++ errx( ++ 1, "got packet type %d (expected %d)", pkt_type, LGTD_LIFX_SET_TAGS ++ ); ++ } ++ ++ if (!pkt) { ++ errx(1, "missing SET_TAGS payload"); ++ } ++ ++ const struct lgtd_lifx_packet_tags *pkt_tags = pkt; ++ uint64_t tags = le64toh(pkt_tags->tags); ++ ++ if (tags != 0x7) { ++ errx( ++ 1, "invalid SET_TAGS payload=%#jx (expected %#x)", ++ (uintmax_t)tags, 0x7 ++ ); ++ } ++ ++ send_to_device_called = true; ++} ++ ++static bool gateway_send_to_site_called_for_gw_1 = false; ++static bool gateway_send_to_site_called_for_gw_2 = false; ++ ++bool ++lgtd_lifx_gateway_send_to_site(struct lgtd_lifx_gateway *gw, ++ enum lgtd_lifx_packet_type pkt_type, ++ const void *pkt) ++{ ++ if (!gw) { ++ errx(1, "missing gateway"); ++ } ++ ++ if (pkt_type != LGTD_LIFX_SET_TAG_LABELS) { ++ errx( ++ 1, "got packet type %#x (expected %#x)", ++ pkt_type, LGTD_LIFX_SET_TAG_LABELS ++ ); ++ } ++ ++ const struct lgtd_lifx_packet_tag_labels *pkt_tag_labels = pkt; ++ uint64_t tags = le64toh(pkt_tag_labels->tags); ++ ++ if (strcmp(pkt_tag_labels->label, "dub")) { ++ errx(1, "got label %s (expected dub)", pkt_tag_labels->label); ++ } ++ ++ if (gw->site.as_integer == 42) { ++ if (tags != 0x1) { ++ errx(1, "got tags %#jx (expected %#x)", (uintmax_t)tags, 0x1); ++ } ++ if (gateway_send_to_site_called_for_gw_1) { ++ errx(1, "LGTD_LIFX_SET_TAG_LABELS already called for gw 1"); ++ } ++ gateway_send_to_site_called_for_gw_1 = true; ++ } else if (gw->site.as_integer == 44) { ++ if (tags != 0x4) { ++ errx(1, "got tags %#jx (expected %#x)", (uintmax_t)tags, 0x4); ++ } ++ if (gateway_send_to_site_called_for_gw_2) { ++ errx(1, "LGTD_LIFX_SET_TAG_LABELS already called for gw 2"); ++ } ++ gateway_send_to_site_called_for_gw_2 = true; ++ } else { ++ errx(1, "LGTD_LIFX_SET_TAG_LABELS received an invalid gateway"); ++ } ++ ++ return true; ++} ++ ++static bool gateway_allocate_tag_id_called = false; ++ ++int ++lgtd_lifx_gateway_allocate_tag_id(struct lgtd_lifx_gateway *gw, ++ int tag_id, ++ const char *tag_label) ++{ ++ if (gateway_allocate_tag_id_called) { ++ errx( ++ 1, "lgtd_lifx_gateway_allocate_tag_id " ++ "should have been called once only" ++ ); ++ } ++ ++ if (tag_id != -1) { ++ errx( ++ 1, "lgtd_lifx_gateway_allocate_tag_id " ++ "tag_id %d (expected -1)", tag_id ++ ); ++ } ++ ++ if (!gw) { ++ errx( ++ 1, "lgtd_lifx_gateway_allocate_tag_id " ++ "must be called with gateway" ++ ); ++ } ++ ++ if (!tag_label) { ++ errx( ++ 1, "lgtd_lifx_gateway_allocate_tag_id " ++ "must be called with a tag_label" ++ ); ++ } ++ ++ if (gw->site.as_integer != 44) { ++ errx( ++ 1, "lgtd_lifx_gateway_allocate_tag_id got the wrong gateway " ++ "%#jx (expected %d)", (uintmax_t)gw->site.as_integer, 44 ++ ); ++ } ++ ++ tag_id = 2; ++ ++ struct lgtd_lifx_tag *tag = lgtd_lifx_tagging_find_tag(tag_label); ++ if (!tag) { ++ errx(1, "tag %s wasn't found", tag_label); ++ } ++ lgtd_tests_add_tag_to_gw(tag, gw, tag_id); ++ ++ gateway_allocate_tag_id_called = true; ++ ++ return tag_id; ++} ++ ++static bool device_list_free_called = false; ++ ++void ++lgtd_router_device_list_free(struct lgtd_router_device_list *devices) ++{ ++ if (!devices) { ++ lgtd_errx(1, "the device list must be passed"); ++ } ++ ++ device_list_free_called = true; ++} ++ ++struct lgtd_router_device_list * ++lgtd_router_targets_to_devices(const struct lgtd_proto_target_list *targets) ++{ ++ if (targets != FAKE_TARGET_LIST) { ++ lgtd_errx(1, "unexpected targets list"); ++ } ++ ++ return &devices; ++} ++ ++static void ++setup_devices(void) ++{ ++ static struct lgtd_lifx_gateway gw_bulb_1 = { ++ .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_1.bulbs), ++ .site = { .as_integer = 42 } ++ }; ++ static struct lgtd_lifx_bulb bulb_1 = { ++ .addr = { 1, 2, 3, 4, 5 }, ++ .state = { ++ .hue = 0xaaaa, ++ .saturation = 0xffff, ++ .brightness = 0xbbbb, ++ .kelvin = 3600, ++ .label = "wave", ++ .power = LGTD_LIFX_POWER_ON, ++ .tags = 1 ++ }, ++ .gw = &gw_bulb_1 ++ }; ++ static struct lgtd_router_device device_1 = { .device = &bulb_1 }; ++ SLIST_INSERT_HEAD(&devices, &device_1, link); ++ struct lgtd_lifx_tag *gw_1_tag_1 = lgtd_tests_insert_mock_tag("dub"); ++ lgtd_tests_add_tag_to_gw(gw_1_tag_1, &gw_bulb_1, 0); ++ ++ struct lgtd_lifx_tag *gw_2_tag_1 = lgtd_tests_insert_mock_tag("vapor"); ++ struct lgtd_lifx_tag *gw_2_tag_2 = lgtd_tests_insert_mock_tag("d^-^b"); ++ static struct lgtd_lifx_gateway gw_bulb_2 = { ++ .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_2.bulbs), ++ .site = { .as_integer = 44 }, ++ .tag_ids = 0x3 ++ }; ++ lgtd_tests_add_tag_to_gw(gw_2_tag_1, &gw_bulb_2, 0); ++ lgtd_tests_add_tag_to_gw(gw_2_tag_2, &gw_bulb_2, 1); ++ static struct lgtd_lifx_bulb bulb_2 = { ++ .addr = { 5, 4, 3, 2, 1 }, ++ .state = { ++ .hue = 0x0000, ++ .saturation = 0x0000, ++ .brightness = 0xffff, ++ .kelvin = 4000, ++ .label = "", ++ .power = LGTD_LIFX_POWER_OFF, ++ .tags = 0x3 ++ }, ++ .gw = &gw_bulb_2 ++ }; ++ static struct lgtd_router_device device_2 = { .device = &bulb_2 }; ++ SLIST_INSERT_HEAD(&devices, &device_2, link); ++} ++ ++int ++main(void) ++{ ++ struct lgtd_client client = { .io = FAKE_BUFFEREVENT }; ++ ++ setup_devices(); ++ ++ lgtd_proto_tag(&client, FAKE_TARGET_LIST, "dub"); ++ ++ const char expected[] = "true"; ++ if (client_write_buf_idx != sizeof(expected) - 1) { ++ lgtd_errx( ++ 1, ++ "%d bytes written, expected %lu " ++ "(got %.*s instead of %s)", ++ client_write_buf_idx, sizeof(expected) - 1UL, ++ client_write_buf_idx, client_write_buf, expected ++ ); ++ } ++ if (memcmp(expected, client_write_buf, sizeof(expected) - 1)) { ++ lgtd_errx( ++ 1, "got %.*s instead of %s", ++ client_write_buf_idx, client_write_buf, expected ++ ); ++ } ++ ++ if (!gateway_send_to_site_called_for_gw_1) { ++ lgtd_errx(1, "SET_TAG_LABELS wasn't sent to gw 1"); ++ } ++ if (!gateway_send_to_site_called_for_gw_2) { ++ lgtd_errx(1, "SET_TAG_LABELS wasn't sent to gw 2"); ++ } ++ if (!device_list_free_called) { ++ lgtd_errx(1, "the list of devices hasn't been freed"); ++ } ++ if (!send_to_device_called) { ++ lgtd_errx(1, "SET_TAGS wasn't send to any device"); ++ } ++ ++ return 0; ++} +diff --git a/tests/core/proto/test_proto_untag.c b/tests/core/proto/test_proto_untag.c +new file mode 100644 +--- /dev/null ++++ b/tests/core/proto/test_proto_untag.c +@@ -0,0 +1,170 @@ ++#include "proto.c" ++ ++#include "mock_client_buf.h" ++#include "mock_daemon.h" ++#include "mock_gateway.h" ++#include "tests_utils.h" ++ ++#define MOCKED_ROUTER_TARGETS_TO_DEVICES ++#define MOCKED_ROUTER_SEND_TO_DEVICE ++#define MOCKED_ROUTER_DEVICE_LIST_FREE ++#include "tests_proto_utils.h" ++ ++static bool device_list_free_called = false; ++ ++void ++lgtd_router_device_list_free(struct lgtd_router_device_list *devices) ++{ ++ if (device_list_free_called) { ++ errx(1, "the device list should have been freed once"); ++ } ++ ++ if (!devices) { ++ errx(1, "the device list must be passed"); ++ } ++ ++ device_list_free_called = true; ++} ++ ++static struct lgtd_lifx_tag *tag_vapor = NULL; ++ ++struct lgtd_router_device_list * ++lgtd_router_targets_to_devices(const struct lgtd_proto_target_list *targets) ++{ ++ if (targets != (void *)0x2a) { ++ lgtd_errx(1, "unexpected targets list"); ++ } ++ ++ static struct lgtd_router_device_list devices = ++ SLIST_HEAD_INITIALIZER(&devices); ++ ++ static struct lgtd_lifx_gateway gw_bulb_1 = { ++ .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_1.bulbs) ++ }; ++ static struct lgtd_lifx_bulb bulb_1 = { ++ .addr = { 1, 2, 3, 4, 5 }, ++ .state = { ++ .hue = 0xaaaa, ++ .saturation = 0xffff, ++ .brightness = 0xbbbb, ++ .kelvin = 3600, ++ .label = "wave", ++ .power = LGTD_LIFX_POWER_ON, ++ .tags = 0 ++ }, ++ .gw = &gw_bulb_1 ++ }; ++ static struct lgtd_router_device device_1 = { .device = &bulb_1 }; ++ SLIST_INSERT_HEAD(&devices, &device_1, link); ++ ++ struct lgtd_lifx_tag *gw_2_tag_2 = lgtd_tests_insert_mock_tag("d^-^b"); ++ struct lgtd_lifx_tag *gw_2_tag_3 = lgtd_tests_insert_mock_tag("wave~"); ++ static struct lgtd_lifx_gateway gw_bulb_2 = { ++ .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_2.bulbs), ++ .tag_ids = 0x7 ++ }; ++ lgtd_tests_add_tag_to_gw(tag_vapor, &gw_bulb_2, 0); ++ lgtd_tests_add_tag_to_gw(gw_2_tag_2, &gw_bulb_2, 1); ++ lgtd_tests_add_tag_to_gw(gw_2_tag_3, &gw_bulb_2, 2); ++ static struct lgtd_lifx_bulb bulb_2 = { ++ .addr = { 5, 4, 3, 2, 1 }, ++ .state = { ++ .hue = 0x0000, ++ .saturation = 0x0000, ++ .brightness = 0xffff, ++ .kelvin = 4000, ++ .label = "", ++ .power = LGTD_LIFX_POWER_OFF, ++ .tags = 0x3 ++ }, ++ .gw = &gw_bulb_2 ++ }; ++ static struct lgtd_router_device device_2 = { .device = &bulb_2 }; ++ SLIST_INSERT_HEAD(&devices, &device_2, link); ++ ++ return &devices; ++} ++ ++static bool send_to_device_called = false; ++ ++void ++lgtd_router_send_to_device(struct lgtd_lifx_bulb *bulb, ++ enum lgtd_lifx_packet_type pkt_type, ++ void *pkt) ++{ ++ if (send_to_device_called) { ++ errx(1, "lgtd_router_send_to_device should have been called once"); ++ } ++ ++ if (!bulb) { ++ errx(1, "lgtd_router_send_to_device must be called with a bulb"); ++ } ++ ++ uint8_t expected_addr[LGTD_LIFX_ADDR_LENGTH] = { 5, 4, 3, 2, 1 }; ++ if (memcmp(bulb->addr, expected_addr, LGTD_LIFX_ADDR_LENGTH)) { ++ errx( ++ 1, "got bulb with addr %s (expected %s)", ++ lgtd_addrtoa(bulb->addr), lgtd_addrtoa(expected_addr) ++ ); ++ } ++ ++ if (pkt_type != LGTD_LIFX_SET_TAGS) { ++ errx( ++ 1, "got packet type %d (expected %d)", pkt_type, LGTD_LIFX_SET_TAGS ++ ); ++ } ++ ++ if (!pkt) { ++ errx(1, "missing SET_TAGS payload"); ++ } ++ ++ struct lgtd_lifx_packet_tags *pkt_tags = pkt; ++ if (pkt_tags->tags != 0x2) { ++ errx( ++ 1, "invalid SET_TAGS payload=%#jx (expected %#x)", ++ (uintmax_t)pkt_tags->tags, 0x2 ++ ); ++ } ++ ++ send_to_device_called = true; ++} ++ ++int ++main(void) ++{ ++ struct lgtd_client client = { .io = FAKE_BUFFEREVENT }; ++ ++ struct lgtd_proto_target_list *targets = (void *)0x2a; ++ ++ tag_vapor = lgtd_tests_insert_mock_tag("vapor"); ++ ++ lgtd_proto_untag(&client, targets, "vapor"); ++ ++ const char expected[] = "true"; ++ ++ if (client_write_buf_idx != sizeof(expected) - 1) { ++ lgtd_errx( ++ 1, ++ "%d bytes written, expected %lu " ++ "(got %.*s instead of %s)", ++ client_write_buf_idx, sizeof(expected) - 1UL, ++ client_write_buf_idx, client_write_buf, expected ++ ); ++ } ++ ++ if (memcmp(expected, client_write_buf, sizeof(expected) - 1)) { ++ lgtd_errx( ++ 1, "got %.*s instead of %s", ++ client_write_buf_idx, client_write_buf, expected ++ ); ++ } ++ ++ if (!device_list_free_called) { ++ lgtd_errx(1, "the list of devices hasn't been freed"); ++ } ++ if (!send_to_device_called) { ++ lgtd_errx(1, "nothing was send to any device"); ++ } ++ ++ return 0; ++} +diff --git a/tests/core/proto/test_proto_untag_tag_does_not_exist.c b/tests/core/proto/test_proto_untag_tag_does_not_exist.c +new file mode 100644 +--- /dev/null ++++ b/tests/core/proto/test_proto_untag_tag_does_not_exist.c +@@ -0,0 +1,90 @@ ++#include "proto.c" ++ ++#include "mock_client_buf.h" ++#include "mock_daemon.h" ++#include "mock_gateway.h" ++#include "tests_utils.h" ++ ++#define MOCKED_ROUTER_TARGETS_TO_DEVICES ++#define MOCKED_ROUTER_SEND_TO_DEVICE ++#define MOCKED_ROUTER_DEVICE_LIST_FREE ++#include "tests_proto_utils.h" ++ ++static bool device_list_free_called = false; ++ ++void ++lgtd_router_device_list_free(struct lgtd_router_device_list *devices) ++{ ++ (void)devices; ++ ++ device_list_free_called = true; ++} ++ ++static bool targets_to_devices_called = false; ++ ++struct lgtd_router_device_list * ++lgtd_router_targets_to_devices(const struct lgtd_proto_target_list *targets) ++{ ++ (void)targets; ++ ++ targets_to_devices_called = true; ++ ++ static struct lgtd_router_device_list devices = ++ SLIST_HEAD_INITIALIZER(&devices); ++ ++ return &devices; ++} ++ ++static bool send_to_device_called = false; ++ ++void ++lgtd_router_send_to_device(struct lgtd_lifx_bulb *bulb, ++ enum lgtd_lifx_packet_type pkt_type, ++ void *pkt) ++{ ++ (void)bulb; ++ (void)pkt_type; ++ (void)pkt; ++ send_to_device_called = true; ++} ++ ++int ++main(void) ++{ ++ struct lgtd_client client = { .io = FAKE_BUFFEREVENT }; ++ ++ struct lgtd_proto_target_list *targets; ++ targets = lgtd_tests_build_target_list("*", NULL); ++ ++ lgtd_proto_untag(&client, targets, "vapor"); ++ ++ const char expected[] = "true"; ++ ++ if (client_write_buf_idx != sizeof(expected) - 1) { ++ lgtd_errx( ++ 1, ++ "%d bytes written, expected %lu " ++ "(got %.*s instead of %s)", ++ client_write_buf_idx, sizeof(expected) - 1UL, ++ client_write_buf_idx, client_write_buf, expected ++ ); ++ } ++ if (memcmp(expected, client_write_buf, sizeof(expected) - 1)) { ++ lgtd_errx( ++ 1, "got %.*s instead of %s", ++ client_write_buf_idx, client_write_buf, expected ++ ); ++ } ++ ++ if (targets_to_devices_called) { ++ lgtd_errx(1, "unexpected call to targets_to_devices"); ++ } ++ if (device_list_free_called) { ++ lgtd_errx(1, "nothing should have been freed"); ++ } ++ if (send_to_device_called) { ++ lgtd_errx(1, "nothing should have been sent to any device"); ++ } ++ ++ return 0; ++} +diff --git a/tests/core/proto/tests_proto_utils.h b/tests/core/proto/tests_proto_utils.h +--- a/tests/core/proto/tests_proto_utils.h ++++ b/tests/core/proto/tests_proto_utils.h +@@ -34,6 +34,18 @@ + } + #endif + ++#ifndef MOCKED_ROUTER_SEND_TO_DEVICE ++void ++lgtd_router_send_to_device(struct lgtd_lifx_bulb *bulb, ++ enum lgtd_lifx_packet_type pkt_type, ++ void *pkt) ++{ ++ (void)bulb; ++ (void)pkt_type; ++ (void)pkt; ++} ++#endif ++ + #ifndef MOCKED_ROUTER_SEND + bool + lgtd_router_send(const struct lgtd_proto_target_list *targets, +diff --git a/tests/core/router/CMakeLists.txt b/tests/core/router/CMakeLists.txt +--- a/tests/core/router/CMakeLists.txt ++++ b/tests/core/router/CMakeLists.txt +@@ -9,6 +9,7 @@ + ${LIGHTSD_SOURCE_DIR}/core/proto.c + ${LIGHTSD_SOURCE_DIR}/core/stats.c + ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c ++ ${LIGHTSD_SOURCE_DIR}/lifx/tagging.c + ${LIGHTSD_SOURCE_DIR}/lifx/timer.c + ${LIGHTSD_SOURCE_DIR}/lifx/wire_proto.c + ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c +diff --git a/tests/core/router/tests_router_utils.h b/tests/core/router/tests_router_utils.h +--- a/tests/core/router/tests_router_utils.h ++++ b/tests/core/router/tests_router_utils.h +@@ -1,5 +1,7 @@ + #pragma once + ++#include "mock_gateway.h" ++ + int lgtd_tests_gw_pkt_queue_size = 0; + struct { + struct lgtd_lifx_gateway *gw; +diff --git a/tests/core/tests_shims.c b/tests/core/tests_shims.c +--- a/tests/core/tests_shims.c ++++ b/tests/core/tests_shims.c +@@ -30,52 +30,3 @@ + lgtd_cleanup(void) + { + } +- +- +-void lgtd_lifx_gateway_handle_pan_gateway(struct lgtd_lifx_gateway *gw, +- const struct lgtd_lifx_packet_header *hdr, +- const struct lgtd_lifx_packet_pan_gateway *pkt) +-{ +- (void)gw; +- (void)hdr; +- (void)pkt; +-} +- +-void lgtd_lifx_gateway_handle_light_status(struct lgtd_lifx_gateway *gw, +- const struct lgtd_lifx_packet_header *hdr, +- const struct lgtd_lifx_packet_light_status *pkt) +-{ +- (void)gw; +- (void)hdr; +- (void)pkt; +-} +- +-void lgtd_lifx_gateway_handle_power_state(struct lgtd_lifx_gateway *gw, +- const struct lgtd_lifx_packet_header *hdr, +- const struct lgtd_lifx_packet_power_state *pkt) +-{ +- (void)gw; +- (void)hdr; +- (void)pkt; +-} +- +-void lgtd_lifx_gateway_handle_tag_labels(struct lgtd_lifx_gateway *gw, +- const struct lgtd_lifx_packet_header *hdr, +- const struct lgtd_lifx_packet_tag_labels *pkt) +-{ +- (void)gw; +- (void)hdr; +- (void)pkt; +-} +- +-struct lgtd_lifx_tag * +-lgtd_lifx_tagging_find_tag(const char *tag_label) +-{ +- struct lgtd_lifx_tag *tag = NULL; +- LIST_FOREACH(tag, &lgtd_lifx_tags, link) { +- if (!strcmp(tag->label, tag_label)) { +- break; +- } +- } +- return tag; +-} +diff --git a/tests/core/tests_shims.h b/tests/core/tests_shims.h +new file mode 100644 +--- /dev/null ++++ b/tests/core/tests_shims.h +@@ -0,0 +1,23 @@ ++#pragma once ++ ++struct lgtd_opts lgtd_opts = { ++ .foreground = false, ++ .log_timestamps = false, ++ .verbosity = LGTD_DEBUG ++}; ++ ++struct event_base *lgtd_ev_base = NULL; ++ ++const char *lgtd_binds = NULL; ++ ++void ++lgtd_cleanup(void) ++{ ++} ++ ++#ifndef MOCKED_DAEMON_UPDATE_PROCTITLE ++void ++lgtd_daemon_update_proctitle(void) ++{ ++} ++#endif +diff --git a/tests/core/tests_utils.c b/tests/core/tests_utils.c +--- a/tests/core/tests_utils.c ++++ b/tests/core/tests_utils.c +@@ -118,8 +118,11 @@ + struct lgtd_lifx_site *site = calloc(1, sizeof(*site)); + site->gw = gw; + site->tag_id = tag_id; ++ LIST_INSERT_HEAD(&tag->sites, site, link); ++ + gw->tags[tag_id] = tag; +- LIST_INSERT_HEAD(&tag->sites, site, link); ++ gw->tag_ids |= LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id); ++ + return site; + } + +diff --git a/tests/lifx/bulb/CMakeLists.txt b/tests/lifx/bulb/CMakeLists.txt +new file mode 100644 +--- /dev/null ++++ b/tests/lifx/bulb/CMakeLists.txt +@@ -0,0 +1,29 @@ ++INCLUDE_DIRECTORIES( ++ ${CMAKE_CURRENT_SOURCE_DIR} ++ ${CMAKE_CURRENT_BINARY_DIR} ++) ++ ++ADD_CORE_LIBRARY( ++ test_lifx_bulb_core STATIC ++ ${LIGHTSD_SOURCE_DIR}/core/log.c ++ ${LIGHTSD_SOURCE_DIR}/core/router.c ++ ${LIGHTSD_SOURCE_DIR}/core/stats.c ++ ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c ++) ++ ++ADD_LIBRARY( ++ test_lifx_bulb STATIC ++ ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c ++ ${LIGHTSD_SOURCE_DIR}/lifx/wire_proto.c ++) ++ ++FUNCTION(ADD_BULB_TEST TEST_SOURCE) ++ ADD_TEST_FROM_C_SOURCES( ++ ${TEST_SOURCE} test_lifx_bulb_core test_lifx_bulb ++ ) ++ENDFUNCTION() ++ ++FILE(GLOB TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test_*.c") ++FOREACH(TEST ${TESTS}) ++ ADD_BULB_TEST(${TEST}) ++ENDFOREACH() +diff --git a/tests/lifx/bulb/test_bulb_close.c b/tests/lifx/bulb/test_bulb_close.c +new file mode 100644 +--- /dev/null ++++ b/tests/lifx/bulb/test_bulb_close.c +@@ -0,0 +1,33 @@ ++#include "bulb.c" ++ ++#include "mock_gateway.h" ++ ++int ++main(void) ++{ ++ struct lgtd_lifx_gateway gw; ++ uint8_t bulb_addr[LGTD_LIFX_ADDR_LENGTH] = { 5, 4, 3, 2, 1, 0 }; ++ struct lgtd_lifx_bulb *bulb = lgtd_lifx_bulb_open(&gw, bulb_addr); ++ ++ bulb->state.power = LGTD_LIFX_POWER_ON; ++ LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(bulbs_powered_on, 1); ++ ++ lgtd_lifx_bulb_close(bulb); ++ ++ if (!RB_EMPTY(&lgtd_lifx_bulbs_table)) { ++ errx(1, "The bulbs table should be empty!"); ++ } ++ ++ if (LGTD_STATS_GET(bulbs) != 0) { ++ errx(1, "The bulbs counter is %d (expected 0)", LGTD_STATS_GET(bulbs)); ++ } ++ ++ if (LGTD_STATS_GET(bulbs_powered_on) != 0) { ++ errx( ++ 1, "The powered on bulbs counter is %d (expected 0)", ++ LGTD_STATS_GET(bulbs_powered_on) ++ ); ++ } ++ ++ return 0; ++} +diff --git a/tests/lifx/bulb/test_bulb_open.c b/tests/lifx/bulb/test_bulb_open.c +new file mode 100644 +--- /dev/null ++++ b/tests/lifx/bulb/test_bulb_open.c +@@ -0,0 +1,44 @@ ++#include "bulb.c" ++ ++#include "mock_gateway.h" ++ ++int ++main(void) ++{ ++ struct lgtd_lifx_gateway gw; ++ uint8_t bulb_addr[LGTD_LIFX_ADDR_LENGTH] = { 5, 4, 3, 2, 1, 0 }; ++ lgtd_time_mono_t now = lgtd_time_monotonic_msecs(); ++ struct lgtd_lifx_bulb *bulb = lgtd_lifx_bulb_open(&gw, bulb_addr); ++ ++ if (!bulb) { ++ errx(1, "lgtd_lifx_bulb_open didn't return any bulb"); ++ } ++ ++ if (memcmp(bulb->addr, bulb_addr, LGTD_LIFX_ADDR_LENGTH)) { ++ errx( ++ 1, "got bulb addr %s (expected %s)", ++ lgtd_addrtoa(bulb->addr), lgtd_addrtoa(bulb_addr) ++ ); ++ } ++ ++ if (bulb->gw != &gw) { ++ errx(1, "got bulb gateway %p (expected %p)", bulb->gw, &gw); ++ } ++ ++ if (lgtd_lifx_bulb_get(bulb_addr) != bulb) { ++ errx(1, "the new bulb can't be found"); ++ } ++ ++ if (bulb->last_light_state_at < now) { ++ errx( ++ 1, "got bulb->last_light_state_at %ju (expected >= %ju)", ++ bulb->last_light_state_at, (uintmax_t)now ++ ); ++ } ++ ++ if (LGTD_STATS_GET(bulbs) != 1) { ++ errx(1, "bulbs counter is %d (expected 1)", LGTD_STATS_GET(bulbs)); ++ } ++ ++ return 0; ++} +diff --git a/tests/lifx/bulb/test_bulb_set_light_state.c b/tests/lifx/bulb/test_bulb_set_light_state.c +new file mode 100644 +--- /dev/null ++++ b/tests/lifx/bulb/test_bulb_set_light_state.c +@@ -0,0 +1,92 @@ ++#include "bulb.c" ++ ++#define MOCKED_LGTD_LIFX_GATEWAY_UPDATE_TAG_REFCOUNTS ++#include "mock_gateway.h" ++ ++static int update_tag_refcouts_call_counts = 0; ++ ++void ++lgtd_lifx_gateway_update_tag_refcounts(struct lgtd_lifx_gateway *gw, ++ uint64_t bulb_tags, ++ uint64_t pkt_tags) ++{ ++ if (gw != (void *)0xdeaf) { ++ errx(1, "got wrong gw %p (expected 0xdeaf)", gw); ++ } ++ ++ if (pkt_tags != 0xfeed) { ++ errx(1, "got pkt_tags %#jx (expected 0xfeed)", (uintmax_t)pkt_tags); ++ } ++ ++ if (!update_tag_refcouts_call_counts) { ++ if (bulb_tags != 0x2a) { ++ errx(1, "got bulb_tags %#jx (expected 0x2a)", (uintmax_t)bulb_tags); ++ } ++ } else { ++ if (bulb_tags != 0xfeed) { ++ errx(1, "got bulb_tags %#jx (expected 0xfeed)", (uintmax_t)bulb_tags); ++ } ++ } ++ ++ update_tag_refcouts_call_counts++; ++} ++ ++int ++main(void) ++{ ++ struct lgtd_lifx_bulb bulb = { ++ .state = { ++ .hue = 54321, ++ .brightness = UINT16_MAX, ++ .kelvin = 12345, ++ .dim = 808, ++ .power = LGTD_LIFX_POWER_OFF, ++ .label = "lair", ++ .tags = 0x2a ++ }, ++ .gw = (void *)0xdeaf ++ }; ++ ++ struct lgtd_lifx_light_state new_state = { ++ .hue = 22222, ++ .brightness = UINT16_MAX / 2, ++ .kelvin = 54321, ++ .dim = 303, ++ .power = LGTD_LIFX_POWER_ON, ++ .label = "caverne", ++ .tags = 0xfeed ++ }; ++ ++ lgtd_lifx_bulb_set_light_state(&bulb, &new_state, 2015); ++ if (memcmp(&bulb.state, &new_state, sizeof(new_state))) { ++ errx(1, "new light state incorrectly set"); ++ } ++ if (LGTD_STATS_GET(bulbs_powered_on) != 1) { ++ errx( ++ 1, "unexpected bulbs_powered_on counter value %d (expected 1)", ++ LGTD_STATS_GET(bulbs_powered_on) ++ ); ++ } ++ if (bulb.last_light_state_at != 2015) { ++ errx( ++ 1, "got bulb.last_light_state = %jx (expected 2015)", ++ (uintmax_t)bulb.last_light_state_at ++ ); ++ } ++ if (update_tag_refcouts_call_counts != 1) { ++ errx(1, "lgtd_lifx_gateway_update_tag_refcounts wasn't called"); ++ } ++ ++ lgtd_lifx_bulb_set_light_state(&bulb, &new_state, 2015); ++ if (update_tag_refcouts_call_counts != 2) { ++ errx(1, "lgtd_lifx_gateway_update_tag_refcounts wasn't called"); ++ } ++ if (LGTD_STATS_GET(bulbs_powered_on) != 1) { ++ errx( ++ 1, "unexpected bulbs_powered_on counter value %d (expected 1)", ++ LGTD_STATS_GET(bulbs_powered_on) ++ ); ++ } ++ ++ return 0; ++} +diff --git a/tests/lifx/bulb/test_bulb_set_power_state.c b/tests/lifx/bulb/test_bulb_set_power_state.c +new file mode 100644 +--- /dev/null ++++ b/tests/lifx/bulb/test_bulb_set_power_state.c +@@ -0,0 +1,39 @@ ++#include "bulb.c" ++ ++#include "mock_gateway.h" ++ ++int ++main(void) ++{ ++ struct lgtd_lifx_bulb bulb = { ++ .state = { ++ .hue = 54321, ++ .brightness = UINT16_MAX, ++ .kelvin = 12345, ++ .dim = 808, ++ .power = LGTD_LIFX_POWER_OFF, ++ .label = "lair", ++ .tags = 0x2a ++ }, ++ .gw = (void *)0xdeaf ++ }; ++ struct lgtd_lifx_light_state new_state; ++ memcpy(&new_state, &bulb.state, sizeof(new_state)); ++ new_state.power = LGTD_LIFX_POWER_ON; ++ ++ ++ for (int i = 0; i != 2; i++) { ++ lgtd_lifx_bulb_set_power_state(&bulb, LGTD_LIFX_POWER_ON); ++ if (memcmp(&bulb.state, &new_state, sizeof(new_state))) { ++ errx(1, "new light state incorrectly set"); ++ } ++ if (LGTD_STATS_GET(bulbs_powered_on) != 1) { ++ errx( ++ 1, "unexpected bulbs_powered_on counter value %d (expected 1)", ++ LGTD_STATS_GET(bulbs_powered_on) ++ ); ++ } ++ } ++ ++ return 0; ++} +diff --git a/tests/lifx/bulb/test_bulb_set_tags.c b/tests/lifx/bulb/test_bulb_set_tags.c +new file mode 100644 +--- /dev/null ++++ b/tests/lifx/bulb/test_bulb_set_tags.c +@@ -0,0 +1,50 @@ ++#include "bulb.c" ++ ++#define MOCKED_LGTD_LIFX_GATEWAY_UPDATE_TAG_REFCOUNTS ++#include "mock_gateway.h" ++ ++static bool update_tag_refcouts_called = false; ++ ++void ++lgtd_lifx_gateway_update_tag_refcounts(struct lgtd_lifx_gateway *gw, ++ uint64_t bulb_tags, ++ uint64_t pkt_tags) ++{ ++ if (gw != (void *)0xdeaf) { ++ errx(1, "got wrong gw %p (expected 0xdeaf)", gw); ++ } ++ ++ if (bulb_tags != 0x2a) { ++ errx(1, "got bulb_tags %#jx (expected 0x2a)", (uintmax_t)bulb_tags); ++ } ++ ++ if (pkt_tags != 0xfeed) { ++ errx(1, "got pkt_tags %#jx (expected 0xfeed)", (uintmax_t)pkt_tags); ++ } ++ ++ update_tag_refcouts_called = true; ++} ++ ++int ++main(void) ++{ ++ struct lgtd_lifx_bulb bulb = { ++ .state = { .tags = 0x2a }, ++ .gw = (void *)0xdeaf ++ }; ++ ++ lgtd_lifx_bulb_set_tags(&bulb, 0xfeed); ++ ++ if (bulb.state.tags != 0xfeed) { ++ errx( ++ 1, "got bulb.state.tags = %#jx (expected 0xfeed)", ++ (uintmax_t)bulb.state.tags ++ ); ++ } ++ ++ if (!update_tag_refcouts_called) { ++ errx(1, "lgtd_lifx_gateway_update_tag_refcounts wasn't called"); ++ } ++ ++ return 0; ++} +diff --git a/tests/lifx/gateway/test_gateway_allocate_tag_id.c b/tests/lifx/gateway/test_gateway_allocate_tag_id.c +new file mode 100644 +--- /dev/null ++++ b/tests/lifx/gateway/test_gateway_allocate_tag_id.c +@@ -0,0 +1,102 @@ ++#include "gateway.c" ++ ++#include <string.h> ++ ++#define MOCKED_LIFX_TAGGING_INCREF ++#include "test_gateway_utils.h" ++ ++static bool tagging_incref_called = false; ++ ++struct lgtd_lifx_tag * ++lgtd_lifx_tagging_incref(const char *label, ++ struct lgtd_lifx_gateway *gw, ++ int tag_id) ++{ ++ if (!label) { ++ errx(1, "missing tag label"); ++ } ++ if (!gw) { ++ errx(1, "missing gateway"); ++ } ++ if (tag_id > 2) { ++ errx(1, "got tag_id %d but expected < 3", tag_id); ++ } ++ ++ struct lgtd_lifx_tag *tag = lgtd_lifx_tagging_find_tag(label); ++ if (!tag) { ++ tag = calloc(1, sizeof(*tag)); ++ strcpy(tag->label, label); ++ struct lgtd_lifx_site *site = calloc(1, sizeof(*site)); ++ site->gw = gw; ++ site->tag_id = tag_id; ++ LIST_INSERT_HEAD(&tag->sites, site, link); ++ } ++ ++ tagging_incref_called = true; ++ ++ return tag; ++} ++ ++int ++main(void) ++{ ++ lgtd_lifx_wire_load_packet_infos_map(); ++ ++ struct lgtd_lifx_gateway gw; ++ memset(&gw, 0, sizeof(gw)); ++ ++ struct lgtd_lifx_packet_header hdr; ++ memset(&hdr, 0, sizeof(hdr)); ++ ++ uint64_t expected_tag_ids = LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(0); ++ ++ lgtd_lifx_gateway_allocate_tag_id(&gw, 0, "test"); ++ if (!gw.tags[0]) { ++ errx(1, "gw.tag_ids[0] shouldn't be NULL"); ++ } ++ if (strcmp(gw.tags[0]->label, "test")) { ++ errx( ++ 1, "unexpected tag %.*s (expected test)", ++ (int)sizeof(gw.tags[0]->label), gw.tags[0]->label ++ ); ++ } ++ if (gw.tag_ids != LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(0)) { ++ errx( ++ 1, "tag_ids = %jx (expected %jx)", ++ (uintmax_t)gw.tag_ids, (uintmax_t)expected_tag_ids ++ ); ++ } ++ if (!tagging_incref_called) { ++ errx(1, "lgtd_lifx_tagging_incref should have been called"); ++ } ++ tagging_incref_called = false; ++ ++ for (int i = 1; i != 3; i++) { ++ int tag_id = lgtd_lifx_gateway_allocate_tag_id(&gw, -1, "lounge"); ++ if (tag_id < 1) { ++ errx(1, "no tag_id was allocated (received tag_id %d)", tag_id); ++ } ++ if (!gw.tags[tag_id]) { ++ errx(1, "gw.tag_ids[%d] shouldn't be NULL", i); ++ } ++ if (strcmp(gw.tags[tag_id]->label, "lounge")) { ++ errx( ++ 1, "unexpected tag %.*s (expected lounge)", ++ (int)sizeof(gw.tags[tag_id]->label), gw.tags[tag_id]->label ++ ); ++ } ++ expected_tag_ids |= LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id); ++ if (gw.tag_ids != expected_tag_ids) { ++ errx( ++ 1, "tag_ids = %jx (expected %jx)", ++ (uintmax_t)gw.tag_ids, (uintmax_t)expected_tag_ids ++ ); ++ } ++ if (!tagging_incref_called) { ++ errx(1, "lgtd_lifx_tagging_incref should have been called"); ++ } ++ tagging_incref_called = false; ++ } ++ ++ return 0; ++} +diff --git a/tests/lifx/gateway/test_gateway_allocate_tag_id_no_tag_id_left.c b/tests/lifx/gateway/test_gateway_allocate_tag_id_no_tag_id_left.c +new file mode 100644 +--- /dev/null ++++ b/tests/lifx/gateway/test_gateway_allocate_tag_id_no_tag_id_left.c +@@ -0,0 +1,89 @@ ++ ++#include <string.h> ++ ++#include "gateway.c" ++ ++#define MOCKED_LIFX_TAGGING_INCREF ++#include "test_gateway_utils.h" ++ ++static bool tagging_incref_called = false; ++ ++struct lgtd_lifx_tag * ++lgtd_lifx_tagging_incref(const char *label, ++ struct lgtd_lifx_gateway *gw, ++ int tag_id) ++{ ++ if (!label) { ++ errx(1, "missing tag label"); ++ } ++ if (!gw) { ++ errx(1, "missing gateway"); ++ } ++ if (tag_id < 0) { ++ errx(1, "got tag_id %d but expected >= 0", tag_id); ++ } ++ ++ struct lgtd_lifx_tag *tag = lgtd_lifx_tagging_find_tag(label); ++ if (!tag) { ++ tag = calloc(1, sizeof(*tag)); ++ strcpy(tag->label, label); ++ struct lgtd_lifx_site *site = calloc(1, sizeof(*site)); ++ site->gw = gw; ++ site->tag_id = tag_id; ++ LIST_INSERT_HEAD(&tag->sites, site, link); ++ } ++ ++ tagging_incref_called = true; ++ ++ return tag; ++} ++ ++int ++main(void) ++{ ++ lgtd_lifx_wire_load_packet_infos_map(); ++ ++ struct lgtd_lifx_gateway gw; ++ memset(&gw, 0, sizeof(gw)); ++ ++ struct lgtd_lifx_packet_header hdr; ++ memset(&hdr, 0, sizeof(hdr)); ++ ++ uint64_t expected_tag_ids = 0; ++ for (int i = 0; i != LGTD_LIFX_GATEWAY_MAX_TAGS; i++) { ++ int tag_id = lgtd_lifx_gateway_allocate_tag_id(&gw, -1, "lounge"); ++ if (tag_id < 0) { ++ errx(1, "no tag_id was allocated (received tag_id %d)", tag_id); ++ } ++ if (!gw.tags[tag_id]) { ++ errx(1, "gw.tag_ids[%d] shouldn't be NULL", i); ++ } ++ if (strcmp(gw.tags[tag_id]->label, "lounge")) { ++ errx( ++ 1, "unexpected tag %.*s (expected lounge)", ++ (int)sizeof(gw.tags[tag_id]->label), gw.tags[tag_id]->label ++ ); ++ } ++ expected_tag_ids |= LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id); ++ if (gw.tag_ids != expected_tag_ids) { ++ errx( ++ 1, "tag_ids = %jx (expected %jx)", ++ (uintmax_t)gw.tag_ids, (uintmax_t)expected_tag_ids ++ ); ++ } ++ if (!tagging_incref_called) { ++ errx(1, "lgtd_lifx_tagging_incref should have been called"); ++ } ++ tagging_incref_called = false; ++ } ++ ++ int tag_id = lgtd_lifx_gateway_allocate_tag_id(&gw, -1, "lounge"); ++ if (tag_id != -1) { ++ errx(1, "tag_ids full but tag_id %d was allocated", tag_id); ++ } ++ if (tagging_incref_called) { ++ errx(1, "lgtd_lifx_tagging_incref should not have been called"); ++ } ++ ++ return 0; ++} +diff --git a/tests/lifx/gateway/test_gateway_update_tag_refcounts.c b/tests/lifx/gateway/test_gateway_update_tag_refcounts.c +new file mode 100644 +--- /dev/null ++++ b/tests/lifx/gateway/test_gateway_update_tag_refcounts.c +@@ -0,0 +1,106 @@ ++#include "gateway.c" ++ ++#include "test_gateway_utils.h" ++ ++int ++main(void) ++{ ++ lgtd_lifx_wire_load_packet_infos_map(); ++ ++ struct lgtd_lifx_gateway gw; ++ memset(&gw, 0, sizeof(gw)); ++ ++ lgtd_lifx_gateway_update_tag_refcounts(&gw, 0, 0); ++ for (int i = 0; i != LGTD_LIFX_GATEWAY_MAX_TAGS; i++) { ++ if (gw.tag_refcounts[i]) { ++ errx( ++ 1, "gw.tag_refcounts[%d] was %d, (expected 0)", ++ i, gw.tag_refcounts[i] ++ ); ++ } ++ } ++ ++ for (int n = 1; n != 3; n++) { ++ lgtd_lifx_gateway_update_tag_refcounts(&gw, 0, 1); ++ if (gw.tag_refcounts[0] != n) { ++ errx( ++ 1, "gw.tag_refcounts[0] was %d (expected %d)", ++ gw.tag_refcounts[0], n ++ ); ++ } ++ for (int i = 1; i != LGTD_LIFX_GATEWAY_MAX_TAGS; i++) { ++ if (gw.tag_refcounts[i]) { ++ errx( ++ 1, "gw.tag_refcounts[%d] was %d (expected 0)", ++ i, gw.tag_refcounts[i] ++ ); ++ } ++ } ++ } ++ ++ lgtd_lifx_gateway_update_tag_refcounts(&gw, 0, 2); ++ gw.tag_ids = 0x2; ++ ++ for (int n = 1; n >= 0; n--) { ++ lgtd_lifx_gateway_update_tag_refcounts(&gw, 1, 0); ++ if (gw.tag_refcounts[0] != n) { ++ errx( ++ 1, "gw.tag_refcounts[0] was %d (expected %d)", ++ gw.tag_refcounts[0], n - 1 ++ ); ++ } ++ if (gw.tag_refcounts[1] != 1) { ++ errx( ++ 1, "gw.tag_refcounts[1] was %d (expected 1)", ++ gw.tag_refcounts[1] ++ ); ++ } ++ for (int i = 2; i != LGTD_LIFX_GATEWAY_MAX_TAGS; i++) { ++ if (gw.tag_refcounts[i]) { ++ errx( ++ 1, "gw.tag_refcounts[%d] was %d (expected 0)", ++ i, gw.tag_refcounts[i] ++ ); ++ } ++ } ++ } ++ if (gw.pkt_ring[0].type != LGTD_LIFX_SET_TAG_LABELS) { ++ errx(1, "SET_TAG_LABELS should have been enqueued on the gateway"); ++ } ++ ++ struct lgtd_lifx_packet_tag_labels *pkt = ++ (void *)&gw_write_buf[sizeof(struct lgtd_lifx_packet_header)]; ++ uint64_t tags = le64toh(pkt->tags); ++ if (tags != ~2ULL) { ++ errx( ++ 1, "tags on LGTD_LIFX_SET_TAG_LABELS was %#jx (expected %#jx)", ++ (uintmax_t)tags, (uintmax_t)~2ULL ++ ); ++ } ++ const char blank_label[LGTD_LIFX_LABEL_SIZE] = { 0 }; ++ if (memcmp(pkt->label, blank_label, LGTD_LIFX_LABEL_SIZE)) { ++ errx( ++ 1, "label on LGTD_LIFX_SET_TAG_LABELS should be " ++ "all zero but got %.*s", LGTD_LIFX_LABEL_SIZE, pkt->label ++ ); ++ } ++ ++ for (int n = 0; n != UINT8_MAX; n++) { ++ lgtd_lifx_gateway_update_tag_refcounts(&gw, 0, 4); ++ } ++ if (gw.tag_refcounts[2] != UINT8_MAX) { ++ errx( ++ 1, "gw.tag_refcounts[2] was %d (expected %d)", ++ gw.tag_refcounts[2], UINT8_MAX ++ ); ++ } ++ lgtd_lifx_gateway_update_tag_refcounts(&gw, 0, 4); ++ if (gw.tag_refcounts[2] != UINT8_MAX) { ++ errx( ++ 1, "gw.tag_refcounts[2] was %d (expected %d)", ++ gw.tag_refcounts[2], UINT8_MAX ++ ); ++ } ++ ++ return 0; ++} +diff --git a/tests/lifx/mock_gateway.h b/tests/lifx/mock_gateway.h +new file mode 100644 +--- /dev/null ++++ b/tests/lifx/mock_gateway.h +@@ -0,0 +1,131 @@ ++#pragma once ++ ++#include "core/time_monotonic.h" ++#include "lifx/bulb.h" ++#include "lifx/gateway.h" ++ ++struct lgtd_lifx_tag; ++struct lgtd_lifx_gateway; ++ ++#ifndef MOCKED_LIFX_GATEWAY_SEND_TO_SITE ++bool ++lgtd_lifx_gateway_send_to_site(struct lgtd_lifx_gateway *gw, ++ enum lgtd_lifx_packet_type pkt_type, ++ const void *pkt) ++{ ++ (void)gw; ++ (void)pkt_type; ++ (void)pkt; ++ return false; ++} ++#endif ++ ++#ifndef MOCKED_LIFX_GATEWAY_ALLOCATE_TAG_ID ++int ++lgtd_lifx_gateway_allocate_tag_id(struct lgtd_lifx_gateway *gw, ++ int tag_id, ++ const char *tag_label) ++{ ++ (void)gw; ++ (void)tag_id; ++ (void)tag_label; ++ return -1; ++} ++#endif ++ ++#ifndef MOCKED_LGTD_LIFX_GATEWAY_HANDLE_PAN_GATEWAY ++void ++lgtd_lifx_gateway_handle_pan_gateway(struct lgtd_lifx_gateway *gw, ++ const struct lgtd_lifx_packet_header *hdr, ++ const struct lgtd_lifx_packet_pan_gateway *pkt) ++{ ++ (void)gw; ++ (void)hdr; ++ (void)pkt; ++} ++#endif ++ ++#ifndef MOCKED_LGTD_LIFX_GATEWAY_HANDLE_LIGHT_STATUS ++void ++lgtd_lifx_gateway_handle_light_status(struct lgtd_lifx_gateway *gw, ++ const struct lgtd_lifx_packet_header *hdr, ++ const struct lgtd_lifx_packet_light_status *pkt) ++{ ++ (void)gw; ++ (void)hdr; ++ (void)pkt; ++} ++#endif ++ ++#ifndef MOCKED_LGTD_LIFX_GATEWAY_HANDLE_POWER_STATE ++void ++lgtd_lifx_gateway_handle_power_state(struct lgtd_lifx_gateway *gw, ++ const struct lgtd_lifx_packet_header *hdr, ++ const struct lgtd_lifx_packet_power_state *pkt) ++{ ++ (void)gw; ++ (void)hdr; ++ (void)pkt; ++} ++#endif ++ ++#ifndef MOCKED_LGTD_LIFX_GATEWAY_HANDLE_TAG_LABELS ++void ++lgtd_lifx_gateway_handle_tag_labels(struct lgtd_lifx_gateway *gw, ++ const struct lgtd_lifx_packet_header *hdr, ++ const struct lgtd_lifx_packet_tag_labels *pkt) ++{ ++ (void)gw; ++ (void)hdr; ++ (void)pkt; ++} ++#endif ++ ++#ifndef MOCKED_LGTD_LIFX_GATEWAY_HANDLE_TAGS ++void ++lgtd_lifx_gateway_handle_tags(struct lgtd_lifx_gateway *gw, ++ const struct lgtd_lifx_packet_header *hdr, ++ const struct lgtd_lifx_packet_tags *pkt) ++{ ++ (void)gw; ++ (void)hdr; ++ (void)pkt; ++} ++#endif ++ ++#ifndef MOCKED_LGTD_LIFX_GATEWAY_DEALLOCATE_TAG_ID ++void ++lgtd_lifx_gateway_deallocate_tag_id(struct lgtd_lifx_gateway *gw, int tag_id) ++{ ++ (void)gw; ++ (void)tag_id; ++} ++#endif ++ ++#ifndef MOCKED_LGTD_LIFX_GATEWAY_GET_TAG_ID ++int ++lgtd_lifx_gateway_get_tag_id(const struct lgtd_lifx_gateway *gw, ++ const struct lgtd_lifx_tag *tag) ++{ ++ int tag_id; ++ LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, gw->tag_ids) { ++ if (gw->tags[tag_id] == tag) { ++ return tag_id; ++ } ++ } ++ ++ return -1; ++} ++#endif ++ ++#ifndef MOCKED_LGTD_LIFX_GATEWAY_UPDATE_TAG_REFCOUNTS ++void ++lgtd_lifx_gateway_update_tag_refcounts(struct lgtd_lifx_gateway *gw, ++ uint64_t bulb_tags, ++ uint64_t pkt_tags) ++{ ++ (void)gw; ++ (void)bulb_tags; ++ (void)pkt_tags; ++} ++#endif +diff --git a/tests/lifx/wire_proto/test_wire_proto_utils.h b/tests/lifx/wire_proto/test_wire_proto_utils.h +--- a/tests/lifx/wire_proto/test_wire_proto_utils.h ++++ b/tests/lifx/wire_proto/test_wire_proto_utils.h +@@ -35,3 +35,12 @@ + (void)hdr; + (void)pkt; + } ++ ++void lgtd_lifx_gateway_handle_tags(struct lgtd_lifx_gateway *gw, ++ const struct lgtd_lifx_packet_header *hdr, ++ const struct lgtd_lifx_packet_tags *pkt) ++{ ++ (void)gw; ++ (void)hdr; ++ (void)pkt; ++} +diff --git a/tests/lightsc b/tests/lightsc +--- a/tests/lightsc ++++ b/tests/lightsc +@@ -72,6 +72,14 @@ + def get_light_state(socket, target): + return jsonrpc_call(socket, "get_light_state", [target]) + ++ ++def tag(socket, target, tag): ++ return jsonrpc_call(socket, "tag", [target, tag]) ++ ++ ++def untag(socket, target, tag): ++ return jsonrpc_call(socket, "untag", [target, tag]) ++ + if __name__ == "__main__": + s = socket.create_connection(("localhost", 1234)) + h = 0
--- a/daemon_module.patch Sun Aug 02 19:02:55 2015 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1071 +0,0 @@ -# HG changeset patch -# Parent d7a2d37c150198e9b0626332d1cc57c538bd5447 - -diff --git a/README.rst b/README.rst ---- a/README.rst -+++ b/README.rst -@@ -32,8 +32,8 @@ - - tag/untag (group/ungroup bulbs together); - - toggle (power on if off and vice-versa, coming up). - --The JSON-RPC interface works on top on IPv4/v6, over a command (named) pipe --(coming up) and Unix sockets (coming up). -+The JSON-RPC interface works on top on IPv4/v6, over a command (named) pipe and -+Unix sockets (coming up). - - lightsd can target single or multiple bulbs at once: - -diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt ---- a/core/CMakeLists.txt -+++ b/core/CMakeLists.txt -@@ -15,6 +15,7 @@ - ADD_EXECUTABLE( - lightsd - client.c -+ daemon.c - jsmn.c - jsonrpc.c - listen.c -diff --git a/core/client.c b/core/client.c ---- a/core/client.c -+++ b/core/client.c -@@ -34,6 +34,8 @@ - #include "jsonrpc.h" - #include "client.h" - #include "proto.h" -+#include "stats.h" -+#include "daemon.h" - #include "lightsd.h" - - struct lgtd_client_list lgtd_clients = LIST_HEAD_INITIALIZER(&lgtd_clients); -@@ -44,6 +46,8 @@ - assert(client); - assert(client->io); - -+ LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(clients, -1); -+ - LIST_REMOVE(client, link); - bufferevent_free(client->io); - free(client); -@@ -217,6 +221,8 @@ - - LIST_INSERT_HEAD(&lgtd_clients, client, link); - -+ LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(clients, 1); -+ - return client; - } - -diff --git a/core/daemon.c b/core/daemon.c -new file mode 100644 ---- /dev/null -+++ b/core/daemon.c -@@ -0,0 +1,155 @@ -+// Copyright (c) 2015, Louis Opter <kalessin@kalessin.fr> -+// -+// This file is part of lighstd. -+// -+// lighstd is free software: you can redistribute it and/or modify -+// it under the terms of the GNU General Public License as published by -+// the Free Software Foundation, either version 3 of the License, or -+// (at your option) any later version. -+// -+// lighstd is distributed in the hope that it will be useful, -+// but WITHOUT ANY WARRANTY; without even the implied warranty of -+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -+// GNU General Public License for more details. -+// -+// You should have received a copy of the GNU General Public License -+// along with lighstd. If not, see <http://www.gnu.org/licenses/>. -+ -+#include <sys/queue.h> -+#include <sys/tree.h> -+#include <sys/types.h> -+#include <endian.h> -+#include <fcntl.h> -+#include <stdbool.h> -+#include <stdint.h> -+#include <stdio.h> -+#include <stdlib.h> -+#include <string.h> -+#include <unistd.h> -+ -+#if LGTD_HAVE_LIBBSD -+#include <bsd/bsd.h> -+#endif -+ -+#include <event2/util.h> -+ -+#include "time_monotonic.h" -+#include "lifx/wire_proto.h" -+#include "lifx/bulb.h" -+#include "lifx/gateway.h" -+#include "jsmn.h" -+#include "jsonrpc.h" -+#include "client.h" -+#include "listen.h" -+#include "daemon.h" -+#include "pipe.h" -+#include "stats.h" -+#include "lightsd.h" -+ -+bool -+lgtd_daemon_unleash(void) -+{ -+ if (chdir("/")) { -+ return false; -+ } -+ -+ int null = open("/dev/null", O_RDWR); -+ if (null == -1) { -+ return false; -+ } -+ -+ const int fds[] = { STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO }; -+ for (int i = 0; i != LGTD_ARRAY_SIZE(fds); ++i) { -+ if (dup2(null, fds[i]) == -1) { -+ close(null); -+ return false; -+ } -+ } -+ close(null); -+ -+#define SUMMON() do { \ -+ switch (fork()) { \ -+ case 0: \ -+ break; \ -+ case -1: \ -+ return false; \ -+ default: \ -+ exit(0); \ -+ } \ -+} while (0) -+ -+ SUMMON(); // \_o< ! -+ setsid(); -+ -+ SUMMON(); // \_o< !! -+ -+ return true; // welcome to UNIX! -+} -+ -+void -+lgtd_daemon_setup_proctitle(int argc, char *argv[], char *envp[]) -+{ -+#if LGTD_HAVE_LIBBSD -+ setproctitle_init(argc, argv, envp); -+ lgtd_daemon_update_proctitle(); -+#else -+ (void)argc; -+ (void)argv; -+ (void)envp; -+#endif -+} -+ -+void -+lgtd_daemon_update_proctitle(void) -+{ -+#if LGTD_HAVE_PROCTITLE -+ char title[LGTD_DAEMON_TITLE_SIZE] = { 0 }; -+ int i = 0; -+ -+#define TITLE_APPEND(fmt, ...) do { \ -+ int n = snprintf((&title[i]), (sizeof(title) - i), (fmt), __VA_ARGS__); \ -+ i = LGTD_MIN(i + n, (int)sizeof(title)); \ -+} while (0) -+ -+#define PREFIX(fmt, ...) TITLE_APPEND( \ -+ "%s" fmt, (i && title[i - 1] == ')' ? "; " : ""), __VA_ARGS__ \ -+) -+ -+#define ADD_ITEM(fmt, ...) TITLE_APPEND( \ -+ "%s" fmt, (i && title[i - 1] != '(' ? ", " : ""), __VA_ARGS__ \ -+) -+#define LOOP(list_type, list, elem_type, prefix, ...) do { \ -+ if (!list_type ## _EMPTY(list)) { \ -+ PREFIX("%s(", prefix); \ -+ elem_type *it; \ -+ list_type ## _FOREACH(it, list, link) { \ -+ ADD_ITEM(__VA_ARGS__); \ -+ } \ -+ TITLE_APPEND("%s", ")"); \ -+ } \ -+} while (0) -+ -+ LOOP( -+ SLIST, &lgtd_listeners, struct lgtd_listen, -+ "listening_on", "%s:[%s]", it->addr, it->port -+ ); -+ -+ LOOP( -+ SLIST, &lgtd_command_pipes, struct lgtd_command_pipe, -+ "command_pipes", "%s", it->path -+ ); -+ -+ if (!LIST_EMPTY(&lgtd_lifx_gateways)) { -+ PREFIX("lifx_gateways(found=%d)", LGTD_STATS_GET(gateways)); -+ } -+ -+ PREFIX( -+ "bulbs(found=%d, on=%d)", -+ LGTD_STATS_GET(bulbs), LGTD_STATS_GET(bulbs_powered_on) -+ ); -+ -+ PREFIX("clients(connected=%d)", LGTD_STATS_GET(clients)); -+ -+ setproctitle("%s", title); -+#endif -+} -diff --git a/core/daemon.h b/core/daemon.h -new file mode 100644 ---- /dev/null -+++ b/core/daemon.h -@@ -0,0 +1,24 @@ -+// Copyright (c) 2015, Louis Opter <kalessin@kalessin.fr> -+// -+// This file is part of lighstd. -+// -+// lighstd is free software: you can redistribute it and/or modify -+// it under the terms of the GNU General Public License as published by -+// the Free Software Foundation, either version 3 of the License, or -+// (at your option) any later version. -+// -+// lighstd is distributed in the hope that it will be useful, -+// but WITHOUT ANY WARRANTY; without even the implied warranty of -+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -+// GNU General Public License for more details. -+// -+// You should have received a copy of the GNU General Public License -+// along with lighstd. If not, see <http://www.gnu.org/licenses/>. -+ -+#pragma once -+ -+enum { LGTD_DAEMON_TITLE_SIZE = 2048 }; -+ -+bool lgtd_daemon_unleash(void); // \_o< -+void lgtd_daemon_setup_proctitle(int, char *[], char *[]); -+void lgtd_daemon_update_proctitle(void); -diff --git a/core/lightsd.c b/core/lightsd.c ---- a/core/lightsd.c -+++ b/core/lightsd.c -@@ -17,13 +17,11 @@ - - #include <sys/queue.h> - #include <sys/tree.h> --#include <sys/types.h> - #include <arpa/inet.h> - #include <assert.h> - #include <endian.h> - #include <err.h> - #include <errno.h> --#include <fcntl.h> - #include <getopt.h> - #include <signal.h> - #include <stdarg.h> -@@ -33,11 +31,6 @@ - #include <stdlib.h> - #include <string.h> - #include <strings.h> --#include <unistd.h> -- --#if LGTD_HAVE_LIBBSD --#include <bsd/bsd.h> --#endif - - #include <event2/event.h> - #include <event2/event_struct.h> -@@ -54,6 +47,7 @@ - #include "client.h" - #include "pipe.h" - #include "listen.h" -+#include "daemon.h" - #include "lightsd.h" - - struct lgtd_opts lgtd_opts = { -@@ -64,8 +58,6 @@ - - struct event_base *lgtd_ev_base = NULL; - --const char *lgtd_binds; -- - void - lgtd_cleanup(void) - { -@@ -151,57 +143,11 @@ - exit(0); - } - --static bool --lgtd_daemonize(void) --{ -- if (chdir("/")) { -- return false; -- } -- -- int null = open("/dev/null", O_RDWR); -- if (null == -1) { -- return false; -- } -- -- const int fds[] = { STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO }; -- for (int i = 0; i != LGTD_ARRAY_SIZE(fds); ++i) { -- if (dup2(null, fds[i]) == -1) { -- close(null); -- return false; -- } -- } -- close(null); -- --#define FORK() do { \ -- switch (fork()) { \ -- case 0: \ -- break; \ -- case -1: \ -- return false; \ -- default: \ -- exit(0); \ -- } \ --} while (0) -- -- FORK(); -- setsid(); -- -- FORK(); -- -- return true; --} -- - int - main(int argc, char *argv[], char *envp[]) - { -- char binds[512] = { 0 }; -- lgtd_binds = binds; -+ lgtd_daemon_setup_proctitle(argc, argv, envp); - --#if LGTD_HAVE_LIBBSD -- setproctitle_init(argc, argv, envp); --#else -- (void)envp; --#endif - lgtd_configure_libevent(); - lgtd_configure_signal_handling(); - -@@ -231,17 +177,10 @@ - if (!sep || !sep[1]) { - lgtd_usage(argv[0]); - } -- strncat(binds, optarg, LGTD_MIN( -- sizeof(binds) - strlen(binds) - 1, strlen(optarg) -- )); -- strncat(binds, ", ", LGTD_MIN( -- sizeof(binds) - strlen(binds) - 1, 2 -- )); - *sep = '\0'; - if (!lgtd_listen_open(optarg, sep + 1)) { - exit(1); - } -- *sep = ':'; - break; - case 'c': - if (!lgtd_command_pipe_open(optarg)) { -@@ -278,12 +217,6 @@ - } - } - -- binds[LGTD_MAX(strlen(binds) - 1, 0)] = '\0'; -- binds[LGTD_MAX(strlen(binds) - 2, 0)] = '\0'; -- if (binds[sizeof(binds) - 2]) { -- memset(binds + sizeof(binds) - 4, '.', 3); -- } -- - argc -= optind; - argv += optind; - -@@ -292,7 +225,7 @@ - lgtd_err(1, "can't setup lightsd"); - } - -- if (!lgtd_opts.foreground && !lgtd_daemonize()) { -+ if (!lgtd_opts.foreground && !lgtd_daemon_unleash()) { - lgtd_err(1, "can't fork to the background"); - } - -diff --git a/core/lightsd.h b/core/lightsd.h ---- a/core/lightsd.h -+++ b/core/lightsd.h -@@ -47,7 +47,6 @@ - enum lgtd_verbosity verbosity; - }; - --extern const char *lgtd_binds; - extern struct lgtd_opts lgtd_opts; - extern struct event_base *lgtd_ev_base; - -@@ -66,6 +65,5 @@ - void lgtd_info(const char *, ...) __attribute__((format(printf, 1, 2))); - void lgtd_debug(const char *, ...) __attribute__((format(printf, 1, 2))); - void lgtd_libevent_log(int, const char *); --void lgtd_update_proctitle(void); - - void lgtd_cleanup(void); -diff --git a/core/listen.c b/core/listen.c ---- a/core/listen.c -+++ b/core/listen.c -@@ -30,6 +30,7 @@ - #include "jsonrpc.h" - #include "client.h" - #include "listen.h" -+#include "daemon.h" - #include "lightsd.h" - - struct lgtd_listen_list lgtd_listeners = -@@ -69,6 +70,8 @@ - evconnlistener_free(listener->evlistener); - free(listener); - } -+ -+ lgtd_daemon_update_proctitle(); - } - - bool -@@ -130,6 +133,8 @@ - - evutil_freeaddrinfo(res); - -+ lgtd_daemon_update_proctitle(); -+ - return true; - - error: -diff --git a/core/listen.h b/core/listen.h ---- a/core/listen.h -+++ b/core/listen.h -@@ -17,6 +17,8 @@ - - #pragma once - -+struct evconnlistener; -+ - struct lgtd_listen { - SLIST_ENTRY(lgtd_listen) link; - const char *addr; -@@ -25,5 +27,7 @@ - }; - SLIST_HEAD(lgtd_listen_list, lgtd_listen); - -+extern struct lgtd_listen_list lgtd_listeners; -+ - bool lgtd_listen_open(const char *, const char *); - void lgtd_listen_close_all(void); -diff --git a/core/log.c b/core/log.c ---- a/core/log.c -+++ b/core/log.c -@@ -180,17 +180,3 @@ - default: break; - } - } -- --void --lgtd_update_proctitle(void) --{ --#if LGTD_HAVE_PROCTITLE -- setproctitle( -- "listening_on(%s); lifx_gateways(found=%d); bulbs(found=%d, on=%d)", -- lgtd_binds, -- LGTD_STATS_GET(gateways), -- LGTD_STATS_GET(bulbs), -- LGTD_STATS_GET(bulbs_powered_on) -- ); --#endif --} -diff --git a/core/pipe.c b/core/pipe.c ---- a/core/pipe.c -+++ b/core/pipe.c -@@ -37,7 +37,7 @@ - #include "pipe.h" - #include "lightsd.h" - --static struct lgtd_command_pipe_list lgtd_command_pipes = -+struct lgtd_command_pipe_list lgtd_command_pipes = - SLIST_HEAD_INITIALIZER(&lgtd_command_pipes); - - static void -@@ -174,6 +174,7 @@ - return false; - } - -+ lgtd_client_open_from_pipe(&pipe->client); - pipe->path = path; - pipe->fd = -1; - -diff --git a/core/pipe.h b/core/pipe.h ---- a/core/pipe.h -+++ b/core/pipe.h -@@ -27,5 +27,7 @@ - }; - SLIST_HEAD(lgtd_command_pipe_list, lgtd_command_pipe); - -+extern struct lgtd_command_pipe_list lgtd_command_pipes; -+ - bool lgtd_command_pipe_open(const char *); - void lgtd_command_pipe_close_all(void); -diff --git a/core/stats.h b/core/stats.h ---- a/core/stats.h -+++ b/core/stats.h -@@ -21,13 +21,15 @@ - int gateways; - int bulbs; - int bulbs_powered_on; -+ int clients; - }; - - void lgtd_stats_add(int, int); - int lgtd_stats_get(int); - - #define LGTD_STATS_GET(name) lgtd_stats_get(offsetof(struct lgtd_stats, name)) --#define LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(name, value) do { \ -- lgtd_stats_add(offsetof(struct lgtd_stats, name), (value)); \ -- lgtd_update_proctitle(); \ -+ -+#define LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(name, value) do { \ -+ lgtd_stats_add(offsetof(struct lgtd_stats, name), (value)); \ -+ lgtd_daemon_update_proctitle(); \ - } while (0) -diff --git a/lifx/bulb.c b/lifx/bulb.c ---- a/lifx/bulb.c -+++ b/lifx/bulb.c -@@ -32,6 +32,7 @@ - #include "core/time_monotonic.h" - #include "bulb.h" - #include "gateway.h" -+#include "core/daemon.h" - #include "core/stats.h" - #include "core/lightsd.h" - -diff --git a/lifx/gateway.c b/lifx/gateway.c ---- a/lifx/gateway.c -+++ b/lifx/gateway.c -@@ -45,6 +45,7 @@ - #include "core/proto.h" - #include "core/router.h" - #include "core/stats.h" -+#include "core/daemon.h" - #include "core/lightsd.h" - - struct lgtd_lifx_gateway_list lgtd_lifx_gateways = -diff --git a/tests/core/daemon/CMakeLists.txt b/tests/core/daemon/CMakeLists.txt -new file mode 100644 ---- /dev/null -+++ b/tests/core/daemon/CMakeLists.txt -@@ -0,0 +1,24 @@ -+INCLUDE_DIRECTORIES( -+ ${CMAKE_CURRENT_SOURCE_DIR} -+ ${CMAKE_CURRENT_BINARY_DIR} -+) -+ -+ADD_CORE_LIBRARY( -+ test_core_daemon STATIC -+ ${LIGHTSD_SOURCE_DIR}/core/log.c -+ ${LIGHTSD_SOURCE_DIR}/core/stats.c -+ ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c -+ ${LIGHTSD_SOURCE_DIR}/lifx/tagging.c -+ ${LIGHTSD_SOURCE_DIR}/lifx/wire_proto.c -+ ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c -+ ${CMAKE_CURRENT_SOURCE_DIR}/../tests_utils.c -+) -+ -+FUNCTION(ADD_DAEMON_TEST TEST_SOURCE) -+ ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_core_daemon) -+ENDFUNCTION() -+ -+FILE(GLOB TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test_*.c") -+FOREACH(TEST ${TESTS}) -+ ADD_DAEMON_TEST(${TEST}) -+ENDFOREACH() -diff --git a/tests/core/daemon/mock_pipe.h b/tests/core/daemon/mock_pipe.h -new file mode 100644 ---- /dev/null -+++ b/tests/core/daemon/mock_pipe.h -@@ -0,0 +1,4 @@ -+#pragma once -+ -+struct lgtd_command_pipe_list lgtd_command_pipes = -+ SLIST_HEAD_INITIALIZER(&lgtd_command_pipes); -diff --git a/tests/core/daemon/test_daemon_update_proctitle.c b/tests/core/daemon/test_daemon_update_proctitle.c -new file mode 100644 ---- /dev/null -+++ b/tests/core/daemon/test_daemon_update_proctitle.c -@@ -0,0 +1,109 @@ -+void mock_setproctitle(const char *fmt, ...) -+ __attribute__((format(printf, 1, 2))); -+ -+#undef LGTD_HAVE_LIBBSD -+#undef LGTD_HAVE_PROCTITLE -+#define LGTD_HAVE_PROCTITLE 1 -+#define setproctitle mock_setproctitle -+#include "daemon.c" -+ -+#include <err.h> -+ -+#include "mock_gateway.h" -+#include "mock_pipe.h" -+ -+#include "tests_utils.h" -+ -+const char *expected = ""; -+int setproctitle_call_count = 0; -+ -+void -+mock_setproctitle(const char *fmt, ...) -+{ -+ if (strcmp(fmt, "%s")) { -+ errx(1, "unexepected format %s (expected %%s)", fmt); -+ } -+ -+ va_list ap; -+ va_start(ap, fmt); -+ const char *title = va_arg(ap, const char *); -+ va_end(ap); -+ -+ if (strcmp(title, expected)) { -+ errx(1, "unexpected title: %s (expected %s)", title, expected); -+ } -+ -+ setproctitle_call_count++; -+} -+ -+int -+main(void) -+{ -+ expected = "bulbs(found=0, on=0); clients(connected=0)"; -+ lgtd_daemon_update_proctitle(); -+ if (setproctitle_call_count != 1) { -+ errx(1, "setproctitle should have been called"); -+ } -+ -+ expected = ( -+ "lifx_gateways(found=1); " -+ "bulbs(found=0, on=0); " -+ "clients(connected=0)" -+ ); -+ struct lgtd_lifx_gateway *gw_1 = lgtd_tests_insert_mock_gateway(1); -+ if (setproctitle_call_count != 2) { -+ errx(1, "setproctitle should have been called"); -+ } -+ -+ expected = ( -+ "lifx_gateways(found=1); " -+ "bulbs(found=1, on=0); " -+ "clients(connected=0)" -+ ); -+ lgtd_tests_insert_mock_bulb(gw_1, 2); -+ expected = ( -+ "lifx_gateways(found=1); " -+ "bulbs(found=2, on=0); " -+ "clients(connected=0)" -+ ); -+ lgtd_tests_insert_mock_bulb(gw_1, 3); -+ if (setproctitle_call_count != 4) { -+ errx(1, "setproctitle should have been called"); -+ } -+ -+ expected = ( -+ "listening_on(foobar.com:[1234]); " -+ "lifx_gateways(found=1); " -+ "bulbs(found=2, on=0); " -+ "clients(connected=0)" -+ ); -+ lgtd_tests_insert_mock_listener("foobar.com", "1234"); -+ lgtd_daemon_update_proctitle(); -+ if (setproctitle_call_count != 5) { -+ errx(1, "setproctitle should have been called"); -+ } -+ -+ expected = ( -+ "listening_on(foobar.com:[1234]); " -+ "lifx_gateways(found=1); " -+ "bulbs(found=2, on=1); " -+ "clients(connected=0)" -+ ); -+ LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(bulbs_powered_on, 1); -+ if (setproctitle_call_count != 6) { -+ errx(1, "setproctitle should have been called"); -+ } -+ -+ expected = ( -+ "listening_on(foobar.com:[1234]); " -+ "lifx_gateways(found=1); " -+ "bulbs(found=2, on=1); " -+ "clients(connected=1)" -+ ); -+ LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(clients, 1); -+ if (setproctitle_call_count != 7) { -+ errx(1, "setproctitle should have been called"); -+ } -+ -+ return 0; -+} -diff --git a/tests/core/mock_daemon.h b/tests/core/mock_daemon.h -new file mode 100644 ---- /dev/null -+++ b/tests/core/mock_daemon.h -@@ -0,0 +1,8 @@ -+#pragma once -+ -+#ifndef MOCKED_DAEMON_UPDATE_PROCTITLE -+void -+lgtd_daemon_update_proctitle(void) -+{ -+} -+#endif -diff --git a/tests/core/proto/CMakeLists.txt b/tests/core/proto/CMakeLists.txt ---- a/tests/core/proto/CMakeLists.txt -+++ b/tests/core/proto/CMakeLists.txt -@@ -16,11 +16,11 @@ - ${CMAKE_CURRENT_SOURCE_DIR}/../tests_utils.c - ) - --FUNCTION(ADD_ROUTER_TEST TEST_SOURCE) -+FUNCTION(ADD_PROTO_TEST TEST_SOURCE) - ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_core_proto) - ENDFUNCTION() - - FILE(GLOB TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test_*.c") - FOREACH(TEST ${TESTS}) -- ADD_ROUTER_TEST(${TEST}) -+ ADD_PROTO_TEST(${TEST}) - ENDFOREACH() -diff --git a/tests/core/proto/test_proto_get_light_state.c b/tests/core/proto/test_proto_get_light_state.c ---- a/tests/core/proto/test_proto_get_light_state.c -+++ b/tests/core/proto/test_proto_get_light_state.c -@@ -1,6 +1,7 @@ - #include "proto.c" - - #include "mock_client_buf.h" -+#include "mock_daemon.h" - #include "mock_gateway.h" - #include "tests_utils.h" - -diff --git a/tests/core/proto/test_proto_get_light_state_empty_device_list.c b/tests/core/proto/test_proto_get_light_state_empty_device_list.c ---- a/tests/core/proto/test_proto_get_light_state_empty_device_list.c -+++ b/tests/core/proto/test_proto_get_light_state_empty_device_list.c -@@ -1,6 +1,7 @@ - #include "proto.c" - - #include "mock_client_buf.h" -+#include "mock_daemon.h" - #include "mock_gateway.h" - #include "tests_utils.h" - -diff --git a/tests/core/proto/test_proto_get_light_state_null_device_list.c b/tests/core/proto/test_proto_get_light_state_null_device_list.c ---- a/tests/core/proto/test_proto_get_light_state_null_device_list.c -+++ b/tests/core/proto/test_proto_get_light_state_null_device_list.c -@@ -1,6 +1,7 @@ - #include "proto.c" - - #include "mock_client_buf.h" -+#include "mock_daemon.h" - #include "mock_gateway.h" - #include "tests_utils.h" - -diff --git a/tests/core/proto/test_proto_power_off.c b/tests/core/proto/test_proto_power_off.c ---- a/tests/core/proto/test_proto_power_off.c -+++ b/tests/core/proto/test_proto_power_off.c -@@ -1,6 +1,7 @@ - #include "proto.c" - - #include "mock_client_buf.h" -+#include "mock_daemon.h" - #include "mock_gateway.h" - #include "tests_utils.h" - -diff --git a/tests/core/proto/test_proto_power_off_routing_error.c b/tests/core/proto/test_proto_power_off_routing_error.c ---- a/tests/core/proto/test_proto_power_off_routing_error.c -+++ b/tests/core/proto/test_proto_power_off_routing_error.c -@@ -1,6 +1,7 @@ - #include "proto.c" - - #include "mock_client_buf.h" -+#include "mock_daemon.h" - #include "mock_gateway.h" - #include "tests_utils.h" - -diff --git a/tests/core/proto/test_proto_power_on.c b/tests/core/proto/test_proto_power_on.c ---- a/tests/core/proto/test_proto_power_on.c -+++ b/tests/core/proto/test_proto_power_on.c -@@ -1,6 +1,7 @@ - #include "proto.c" - - #include "mock_client_buf.h" -+#include "mock_daemon.h" - #include "mock_gateway.h" - #include "tests_utils.h" - -diff --git a/tests/core/proto/test_proto_power_on_routing_error.c b/tests/core/proto/test_proto_power_on_routing_error.c ---- a/tests/core/proto/test_proto_power_on_routing_error.c -+++ b/tests/core/proto/test_proto_power_on_routing_error.c -@@ -1,6 +1,7 @@ - #include "proto.c" - - #include "mock_client_buf.h" -+#include "mock_daemon.h" - #include "mock_gateway.h" - #include "tests_utils.h" - -diff --git a/tests/core/proto/test_proto_set_light_from_hsbk.c b/tests/core/proto/test_proto_set_light_from_hsbk.c ---- a/tests/core/proto/test_proto_set_light_from_hsbk.c -+++ b/tests/core/proto/test_proto_set_light_from_hsbk.c -@@ -3,6 +3,7 @@ - #include "proto.c" - - #include "mock_client_buf.h" -+#include "mock_daemon.h" - #include "mock_gateway.h" - #include "tests_utils.h" - -diff --git a/tests/core/proto/test_proto_set_light_from_hsbk_on_routing_error.c b/tests/core/proto/test_proto_set_light_from_hsbk_on_routing_error.c ---- a/tests/core/proto/test_proto_set_light_from_hsbk_on_routing_error.c -+++ b/tests/core/proto/test_proto_set_light_from_hsbk_on_routing_error.c -@@ -3,6 +3,7 @@ - #include "proto.c" - - #include "mock_client_buf.h" -+#include "mock_daemon.h" - #include "mock_gateway.h" - #include "tests_utils.h" - -diff --git a/tests/core/proto/test_proto_set_waveform.c b/tests/core/proto/test_proto_set_waveform.c ---- a/tests/core/proto/test_proto_set_waveform.c -+++ b/tests/core/proto/test_proto_set_waveform.c -@@ -3,6 +3,7 @@ - #include "proto.c" - - #include "mock_client_buf.h" -+#include "mock_daemon.h" - #include "mock_gateway.h" - #include "tests_utils.h" - -diff --git a/tests/core/proto/test_proto_set_waveform_on_routing_error.c b/tests/core/proto/test_proto_set_waveform_on_routing_error.c ---- a/tests/core/proto/test_proto_set_waveform_on_routing_error.c -+++ b/tests/core/proto/test_proto_set_waveform_on_routing_error.c -@@ -3,6 +3,7 @@ - #include "proto.c" - - #include "mock_client_buf.h" -+#include "mock_daemon.h" - #include "mock_gateway.h" - #include "tests_utils.h" - -diff --git a/tests/core/proto/test_proto_tag_create.c b/tests/core/proto/test_proto_tag_create.c ---- a/tests/core/proto/test_proto_tag_create.c -+++ b/tests/core/proto/test_proto_tag_create.c -@@ -1,6 +1,7 @@ - #include "proto.c" - - #include "mock_client_buf.h" -+#include "mock_daemon.h" - #define MOCKED_LIFX_GATEWAY_SEND_TO_SITE - #define MOCKED_LIFX_GATEWAY_ALLOCATE_TAG_ID - #include "mock_gateway.h" -diff --git a/tests/core/proto/test_proto_tag_create_lifx_gw_tag_ids_full.c b/tests/core/proto/test_proto_tag_create_lifx_gw_tag_ids_full.c ---- a/tests/core/proto/test_proto_tag_create_lifx_gw_tag_ids_full.c -+++ b/tests/core/proto/test_proto_tag_create_lifx_gw_tag_ids_full.c -@@ -1,6 +1,7 @@ - #include "proto.c" - - #include "mock_client_buf.h" -+#include "mock_daemon.h" - #define MOCKED_LIFX_GATEWAY_SEND_TO_SITE - #define MOCKED_LIFX_GATEWAY_ALLOCATE_TAG_ID - #include "mock_gateway.h" -diff --git a/tests/core/proto/test_proto_tag_update.c b/tests/core/proto/test_proto_tag_update.c ---- a/tests/core/proto/test_proto_tag_update.c -+++ b/tests/core/proto/test_proto_tag_update.c -@@ -1,6 +1,7 @@ - #include "proto.c" - - #include "mock_client_buf.h" -+#include "mock_daemon.h" - #define MOCKED_LIFX_GATEWAY_SEND_TO_SITE - #define MOCKED_LIFX_GATEWAY_ALLOCATE_TAG_ID - #include "mock_gateway.h" -diff --git a/tests/core/proto/test_proto_untag.c b/tests/core/proto/test_proto_untag.c ---- a/tests/core/proto/test_proto_untag.c -+++ b/tests/core/proto/test_proto_untag.c -@@ -1,6 +1,7 @@ - #include "proto.c" - - #include "mock_client_buf.h" -+#include "mock_daemon.h" - #include "mock_gateway.h" - #include "tests_utils.h" - -diff --git a/tests/core/proto/test_proto_untag_tag_does_not_exist.c b/tests/core/proto/test_proto_untag_tag_does_not_exist.c ---- a/tests/core/proto/test_proto_untag_tag_does_not_exist.c -+++ b/tests/core/proto/test_proto_untag_tag_does_not_exist.c -@@ -1,6 +1,7 @@ - #include "proto.c" - - #include "mock_client_buf.h" -+#include "mock_daemon.h" - #include "mock_gateway.h" - #include "tests_utils.h" - -diff --git a/tests/core/router/test_router_send_to_broadcast.c b/tests/core/router/test_router_send_to_broadcast.c ---- a/tests/core/router/test_router_send_to_broadcast.c -+++ b/tests/core/router/test_router_send_to_broadcast.c -@@ -1,5 +1,6 @@ - #include "router.c" - -+#include "mock_daemon.h" - #include "tests_utils.h" - - #include "tests_router_utils.h" -diff --git a/tests/core/router/test_router_send_to_device.c b/tests/core/router/test_router_send_to_device.c ---- a/tests/core/router/test_router_send_to_device.c -+++ b/tests/core/router/test_router_send_to_device.c -@@ -1,5 +1,6 @@ - #include "router.c" - -+#include "mock_daemon.h" - #include "tests_utils.h" - #include "tests_router_utils.h" - -diff --git a/tests/core/router/test_router_send_to_invalid_targets.c b/tests/core/router/test_router_send_to_invalid_targets.c ---- a/tests/core/router/test_router_send_to_invalid_targets.c -+++ b/tests/core/router/test_router_send_to_invalid_targets.c -@@ -1,5 +1,6 @@ - #include "router.c" - -+#include "mock_daemon.h" - #include "tests_utils.h" - #include "tests_router_utils.h" - -diff --git a/tests/core/router/test_router_send_to_label.c b/tests/core/router/test_router_send_to_label.c ---- a/tests/core/router/test_router_send_to_label.c -+++ b/tests/core/router/test_router_send_to_label.c -@@ -1,5 +1,6 @@ - #include "router.c" - -+#include "mock_daemon.h" - #include "tests_utils.h" - #include "tests_router_utils.h" - -diff --git a/tests/core/router/test_router_send_to_tag.c b/tests/core/router/test_router_send_to_tag.c ---- a/tests/core/router/test_router_send_to_tag.c -+++ b/tests/core/router/test_router_send_to_tag.c -@@ -1,5 +1,6 @@ - #include "router.c" - -+#include "mock_daemon.h" - #include "tests_utils.h" - #include "tests_router_utils.h" - -diff --git a/tests/core/router/test_router_targets_to_devices.c b/tests/core/router/test_router_targets_to_devices.c ---- a/tests/core/router/test_router_targets_to_devices.c -+++ b/tests/core/router/test_router_targets_to_devices.c -@@ -1,5 +1,6 @@ - #include "router.c" - -+#include "mock_daemon.h" - #include "tests_utils.h" - #include "tests_router_utils.h" - -diff --git a/tests/core/tests_shims.h b/tests/core/tests_shims.h -new file mode 100644 ---- /dev/null -+++ b/tests/core/tests_shims.h -@@ -0,0 +1,23 @@ -+#pragma once -+ -+struct lgtd_opts lgtd_opts = { -+ .foreground = false, -+ .log_timestamps = false, -+ .verbosity = LGTD_DEBUG -+}; -+ -+struct event_base *lgtd_ev_base = NULL; -+ -+const char *lgtd_binds = NULL; -+ -+void -+lgtd_cleanup(void) -+{ -+} -+ -+#ifndef MOCKED_DAEMON_UPDATE_PROCTITLE -+void -+lgtd_daemon_update_proctitle(void) -+{ -+} -+#endif -diff --git a/tests/core/tests_utils.c b/tests/core/tests_utils.c ---- a/tests/core/tests_utils.c -+++ b/tests/core/tests_utils.c -@@ -24,6 +24,9 @@ - #include "core/jsonrpc.h" - #include "core/client.h" - #include "core/proto.h" -+#include "core/listen.h" -+#include "core/daemon.h" -+#include "core/stats.h" - #include "lifx/bulb.h" - #include "lifx/gateway.h" - #include "tests_utils.h" -@@ -31,6 +34,9 @@ - struct lgtd_lifx_gateway_list lgtd_lifx_gateways = - LIST_HEAD_INITIALIZER(&lgtd_lifx_gateways); - -+struct lgtd_listen_list lgtd_listeners = -+ SLIST_HEAD_INITIALIZER(&lgtd_listeners); -+ - struct lgtd_lifx_gateway * - lgtd_tests_insert_mock_gateway(int id) - { -@@ -41,6 +47,8 @@ - - LIST_INSERT_HEAD(&lgtd_lifx_gateways, gw, link); - -+ LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(gateways, 1); -+ - return gw; - } - -@@ -118,6 +126,17 @@ - return site; - } - -+struct lgtd_listen * -+lgtd_tests_insert_mock_listener(const char *addr, const char *port) -+{ -+ struct lgtd_listen *listener = calloc(1, sizeof(*listener)); -+ listener->addr = addr; -+ listener->port = port; -+ SLIST_INSERT_HEAD(&lgtd_listeners, listener, link); -+ -+ return listener; -+} -+ - char * - lgtd_tests_make_temp_dir(void) - { -diff --git a/tests/core/tests_utils.h b/tests/core/tests_utils.h ---- a/tests/core/tests_utils.h -+++ b/tests/core/tests_utils.h -@@ -40,3 +40,4 @@ - struct lgtd_lifx_site *lgtd_tests_add_tag_to_gw(struct lgtd_lifx_tag *, - struct lgtd_lifx_gateway *, - int); -+struct lgtd_listen *lgtd_tests_insert_mock_listener(const char *addr, const char *port); -diff --git a/tests/lifx/tests_shims.c b/tests/lifx/tests_shims.c ---- a/tests/lifx/tests_shims.c -+++ b/tests/lifx/tests_shims.c -@@ -37,3 +37,8 @@ - return ntohs(in6_peer->sin6_port); - } - } -+ -+void -+lgtd_daemon_update_proctitle(void) -+{ -+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/fix_crash_in_get_light_state_when_tag_does_not_exist.patch Mon Aug 03 01:18:55 2015 -0700 @@ -0,0 +1,31 @@ +# HG changeset patch +# Parent 53f4cb08f582e3657c896138823cf08ac2dcccf8 + +diff --git a/core/proto.c b/core/proto.c +--- a/core/proto.c ++++ b/core/proto.c +@@ -209,10 +209,20 @@ + bool comma = false; + int tag_id; + LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, bulb->state.tags) { +- LGTD_CLIENT_WRITE_STRING(client, comma ? ",\"" : "\""); +- LGTD_CLIENT_WRITE_STRING(client, bulb->gw->tags[tag_id]->label); +- LGTD_CLIENT_WRITE_STRING(client, "\""); +- comma = true; ++ if (LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id) & bulb->gw->tag_ids) { ++ lgtd_client_write_string(client, comma ? ",\"" : "\""); ++ lgtd_client_write_string(client, bulb->gw->tags[tag_id]->label); ++ lgtd_client_write_string(client, "\""); ++ comma = true; ++ } else { ++ lgtd_warnx( ++ "tag_id %d on bulb %.*s (%s) doesn't " ++ "exist on gw [%s]:%hu (site %s)", ++ tag_id, (int)sizeof(bulb->state.label), bulb->state.label, ++ lgtd_addrtoa(bulb->addr), bulb->gw->ip_addr, bulb->gw->port, ++ lgtd_addrtoa(bulb->gw->site.as_array) ++ ); ++ } + } + + lgtd_client_write_string(
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/fix_lifx_wire_float_endian_functions_naming.patch Mon Aug 03 01:18:55 2015 -0700 @@ -0,0 +1,59 @@ +# HG changeset patch +# Parent 80fb374d7109e03c44f049a2d7a845abf00663e1 + +diff --git a/lifx/wire_proto.c b/lifx/wire_proto.c +--- a/lifx/wire_proto.c ++++ b/lifx/wire_proto.c +@@ -382,7 +382,7 @@ + pkt->brightness = htole16(pkt->brightness); + pkt->kelvin = htole16(pkt->kelvin); + pkt->period = htole32(pkt->period); +- pkt->cycles = lifx_wire_htolefloat(pkt->cycles); ++ pkt->cycles = lgtd_lifx_wire_htolefloat(pkt->cycles); + pkt->skew_ratio = htole16(pkt->skew_ratio); + } + +diff --git a/lifx/wire_proto.h b/lifx/wire_proto.h +--- a/lifx/wire_proto.h ++++ b/lifx/wire_proto.h +@@ -26,14 +26,14 @@ + typedef float floatle_t; + + static inline floatle_t +-lifx_wire_htolefloat(float f) ++lgtd_lifx_wire_htolefloat(float f) + { + *(uint32_t *)&f = htole32(*(uint32_t *)&f); + return f; + } + + static inline floatle_t +-lifx_wire_lefloattoh(float f) ++lgtd_lifx_wire_lefloattoh(float f) + { + *(uint32_t *)&f = le32toh(*(uint32_t *)&f); + return f; +diff --git a/tests/core/proto/test_proto_set_waveform.c b/tests/core/proto/test_proto_set_waveform.c +--- a/tests/core/proto/test_proto_set_waveform.c ++++ b/tests/core/proto/test_proto_set_waveform.c +@@ -37,7 +37,7 @@ + int brightness = le16toh(waveform->brightness); + int kelvin = le16toh(waveform->kelvin); + int period = le32toh(waveform->period); +- float cycles = lifx_wire_lefloattoh(waveform->cycles); ++ float cycles = lgtd_lifx_wire_lefloattoh(waveform->cycles); + int skew_ratio = le16toh(waveform->skew_ratio); + + if (waveform_type != LGTD_LIFX_WAVEFORM_SAW) { +diff --git a/tests/core/proto/test_proto_set_waveform_on_routing_error.c b/tests/core/proto/test_proto_set_waveform_on_routing_error.c +--- a/tests/core/proto/test_proto_set_waveform_on_routing_error.c ++++ b/tests/core/proto/test_proto_set_waveform_on_routing_error.c +@@ -37,7 +37,7 @@ + int brightness = le16toh(waveform->brightness); + int kelvin = le16toh(waveform->kelvin); + int period = le32toh(waveform->period); +- float cycles = lifx_wire_lefloattoh(waveform->cycles); ++ float cycles = lgtd_lifx_wire_lefloattoh(waveform->cycles); + int skew_ratio = le16toh(waveform->skew_ratio); + + if (waveform_type != LGTD_LIFX_WAVEFORM_SAW) {
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/fix_set_power_state.patch Mon Aug 03 01:18:55 2015 -0700 @@ -0,0 +1,32 @@ +# HG changeset patch +# Parent 46aa876b47a3e3c07b4855aa4cbff94acba75e16 + +diff --git a/lifx/wire_proto.c b/lifx/wire_proto.c +--- a/lifx/wire_proto.c ++++ b/lifx/wire_proto.c +@@ -341,16 +341,22 @@ + pkt->brightness = le16toh(pkt->brightness); + pkt->kelvin = le16toh(pkt->kelvin); + pkt->dim = le16toh(pkt->dim); +- pkt->power = le16toh(pkt->power); ++ // The bulbs actually return power values between 0 and 0xffff, not sure ++ // what the intermediate values mean, let's pull them down to 0: ++ if (pkt->power != LGTD_LIFX_POWER_ON) { ++ pkt->power = LGTD_LIFX_POWER_OFF; ++ } + pkt->tags = le64toh(pkt->tags); + } + + void + lgtd_lifx_wire_decode_power_state(struct lgtd_lifx_packet_power_state *pkt) + { +- (void)pkt; ++ assert(pkt); + +- assert(pkt); ++ if (pkt->power != LGTD_LIFX_POWER_ON) { ++ pkt->power = LGTD_LIFX_POWER_OFF; ++ } + } + + void
--- a/fix_usage_and_version.patch Sun Aug 02 19:02:55 2015 -0700 +++ b/fix_usage_and_version.patch Mon Aug 03 01:18:55 2015 -0700 @@ -1,22 +1,18 @@ # HG changeset patch -# Parent 16c122197bf6ffffef579622c276b66b89e197b3 -Display the usage when no arguments are passed in and fix -V +# Parent 298fe56d98508afd0b2d3098264f4661d5908fe5 diff --git a/core/lightsd.c b/core/lightsd.c --- a/core/lightsd.c +++ b/core/lightsd.c -@@ -138,8 +138,8 @@ - { - printf( - "Usage: %s ...\n\n" -- " [-l,--listen addr:port [-l ...]]\n" -- " [-c,--comand-pipe /command/fifo -[c ...]]\n" -+ " [-l,--listen addr:port [+]]\n" -+ " [-c,--comand-pipe /command/fifo [+]]\n" - " [-f,--foreground]\n" - " [-t,--no-timestamps]\n" - " [-h,--help]\n" -@@ -217,6 +217,10 @@ +@@ -138,6 +138,7 @@ + " [-v,--verbosity debug|info|warning|error]\n", + progname + ); ++ lgtd_cleanup(); + exit(0); + } + +@@ -159,6 +160,10 @@ }; const char short_opts[] = "l:c:fthv:V";
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ignore_duplicate_listening_address.patch Mon Aug 03 01:18:55 2015 -0700 @@ -0,0 +1,36 @@ +# HG changeset patch +# Parent d8e34f128c677442c5c047793207dbc1664b6c2e + +diff --git a/core/listen.c b/core/listen.c +--- a/core/listen.c ++++ b/core/listen.c +@@ -20,6 +20,7 @@ + #include <err.h> + #include <stdbool.h> + #include <stdlib.h> ++#include <string.h> + #include <unistd.h> + + #include <event2/listener.h> +@@ -76,6 +77,13 @@ + assert(addr); + assert(port); + ++ struct lgtd_listen *listener; ++ SLIST_FOREACH(listener, &lgtd_listeners, link) { ++ if (!strcmp(listener->addr, addr) && listener->port == port) { ++ return true; ++ } ++ } ++ + struct evutil_addrinfo *res = NULL, hints = { + .ai_family = AF_UNSPEC, + .ai_socktype = SOCK_STREAM, +@@ -91,7 +99,6 @@ + return false; + } + +- struct lgtd_listen *listener; + struct evconnlistener *evlistener; + for (struct evutil_addrinfo *it = res; it; it = it->ai_next) { + evlistener = NULL;
--- a/ignore_duplicated_listening_addresses.patch Sun Aug 02 19:02:55 2015 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,37 +0,0 @@ -# HG changeset patch -# Parent 9e1c55d17b4004d7b2841177c8aec6a37391090d -Ignore duplicated listen addresses - -diff --git a/core/listen.c b/core/listen.c ---- a/core/listen.c -+++ b/core/listen.c -@@ -20,6 +20,7 @@ - #include <err.h> - #include <stdbool.h> - #include <stdlib.h> -+#include <string.h> - #include <unistd.h> - - #include <event2/listener.h> -@@ -76,6 +77,13 @@ - assert(addr); - assert(port); - -+ struct lgtd_listen *listener; -+ SLIST_FOREACH(listener, &lgtd_listeners, link) { -+ if (!strcmp(listener->addr, addr) && listener->port == port) { -+ return true; -+ } -+ } -+ - struct evutil_addrinfo *res = NULL, hints = { - .ai_family = AF_UNSPEC, - .ai_socktype = SOCK_STREAM, -@@ -91,7 +99,6 @@ - return false; - } - -- struct lgtd_listen *listener; - struct evconnlistener *evlistener; - for (struct evutil_addrinfo *it = res; it; it = it->ai_next) { - evlistener = NULL;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/leftovers.patch Mon Aug 03 01:18:55 2015 -0700 @@ -0,0 +1,227 @@ +# HG changeset patch +# Parent 2be7ea678eaadacc07b4e7b04af720409b5d2fca + +diff --git a/README.rst b/README.rst +--- a/README.rst ++++ b/README.rst +@@ -32,8 +32,8 @@ + - tag/untag (group/ungroup bulbs together); + - toggle (power on if off and vice-versa, coming up). + +-The JSON-RPC interface works on top on IPv4/v6, over a command (named) pipe and +-Unix sockets (coming up). ++The JSON-RPC interface works on top of IPv4/v6 Unix sockets (coming up) or over ++a command pipe (named pipe, see mkfifo(1)). + + lightsd can target single or multiple bulbs at once: + +diff --git a/core/jsonrpc.c b/core/jsonrpc.c +--- a/core/jsonrpc.c ++++ b/core/jsonrpc.c +@@ -129,7 +129,7 @@ + return c == '-' || (c >= '0' && c <= '9'); + } + +-static bool __attribute__((unused)) ++static bool + lgtd_jsonrpc_type_bool(const jsmntok_t *t, const char *json) + { + if (t->type != JSMN_PRIMITIVE) { +diff --git a/core/lightsd.h b/core/lightsd.h +--- a/core/lightsd.h ++++ b/core/lightsd.h +@@ -17,6 +17,8 @@ + + #pragma once + ++struct sockaddr_storage; ++ + #ifndef __attribute__ + # define __atttribute__(e) + #endif +diff --git a/core/log.c b/core/log.c +--- a/core/log.c ++++ b/core/log.c +@@ -17,6 +17,7 @@ + + #include <sys/tree.h> + #include <sys/time.h> ++#include <sys/types.h> + #include <arpa/inet.h> + #include <assert.h> + #include <endian.h> +@@ -26,6 +27,7 @@ + #include <stdbool.h> + #include <stdint.h> + #include <stdio.h> ++#include <stdlib.h> + #include <time.h> + + #if LGTD_HAVE_LIBBSD +@@ -175,6 +177,6 @@ + case EVENT_LOG_MSG: lgtd_info("%s", msg); break; + case EVENT_LOG_WARN: lgtd_warnx("%s", msg) break; + case EVENT_LOG_ERR: lgtd_warnx("%s", msg); break; +- default: break; ++ default: break; + } + } +diff --git a/core/router.c b/core/router.c +--- a/core/router.c ++++ b/core/router.c +@@ -61,6 +61,8 @@ + ); + assert(pkt_infos); + ++ pkt_infos->encode(pkt); ++ + lgtd_lifx_gateway_enqueue_packet( + gw, &hdr, pkt_type, pkt, pkt_infos->size + ); +@@ -372,8 +374,8 @@ + void + lgtd_router_device_list_free(struct lgtd_router_device_list *devices) + { +- assert(devices); +- +- lgtd_router_clear_device_list(devices); +- free(devices); ++ if (devices) { ++ lgtd_router_clear_device_list(devices); ++ free(devices); ++ } + } +diff --git a/lifx/gateway.c b/lifx/gateway.c +--- a/lifx/gateway.c ++++ b/lifx/gateway.c +@@ -150,13 +150,6 @@ + if (type == LGTD_LIFX_GET_TAG_LABELS) { + gw->pending_refresh_req = false; + } +- if (lgtd_opts.verbosity <= LGTD_DEBUG) { +- const struct lgtd_lifx_packet_infos *pkt_infos = +- lgtd_lifx_wire_get_packet_infos(type); +- lgtd_debug( +- "%s --> [%s]:%hu", pkt_infos->name, gw->ip_addr, gw->port +- ); +- } + gw->pkt_ring[gw->pkt_ring_tail].type = 0; + LGTD_LIFX_GATEWAY_INC_MESSAGE_RING_INDEX(gw->pkt_ring_tail); + gw->pkt_ring_full = false; +diff --git a/lifx/gateway.h b/lifx/gateway.h +--- a/lifx/gateway.h ++++ b/lifx/gateway.h +@@ -21,7 +21,7 @@ + // according to my own tests, aggressively polling a bulb doesn't raise its + // consumption at all (and it's interesting to note that a turned off bulb + // still draw about 2W in ZigBee and about 3W in WiFi). +-enum { LGTD_LIFX_GATEWAY_MIN_REFRESH_INTERVAL_MSECS = 200 }; ++enum { LGTD_LIFX_GATEWAY_MIN_REFRESH_INTERVAL_MSECS = 1000 }; + + // You can't send more than one lifx packet per UDP datagram. + enum { LGTD_LIFX_GATEWAY_PACKET_RING_SIZE = 16 }; +diff --git a/lifx/timer.h b/lifx/timer.h +--- a/lifx/timer.h ++++ b/lifx/timer.h +@@ -17,11 +17,11 @@ + + #pragma once + +-enum { LGTD_LIFX_TIMER_WATCHDOG_INTERVAL_MSECS = 200 }; ++enum { LGTD_LIFX_TIMER_WATCHDOG_INTERVAL_MSECS = 500 }; + enum { LGTD_LIFX_TIMER_ACTIVE_DISCOVERY_INTERVAL_MSECS = 2000 }; + enum { LGTD_LIFX_TIMER_PASSIVE_DISCOVERY_INTERVAL_MSECS = 10000 }; +-enum { LGTD_LIFX_TIMER_DEVICE_TIMEOUT_MSECS = 2000 }; +-enum { LGTD_LIFX_TIMER_DEVICE_FORCE_REFRESH_MSECS = 600 }; ++enum { LGTD_LIFX_TIMER_DEVICE_TIMEOUT_MSECS = 4000 }; ++enum { LGTD_LIFX_TIMER_DEVICE_FORCE_REFRESH_MSECS = 2000 }; + + bool lgtd_lifx_timer_setup(void); + void lgtd_lifx_timer_close(void); +diff --git a/tests/core/proto/CMakeLists.txt b/tests/core/proto/CMakeLists.txt +--- a/tests/core/proto/CMakeLists.txt ++++ b/tests/core/proto/CMakeLists.txt +@@ -16,11 +16,11 @@ + ${CMAKE_CURRENT_SOURCE_DIR}/../tests_utils.c + ) + +-FUNCTION(ADD_ROUTER_TEST TEST_SOURCE) ++FUNCTION(ADD_PROTO_TEST TEST_SOURCE) + ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_core_proto) + ENDFUNCTION() + + FILE(GLOB TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test_*.c") + FOREACH(TEST ${TESTS}) +- ADD_ROUTER_TEST(${TEST}) ++ ADD_PROTO_TEST(${TEST}) + ENDFOREACH() +diff --git a/tests/core/proto/test_proto_get_light_state.c b/tests/core/proto/test_proto_get_light_state.c +--- a/tests/core/proto/test_proto_get_light_state.c ++++ b/tests/core/proto/test_proto_get_light_state.c +@@ -31,6 +31,9 @@ + static struct lgtd_router_device_list devices = + SLIST_HEAD_INITIALIZER(&devices); + ++ static struct lgtd_lifx_gateway gw_bulb_1 = { ++ .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_1.bulbs) ++ }; + static struct lgtd_lifx_bulb bulb_1 = { + .addr = { 1, 2, 3, 4, 5 }, + .state = { +@@ -41,7 +44,8 @@ + .label = "wave", + .power = LGTD_LIFX_POWER_ON, + .tags = 0 +- } ++ }, ++ .gw = &gw_bulb_1 + }; + static struct lgtd_router_device device_1 = { .device = &bulb_1 }; + SLIST_INSERT_HEAD(&devices, &device_1, link); +@@ -105,7 +109,7 @@ + 1, + "%d bytes written, expected %lu " + "(got %.*s instead of %s)", +- client_write_buf_idx, sizeof(expected) - 1, ++ client_write_buf_idx, sizeof(expected) - 1UL, + client_write_buf_idx, client_write_buf, expected + ); + } +diff --git a/tests/core/proto/test_proto_get_light_state_empty_device_list.c b/tests/core/proto/test_proto_get_light_state_empty_device_list.c +--- a/tests/core/proto/test_proto_get_light_state_empty_device_list.c ++++ b/tests/core/proto/test_proto_get_light_state_empty_device_list.c +@@ -47,7 +47,7 @@ + if (client_write_buf_idx != sizeof(expected) - 1) { + lgtd_errx( + 1, "%d bytes written, expected %lu", +- client_write_buf_idx, sizeof(expected) - 1 ++ client_write_buf_idx, sizeof(expected) - 1UL + ); + } + +diff --git a/tests/core/tests_shims.c b/tests/core/tests_shims.c +--- a/tests/core/tests_shims.c ++++ b/tests/core/tests_shims.c +@@ -24,7 +24,9 @@ + .verbosity = LGTD_DEBUG + }; + +-struct event_base *lgtd_ev_base = NULL; ++struct event_base *lgtd_ev_base = (void *)0x1234; ++ ++const char *lgtd_binds = NULL; + + void + lgtd_cleanup(void) +diff --git a/tests/lifx/tests_shims.c b/tests/lifx/tests_shims.c +--- a/tests/lifx/tests_shims.c ++++ b/tests/lifx/tests_shims.c +@@ -15,6 +15,8 @@ + + struct event_base *lgtd_ev_base = NULL; + ++const char *lgtd_binds = NULL; ++ + void + lgtd_cleanup(void) + {
--- a/series Sun Aug 02 19:02:55 2015 -0700 +++ b/series Mon Aug 03 01:18:55 2015 -0700 @@ -1,6 +1,10 @@ -add_daemonization_with_nice_proctitle.patch -tag_untag.patch -ignore_duplicated_listening_addresses.patch +ignore_duplicate_listening_address.patch add_command_pipe.patch +add_daemon_module.patch fix_usage_and_version.patch -daemon_module.patch +add_tag_and_untag.patch +update_readme.patch +fix_set_power_state.patch +fix_crash_in_get_light_state_when_tag_does_not_exist.patch +fix_lifx_wire_float_endian_functions_naming.patch +leftovers.patch
--- a/tag_untag.patch Sun Aug 02 19:02:55 2015 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,4100 +0,0 @@ -# HG changeset patch -# Parent 2f8c26d59f71c5c67b0252b2220df91d13e52321 -Add the ability to tag (group) or untag (ungroup) bulbs - -diff --git a/CMakeLists.txt b/CMakeLists.txt ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -1,4 +1,4 @@ --CMAKE_MINIMUM_REQUIRED(VERSION 2.8) -+CMAKE_MINIMUM_REQUIRED(VERSION 2.8.11) # first version TARGET_INCLUDE_DIRECTORIES - - PROJECT(LIGHTSD C) - -@@ -35,7 +35,7 @@ - INCLUDE(AddAllSubdirectories) - INCLUDE(AddTestFromSources) - --SET(CMAKE_C_FLAGS "-pipe -Wextra -Wall -Wstrict-prototypes -std=c99") -+SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pipe -Wextra -Wall -Wstrict-prototypes -std=c99") - - ADD_DEFINITIONS( - # Only relevant for the GNU libc: -@@ -58,10 +58,11 @@ - ENDIF () - - INCLUDE_DIRECTORIES( -- ${LIGHTSD_SOURCE_DIR}/compat/generic - ${LIGHTSD_BINARY_DIR}/compat -+ ${LIGHTSD_BINARY_DIR}/compat/generic - ) - -+ADD_SUBDIRECTORY(compat) - ADD_SUBDIRECTORY(core) - ADD_SUBDIRECTORY(lifx) - ADD_SUBDIRECTORY(tests) -diff --git a/CMakeScripts/AddTestFromSources.cmake b/CMakeScripts/AddTestFromSources.cmake ---- a/CMakeScripts/AddTestFromSources.cmake -+++ b/CMakeScripts/AddTestFromSources.cmake -@@ -1,11 +1,11 @@ --FUNCTION(ADD_TEST_FROM_C_SOURCES TEST_SOURCE TEST_LIB) -+FUNCTION(ADD_TEST_FROM_C_SOURCES TEST_SOURCE) - STRING(LENGTH ${TEST_SOURCE} TEST_NAME_LEN) - STRING(LENGTH "test_" PREFIX_LEN) - MATH(EXPR TEST_NAME_LEN "${TEST_NAME_LEN} - 2 - ${PREFIX_LEN}") - STRING(SUBSTRING ${ARGV0} ${PREFIX_LEN} ${TEST_NAME_LEN} TEST_NAME) -- ADD_EXECUTABLE(${TEST_NAME} ${TEST_SOURCE} ${ARGN}) -- IF (TEST_LIB) -- TARGET_LINK_LIBRARIES(${TEST_NAME} ${TEST_LIB}) -+ ADD_EXECUTABLE(${TEST_NAME} ${TEST_SOURCE}) -+ IF (ARGN) -+ TARGET_LINK_LIBRARIES(${TEST_NAME} ${ARGN}) - ENDIF () - ADD_TEST(test_${TEST_NAME} ${TEST_NAME}) - ENDFUNCTION() -diff --git a/README.rst b/README.rst ---- a/README.rst -+++ b/README.rst -@@ -29,8 +29,7 @@ - - set_light_from_hsbk; - - set_waveform (change the light according to a function like SAW or SINE); - - get_light_state; --- tag/untag (group/ungroup bulbs together, coming up: need unit & regression -- tests); -+- tag/untag (group/ungroup bulbs together); - - toggle (power on if off and vice-versa, coming up). - - The JSON-RPC interface works on top on IPv4/v6, over a command (named) pipe -@@ -104,4 +103,22 @@ - - Use the ``-f`` option to run lightsd in the foreground. - -+Known issues -+------------ -+ -+The grouping (tagging) code of the LIFX White 800 is bugged: after a tagging -+operation the LIFX White 800 keep saying it has no tags. Reboot the bulb to make -+the tags appears. -+ -+Power ON/OFF are the only commands with auto-retry, i.e: lightsd will keep -+sending the command to the bulb until its state changes. This is not implemented -+(yet) for ``set_light_from_hsbk`` and ``set_waveform``. -+ -+While lighsd appears to be pretty stable, if you want to run lightsd in the -+background, I recommend doing it in a processor supervisor (e.g: Supervisor_) -+that can restart lightsd in case of crash. Otherwise, please send me your crash -+reports! -+ -+.. _Supervisor: http://www.supervisord.org/ -+ - .. vim: set tw=80 spelllang=en spell: -diff --git a/compat/CMakeLists.txt b/compat/CMakeLists.txt -new file mode 100644 ---- /dev/null -+++ b/compat/CMakeLists.txt -@@ -0,0 +1,1 @@ -+ADD_SUBDIRECTORY(generic) -diff --git a/compat/generic/CMakeLists.txt b/compat/generic/CMakeLists.txt -new file mode 100644 ---- /dev/null -+++ b/compat/generic/CMakeLists.txt -@@ -0,0 +1,1 @@ -+FILE(COPY sys DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) -diff --git a/core/jsonrpc.c b/core/jsonrpc.c ---- a/core/jsonrpc.c -+++ b/core/jsonrpc.c -@@ -977,6 +977,90 @@ - lgtd_proto_target_list_clear(&targets); - } - -+static void -+lgtd_jsonrpc_check_and_call_proto_tag_or_untag(struct lgtd_client *client, -+ void (*lgtd_proto_fn)(struct lgtd_client *, -+ const struct lgtd_proto_target_list *, -+ const char *)) -+ -+{ -+ struct lgtd_jsonrpc_target_args { -+ const jsmntok_t *target; -+ int target_ntokens; -+ const jsmntok_t *tag; -+ } params = { NULL, 0, NULL }; -+ static const struct lgtd_jsonrpc_node schema[] = { -+ LGTD_JSONRPC_NODE( -+ "target", -+ offsetof(struct lgtd_jsonrpc_target_args, target), -+ offsetof(struct lgtd_jsonrpc_target_args, target_ntokens), -+ lgtd_jsonrpc_type_string_number_or_array, -+ false -+ ), -+ LGTD_JSONRPC_NODE( -+ "tag", -+ offsetof(struct lgtd_jsonrpc_target_args, tag), -+ -1, -+ lgtd_jsonrpc_type_string, -+ false -+ ) -+ }; -+ -+ struct lgtd_jsonrpc_request *req = client->current_request; -+ bool ok = lgtd_jsonrpc_extract_and_validate_params_against_schema( -+ ¶ms, -+ schema, -+ LGTD_ARRAY_SIZE(schema), -+ req->params, -+ req->params_ntokens, -+ client->json -+ ); -+ if (!ok) { -+ lgtd_jsonrpc_send_error( -+ client, LGTD_JSONRPC_INVALID_PARAMS, "Invalid parameters" -+ ); -+ return; -+ } -+ -+ struct lgtd_proto_target_list targets = SLIST_HEAD_INITIALIZER(&targets); -+ ok = lgtd_jsonrpc_build_target_list( -+ &targets, client, params.target, params.target_ntokens -+ ); -+ if (!ok) { -+ return; -+ } -+ -+ char *tag = strndup( -+ &client->json[params.tag->start], LGTD_JSONRPC_TOKEN_LEN(params.tag) -+ ); -+ if (!tag) { -+ lgtd_warn("can't allocate a tag"); -+ lgtd_jsonrpc_send_error( -+ client, LGTD_JSONRPC_INTERNAL_ERROR, "Can't allocate memory" -+ ); -+ goto error_strdup; -+ } -+ -+ lgtd_proto_fn(client, &targets, tag); -+ -+ free(tag); -+ -+error_strdup: -+ lgtd_proto_target_list_clear(&targets); -+} -+ -+static void -+lgtd_jsonrpc_check_and_call_tag(struct lgtd_client *client) -+{ -+ return lgtd_jsonrpc_check_and_call_proto_tag_or_untag(client, lgtd_proto_tag); -+} -+ -+static void -+lgtd_jsonrpc_check_and_call_untag(struct lgtd_client *client) -+{ -+ return lgtd_jsonrpc_check_and_call_proto_tag_or_untag(client, lgtd_proto_untag); -+} -+ - void - lgtd_jsonrpc_dispatch_request(struct lgtd_client *client, int parsed) - { -@@ -1001,6 +1085,14 @@ - LGTD_JSONRPC_METHOD( - "get_light_state", 1, // t - lgtd_jsonrpc_check_and_call_get_light_state -+ ), -+ LGTD_JSONRPC_METHOD( -+ "tag", 2, // t, tag -+ lgtd_jsonrpc_check_and_call_tag -+ ), -+ LGTD_JSONRPC_METHOD( -+ "untag", 2, // t, tag -+ lgtd_jsonrpc_check_and_call_untag - ) - }; - -diff --git a/core/proto.c b/core/proto.c ---- a/core/proto.c -+++ b/core/proto.c -@@ -209,10 +209,20 @@ - bool comma = false; - int tag_id; - LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, bulb->state.tags) { -- LGTD_CLIENT_WRITE_STRING(client, comma ? ",\"" : "\""); -- LGTD_CLIENT_WRITE_STRING(client, bulb->gw->tags[tag_id]->label); -- LGTD_CLIENT_WRITE_STRING(client, "\""); -- comma = true; -+ if (LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id) & bulb->gw->tag_ids) { -+ LGTD_CLIENT_WRITE_STRING(client, comma ? ",\"" : "\""); -+ LGTD_CLIENT_WRITE_STRING(client, bulb->gw->tags[tag_id]->label); -+ LGTD_CLIENT_WRITE_STRING(client, "\""); -+ comma = true; -+ } else { -+ lgtd_warnx( -+ "tag_id %d on bulb %.*s (%s) doesn't " -+ "exist on gw [%s]:%hu (site %s)", -+ tag_id, (int)sizeof(bulb->state.label), bulb->state.label, -+ lgtd_addrtoa(bulb->addr), bulb->gw->ip_addr, bulb->gw->port, -+ lgtd_addrtoa(bulb->gw->site.as_array) -+ ); -+ } - } - - LGTD_CLIENT_WRITE_STRING( -@@ -224,3 +234,145 @@ - - lgtd_router_device_list_free(devices); - } -+ -+void -+lgtd_proto_tag(struct lgtd_client *client, -+ const struct lgtd_proto_target_list *targets, -+ const char *tag_label) -+{ -+ assert(client); -+ assert(targets); -+ assert(tag_label); -+ -+ struct lgtd_router_device_list *devices; -+ devices = lgtd_router_targets_to_devices(targets); -+ if (!devices) { -+ goto error_tag_alloc; -+ } -+ -+ struct lgtd_lifx_tag *tag = lgtd_lifx_tagging_find_tag(tag_label); -+ if (!tag) { -+ tag = lgtd_lifx_tagging_allocate_tag(tag_label); -+ if (!tag) { -+ goto error_tag_alloc; -+ } -+ lgtd_info("created tag [%s]", tag_label); -+ } -+ -+ struct lgtd_router_device *device; -+ struct lgtd_lifx_site *site; -+ -+ // Loop over the devices and do allocations first, this makes error -+ // handling easier (since you can't rollback enqueued packets) and build -+ // the list of affected gateways so we can do SET_TAG_LABELS: -+ SLIST_FOREACH(device, devices, link) { -+ struct lgtd_lifx_gateway *gw = device->device->gw; -+ int tag_id = lgtd_lifx_gateway_get_tag_id(gw, tag); -+ if (tag_id == -1) { -+ tag_id = lgtd_lifx_gateway_allocate_tag_id(gw, -1, tag_label); -+ if (tag_id == -1) { -+ goto error_site_alloc; -+ } -+ } -+ } -+ -+ // SET_TAG_LABELS, this is idempotent, do it everytime so we can recover -+ // from any bad state: -+ LIST_FOREACH(site, &tag->sites, link) { -+ int tag_id = site->tag_id; -+ assert(tag_id > -1 && tag_id < LGTD_LIFX_GATEWAY_MAX_TAGS); -+ struct lgtd_lifx_packet_tag_labels pkt = { .tags = 0 }; -+ pkt.tags = LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id); -+ strncpy(pkt.label, tag_label, sizeof(pkt.label) - 1); -+ lgtd_lifx_wire_encode_tag_labels(&pkt); -+ bool enqueued = lgtd_lifx_gateway_send_to_site( -+ site->gw, LGTD_LIFX_SET_TAG_LABELS, &pkt -+ ); -+ if (!enqueued) { -+ goto error_site_alloc; -+ } -+ lgtd_info( -+ "created tag [%s] with id %d on gw [%s]:%hu", -+ tag_label, tag_id, site->gw->ip_addr, site->gw->port -+ ); -+ } -+ -+ // Finally SET_TAGS on the devices: -+ SLIST_FOREACH(device, devices, link) { -+ struct lgtd_lifx_bulb *bulb = device->device; -+ int tag_id = lgtd_lifx_gateway_get_tag_id(bulb->gw, tag); -+ assert(tag_id > -1 && tag_id < LGTD_LIFX_GATEWAY_MAX_TAGS); -+ int tag_value = LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id); -+ if (!(bulb->state.tags & tag_value)) { -+ struct lgtd_lifx_packet_tags pkt; -+ pkt.tags = bulb->state.tags | tag_value; -+ lgtd_lifx_wire_encode_tags(&pkt); -+ lgtd_router_send_to_device(bulb, LGTD_LIFX_SET_TAGS, &pkt); -+ } -+ } -+ -+ SEND_RESULT(client, true); -+ goto fini; -+ -+error_site_alloc: -+ if (LIST_EMPTY(&tag->sites)) { -+ lgtd_lifx_tagging_deallocate_tag(tag); -+ } else { // tagging_decref will deallocate the tag for us: -+ struct lgtd_lifx_site *next_site; -+ LIST_FOREACH_SAFE(site, &tag->sites, link, next_site) { -+ lgtd_lifx_gateway_deallocate_tag_id(site->gw, site->tag_id); -+ } -+ } -+error_tag_alloc: -+ lgtd_client_send_error( -+ client, LGTD_CLIENT_INTERNAL_ERROR, "couldn't allocate new tag" -+ ); -+fini: -+ lgtd_router_device_list_free(devices); -+ return; -+} -+ -+void -+lgtd_proto_untag(struct lgtd_client *client, -+ const struct lgtd_proto_target_list *targets, -+ const char *tag_label) -+{ -+ assert(client); -+ assert(targets); -+ assert(tag_label); -+ -+ struct lgtd_lifx_tag *tag = lgtd_lifx_tagging_find_tag(tag_label); -+ if (!tag) { -+ SEND_RESULT(client, true); -+ return; -+ } -+ -+ struct lgtd_router_device_list *devices = NULL; -+ devices = lgtd_router_targets_to_devices(targets); -+ if (!devices) { -+ lgtd_client_send_error( -+ client, LGTD_CLIENT_INTERNAL_ERROR, "couldn't allocate memory" -+ ); -+ return; -+ } -+ -+ struct lgtd_router_device *device; -+ SLIST_FOREACH(device, devices, link) { -+ struct lgtd_lifx_bulb *bulb = device->device; -+ struct lgtd_lifx_gateway *gw = bulb->gw; -+ int tag_id = lgtd_lifx_gateway_get_tag_id(gw, tag); -+ if (tag_id != -1) { -+ int tag_value = LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id); -+ if (bulb->state.tags & tag_value) { -+ struct lgtd_lifx_packet_tags pkt; -+ pkt.tags = bulb->state.tags & ~tag_value; -+ lgtd_lifx_wire_encode_tags(&pkt); -+ lgtd_router_send_to_device(bulb, LGTD_LIFX_SET_TAGS, &pkt); -+ } -+ } -+ } -+ -+ SEND_RESULT(client, true); -+ -+ lgtd_router_device_list_free(devices); -+} -diff --git a/core/proto.h b/core/proto.h ---- a/core/proto.h -+++ b/core/proto.h -@@ -39,3 +39,5 @@ - void lgtd_proto_power_on(struct lgtd_client *, const struct lgtd_proto_target_list *); - void lgtd_proto_power_off(struct lgtd_client *, const struct lgtd_proto_target_list *); - void lgtd_proto_get_light_state(struct lgtd_client *, const struct lgtd_proto_target_list *); -+void lgtd_proto_tag(struct lgtd_client *, const struct lgtd_proto_target_list *, const char *); -+void lgtd_proto_untag(struct lgtd_client *, const struct lgtd_proto_target_list *, const char *); -diff --git a/core/router.c b/core/router.c ---- a/core/router.c -+++ b/core/router.c -@@ -61,6 +61,8 @@ - ); - assert(pkt_infos); - -+ pkt_infos->encode(pkt); -+ - lgtd_lifx_gateway_enqueue_packet( - gw, &hdr, pkt_type, pkt, pkt_infos->size - ); -@@ -372,8 +374,8 @@ - void - lgtd_router_device_list_free(struct lgtd_router_device_list *devices) - { -- assert(devices); -- -- lgtd_router_clear_device_list(devices); -- free(devices); -+ if (devices) { -+ lgtd_router_clear_device_list(devices); -+ free(devices); -+ } - } -diff --git a/lifx/bulb.c b/lifx/bulb.c ---- a/lifx/bulb.c -+++ b/lifx/bulb.c -@@ -76,12 +76,29 @@ - assert(bulb); - assert(bulb->gw); - -+#ifndef NDEBUG -+ // FIXME: Yeah, so an unit test lgtd_lifx_gateway_remove_and_close_bulb -+ // would be better because it can be automated, but this looks so much -+ // easier to do and this code path is often exercised: -+ int tag_id; -+ LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, bulb->state.tags) { -+ int n = 0; -+ struct lgtd_lifx_bulb *gw_bulb; -+ SLIST_FOREACH(gw_bulb, &bulb->gw->bulbs, link_by_gw) { -+ assert(gw_bulb != bulb); -+ if (LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id) & gw_bulb->state.tags) { -+ n++; -+ } -+ } -+ assert(bulb->gw->tag_refcounts[tag_id] == n); -+ } -+#endif -+ - LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(bulbs, -1); - if (bulb->state.power == LGTD_LIFX_POWER_ON) { - LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(bulbs_powered_on, -1); - } - RB_REMOVE(lgtd_lifx_bulb_map, &lgtd_lifx_bulbs_table, bulb); -- SLIST_REMOVE(&bulb->gw->bulbs, bulb, lgtd_lifx_bulb, link_by_gw); - lgtd_info( - "closed bulb \"%.*s\" (%s) on [%s]:%hu", - LGTD_LIFX_LABEL_SIZE, -@@ -107,6 +124,8 @@ - ); - } - -+ lgtd_lifx_gateway_update_tag_refcounts(bulb->gw, bulb->state.tags, state->tags); -+ - bulb->last_light_state_at = received_at; - memcpy(&bulb->state, state, sizeof(bulb->state)); - } -@@ -124,3 +143,13 @@ - - bulb->state.power = power; - } -+ -+void -+lgtd_lifx_bulb_set_tags(struct lgtd_lifx_bulb *bulb, uint64_t tags) -+{ -+ assert(bulb); -+ -+ lgtd_lifx_gateway_update_tag_refcounts(bulb->gw, bulb->state.tags, tags); -+ -+ bulb->state.tags = tags; -+} -diff --git a/lifx/bulb.h b/lifx/bulb.h ---- a/lifx/bulb.h -+++ b/lifx/bulb.h -@@ -68,3 +68,4 @@ - const struct lgtd_lifx_light_state *, - lgtd_time_mono_t); - void lgtd_lifx_bulb_set_power_state(struct lgtd_lifx_bulb *, uint16_t); -+void lgtd_lifx_bulb_set_tags(struct lgtd_lifx_bulb *, uint64_t); -diff --git a/lifx/gateway.c b/lifx/gateway.c ---- a/lifx/gateway.c -+++ b/lifx/gateway.c -@@ -70,9 +70,9 @@ - lgtd_lifx_tagging_decref(gw->tags[i], gw); - } - } -- struct lgtd_lifx_bulb *bulb, *next_bulb; -- SLIST_FOREACH_SAFE(bulb, &gw->bulbs, link_by_gw, next_bulb) { -- lgtd_lifx_bulb_close(bulb); -+ while (!SLIST_EMPTY(&gw->bulbs)) { -+ struct lgtd_lifx_bulb *bulb = SLIST_FIRST(&gw->bulbs); -+ lgtd_lifx_gateway_remove_and_close_bulb(gw, bulb); - } - - lgtd_info( -@@ -82,6 +82,23 @@ - free(gw); - } - -+void -+lgtd_lifx_gateway_remove_and_close_bulb(struct lgtd_lifx_gateway *gw, -+ struct lgtd_lifx_bulb *bulb) -+{ -+ assert(gw); -+ assert(bulb); -+ -+ int tag_id; -+ LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, bulb->state.tags) { -+ assert(gw->tag_refcounts[tag_id] > 0); -+ gw->tag_refcounts[tag_id]--; -+ } -+ SLIST_REMOVE(&gw->bulbs, bulb, lgtd_lifx_bulb, link_by_gw); -+ -+ lgtd_lifx_bulb_close(bulb); -+} -+ - static void - lgtd_lifx_gateway_write_callback(evutil_socket_t socket, - short events, void *ctx) -@@ -132,13 +149,6 @@ - if (type == LGTD_LIFX_GET_TAG_LABELS) { - gw->pending_refresh_req = false; - } -- if (lgtd_opts.verbosity <= LGTD_DEBUG) { -- const struct lgtd_lifx_packet_infos *pkt_infos = -- lgtd_lifx_wire_get_packet_infos(type); -- lgtd_debug( -- "%s --> [%s]:%hu", pkt_infos->name, gw->ip_addr, gw->port -- ); -- } - gw->pkt_ring[gw->pkt_ring_tail].type = 0; - LGTD_LIFX_GATEWAY_INC_MESSAGE_RING_INDEX(gw->pkt_ring_tail); - gw->pkt_ring_full = false; -@@ -150,36 +160,77 @@ - } - } - -+static bool -+lgtd_lifx_gateway_send_to_site_impl(struct lgtd_lifx_gateway *gw, -+ enum lgtd_lifx_packet_type pkt_type, -+ const void *pkt, -+ const struct lgtd_lifx_packet_infos **pkt_infos) -+{ -+ assert(gw); -+ assert(pkt_infos); -+ -+ struct lgtd_lifx_packet_header hdr; -+ union lgtd_lifx_target target = { .addr = gw->site.as_array }; -+ *pkt_infos = lgtd_lifx_wire_setup_header( -+ &hdr, -+ LGTD_LIFX_TARGET_SITE, -+ target, -+ gw->site.as_array, -+ pkt_type -+ ); -+ assert(*pkt_infos); -+ -+ lgtd_lifx_gateway_enqueue_packet(gw, &hdr, pkt_type, pkt, (*pkt_infos)->size); -+ -+ return true; // FIXME, have real return values on the send paths... -+} -+ -+static bool -+lgtd_lifx_gateway_send_to_site_quiet(struct lgtd_lifx_gateway *gw, -+ enum lgtd_lifx_packet_type pkt_type, -+ const void *pkt) -+{ -+ -+ const struct lgtd_lifx_packet_infos *pkt_infos; -+ bool rv = lgtd_lifx_gateway_send_to_site_impl( -+ gw, pkt_type, pkt, &pkt_infos -+ ); -+ -+ lgtd_debug( -+ "sending %s to site %s", -+ pkt_infos->name, lgtd_addrtoa(gw->site.as_array) -+ ); -+ -+ return rv; // FIXME, have real return values on the send paths... -+} -+ -+bool -+lgtd_lifx_gateway_send_to_site(struct lgtd_lifx_gateway *gw, -+ enum lgtd_lifx_packet_type pkt_type, -+ const void *pkt) -+{ -+ const struct lgtd_lifx_packet_infos *pkt_infos; -+ bool rv = lgtd_lifx_gateway_send_to_site_impl( -+ gw, pkt_type, pkt, &pkt_infos -+ ); -+ -+ lgtd_info( -+ "sending %s to site %s", -+ pkt_infos->name, lgtd_addrtoa(gw->site.as_array) -+ ); -+ -+ return rv; // FIXME, have real return values on the send paths... -+} -+ - static void - lgtd_lifx_gateway_send_get_all_light_state(struct lgtd_lifx_gateway *gw) - { - assert(gw); - -- struct lgtd_lifx_packet_header hdr; -- union lgtd_lifx_target target = { .addr = gw->site.as_array }; -+ lgtd_lifx_gateway_send_to_site_quiet(gw, LGTD_LIFX_GET_LIGHT_STATE, NULL); - -- lgtd_lifx_wire_setup_header( -- &hdr, -- LGTD_LIFX_TARGET_SITE, -- target, -- gw->site.as_array, -- LGTD_LIFX_GET_LIGHT_STATE -- ); -- lgtd_lifx_gateway_enqueue_packet( -- gw, &hdr, LGTD_LIFX_GET_LIGHT_STATE, NULL, 0 -- ); -- -- struct lgtd_lifx_packet_get_tag_labels pkt = { .tags = LGTD_LIFX_ALL_TAGS }; -- lgtd_lifx_wire_setup_header( -- &hdr, -- LGTD_LIFX_TARGET_SITE, -- target, -- gw->site.as_array, -- LGTD_LIFX_GET_TAG_LABELS -- ); -- lgtd_lifx_gateway_enqueue_packet( -- gw, &hdr, LGTD_LIFX_GET_TAG_LABELS, &pkt, sizeof(pkt) -- ); -+ struct lgtd_lifx_packet_tags pkt = { .tags = LGTD_LIFX_ALL_TAGS }; -+ lgtd_lifx_gateway_send_to_site_quiet(gw, LGTD_LIFX_GET_TAG_LABELS, &pkt); - - gw->pending_refresh_req = true; - } -@@ -370,19 +421,55 @@ - } - - void -+lgtd_lifx_gateway_update_tag_refcounts(struct lgtd_lifx_gateway *gw, -+ uint64_t bulb_tags, -+ uint64_t pkt_tags) -+{ -+ uint64_t changes = bulb_tags ^ pkt_tags; -+ uint64_t added_tags = changes & pkt_tags; -+ uint64_t removed_tags = changes & bulb_tags; -+ int tag_id; -+ -+ LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, added_tags) { -+ if (gw->tag_refcounts[tag_id] != UINT8_MAX) { -+ gw->tag_refcounts[tag_id]++; -+ } else { -+ lgtd_warnx( -+ "reached refcount limit (%u) for tag [%s] (%d) on gw [%s]:%hu", -+ UINT8_MAX, gw->tags[tag_id] ? gw->tags[tag_id]->label : NULL, -+ tag_id, gw->ip_addr, gw->port -+ ); -+ } -+ } -+ -+ LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, removed_tags) { -+ assert(gw->tag_refcounts[tag_id] > 0); -+ if (--gw->tag_refcounts[tag_id] == 0) { -+ lgtd_info( -+ "deleting unused tag [%s] (%d) from gw [%s]:%hu (site %s)", -+ gw->tags[tag_id] ? gw->tags[tag_id]->label : NULL, tag_id, -+ gw->ip_addr, gw->port, lgtd_addrtoa(gw->site.as_array) -+ ); -+ struct lgtd_lifx_packet_tag_labels pkt = { -+ .tags = ~(gw->tag_ids & ~LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id)) -+ }; -+ lgtd_lifx_wire_encode_tag_labels(&pkt); -+ lgtd_lifx_gateway_send_to_site(gw, LGTD_LIFX_SET_TAG_LABELS, &pkt); -+ } -+ } -+} -+ -+void - lgtd_lifx_gateway_handle_pan_gateway(struct lgtd_lifx_gateway *gw, - const struct lgtd_lifx_packet_header *hdr, - const struct lgtd_lifx_packet_pan_gateway *pkt) - { -- (void)pkt; -- - assert(gw && hdr && pkt); - - lgtd_debug( -- "SET_PAN_GATEWAY <-- [%s]:%hu - %s site=%s", -- gw->ip_addr, gw->port, -- lgtd_addrtoa(hdr->target.device_addr), -- lgtd_addrtoa(hdr->site) -+ "SET_PAN_GATEWAY <-- [%s]:%hu - %s site=%s, service_type=%d", -+ gw->ip_addr, gw->port, lgtd_addrtoa(hdr->target.device_addr), -+ lgtd_addrtoa(hdr->site), pkt->service_type - ); - } - -@@ -484,16 +571,44 @@ - } - - int -+lgtd_lifx_gateway_get_tag_id(const struct lgtd_lifx_gateway *gw, -+ const struct lgtd_lifx_tag *tag) -+{ -+ assert(gw); -+ assert(tag); -+ -+ int tag_id; -+ LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, gw->tag_ids) { -+ if (gw->tags[tag_id] == tag) { -+ return tag_id; -+ } -+ } -+ -+ return -1; -+} -+ -+int - lgtd_lifx_gateway_allocate_tag_id(struct lgtd_lifx_gateway *gw, - int tag_id, - const char *tag_label) - { - assert(gw); - assert(tag_label); -- // allocating a new tag_id (tag_id == -1) isn't supported yet: -- assert(tag_id >= 0); -+ assert(tag_id >= -1); - assert(tag_id < LGTD_LIFX_GATEWAY_MAX_TAGS); - -+ if (tag_id == -1) { -+ tag_id = lgtd_lifx_wire_bitscan64_forward(~gw->tag_ids); -+ if (tag_id == -1) { -+ lgtd_warnx( -+ "no tag_id left for new tag [%s] on gw [%s]:%hu (site %s)", -+ tag_label, gw->ip_addr, gw->port, -+ lgtd_addrtoa(gw->site.as_array) -+ ); -+ return -1; -+ } -+ } -+ - if (!(gw->tag_ids & LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id))) { - struct lgtd_lifx_tag *tag; - tag = lgtd_lifx_tagging_incref(tag_label, gw, tag_id); -@@ -544,9 +659,9 @@ - assert(gw && hdr && pkt); - - lgtd_debug( -- "SET_TAG_LABELS <-- [%s]:%hu - %s label=%s, tags=%jx", -+ "SET_TAG_LABELS <-- [%s]:%hu - %s label=%.*s, tags=%jx", - gw->ip_addr, gw->port, lgtd_addrtoa(hdr->target.device_addr), -- pkt->label, (uintmax_t)pkt->tags -+ LGTD_LIFX_LABEL_SIZE, pkt->label, (uintmax_t)pkt->tags - ); - - int tag_id; -@@ -558,3 +673,38 @@ - } - } - } -+ -+void lgtd_lifx_gateway_handle_tags(struct lgtd_lifx_gateway *gw, -+ const struct lgtd_lifx_packet_header *hdr, -+ const struct lgtd_lifx_packet_tags *pkt) -+{ -+ assert(gw && hdr && pkt); -+ -+ lgtd_debug( -+ "SET_TAGS <-- [%s]:%hu - %s tags=%#jx", -+ gw->ip_addr, gw->port, lgtd_addrtoa(hdr->target.device_addr), -+ (uintmax_t)pkt->tags -+ ); -+ -+ struct lgtd_lifx_bulb *b = lgtd_lifx_gateway_get_or_open_bulb( -+ gw, hdr->target.device_addr -+ ); -+ if (!b) { -+ return; -+ } -+ -+ int tag_id; -+ LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, pkt->tags) { -+ if (!(gw->tag_ids & LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id))) { -+ lgtd_warnx( -+ "trying to set unknown tag_id %d (%#jx) " -+ "on bulb %s (%.*s), gw [%s]:%hu (site %s)", -+ tag_id, LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id), -+ lgtd_addrtoa(b->addr), LGTD_LIFX_LABEL_SIZE, b->state.label, -+ gw->ip_addr, gw->port, lgtd_addrtoa(gw->site.as_array) -+ ); -+ } -+ } -+ -+ lgtd_lifx_bulb_set_tags(b, pkt->tags); -+} -diff --git a/lifx/gateway.h b/lifx/gateway.h ---- a/lifx/gateway.h -+++ b/lifx/gateway.h -@@ -21,7 +21,7 @@ - // according to my own tests, aggressively polling a bulb doesn't raise its - // consumption at all (and it's interesting to note that a turned off bulb - // still draw about 2W in ZigBee and about 3W in WiFi). --enum { LGTD_LIFX_GATEWAY_MIN_REFRESH_INTERVAL_MSECS = 200 }; -+enum { LGTD_LIFX_GATEWAY_MIN_REFRESH_INTERVAL_MSECS = 1000 }; - - // You can't send more than one lifx packet per UDP datagram. - enum { LGTD_LIFX_GATEWAY_PACKET_RING_SIZE = 16 }; -@@ -51,6 +51,7 @@ - } site; - uint64_t tag_ids; - struct lgtd_lifx_tag *tags[LGTD_LIFX_GATEWAY_MAX_TAGS]; -+ uint8_t tag_refcounts[LGTD_LIFX_GATEWAY_MAX_TAGS]; - evutil_socket_t socket; - // Those three timers let us measure the latency of the gateway. If we - // aren't the only client on the network then this won't be accurate since -@@ -84,6 +85,7 @@ - - void lgtd_lifx_gateway_close(struct lgtd_lifx_gateway *); - void lgtd_lifx_gateway_close_all(void); -+void lgtd_lifx_gateway_remove_and_close_bulb(struct lgtd_lifx_gateway *, struct lgtd_lifx_bulb *); - - void lgtd_lifx_gateway_force_refresh(struct lgtd_lifx_gateway *); - -@@ -92,7 +94,14 @@ - enum lgtd_lifx_packet_type, - const void *, - int); -+// This could be on router but it's LIFX specific so I'd rather keep it here: -+bool lgtd_lifx_gateway_send_to_site(struct lgtd_lifx_gateway *, -+ enum lgtd_lifx_packet_type, -+ const void *); - -+void lgtd_lifx_gateway_update_tag_refcounts(struct lgtd_lifx_gateway *, uint64_t, uint64_t); -+ -+int lgtd_lifx_gateway_get_tag_id(const struct lgtd_lifx_gateway *, const struct lgtd_lifx_tag *); - int lgtd_lifx_gateway_allocate_tag_id(struct lgtd_lifx_gateway *, int, const char *); - void lgtd_lifx_gateway_deallocate_tag_id(struct lgtd_lifx_gateway *, int); - -@@ -108,3 +117,6 @@ - void lgtd_lifx_gateway_handle_tag_labels(struct lgtd_lifx_gateway *, - const struct lgtd_lifx_packet_header *, - const struct lgtd_lifx_packet_tag_labels *); -+void lgtd_lifx_gateway_handle_tags(struct lgtd_lifx_gateway *, -+ const struct lgtd_lifx_packet_header *, -+ const struct lgtd_lifx_packet_tags *); -diff --git a/lifx/tagging.c b/lifx/tagging.c ---- a/lifx/tagging.c -+++ b/lifx/tagging.c -@@ -66,6 +66,32 @@ - } - - struct lgtd_lifx_tag * -+lgtd_lifx_tagging_allocate_tag(const char *tag_label) -+{ -+ assert(tag_label); -+ assert(strlen(tag_label) < LGTD_LIFX_LABEL_SIZE); -+ -+ struct lgtd_lifx_tag *tag = calloc(1, sizeof(*tag)); -+ if (!tag) { -+ return NULL; -+ } -+ -+ strncpy(tag->label, tag_label, sizeof(tag->label) - 1); -+ LIST_INSERT_HEAD(&lgtd_lifx_tags, tag, link); -+ return tag; -+} -+ -+void -+lgtd_lifx_tagging_deallocate_tag(struct lgtd_lifx_tag *tag) -+{ -+ assert(tag); -+ assert(LIST_EMPTY(&tag->sites)); -+ -+ LIST_REMOVE(tag, link); -+ free(tag); -+} -+ -+struct lgtd_lifx_tag * - lgtd_lifx_tagging_incref(const char *tag_label, - struct lgtd_lifx_gateway *gw, - int tag_id) -@@ -77,12 +103,10 @@ - bool dealloc_tag = false; - struct lgtd_lifx_tag *tag = lgtd_lifx_tagging_find_tag(tag_label); - if (!tag) { -- tag = calloc(1, sizeof(*tag)); -+ tag = lgtd_lifx_tagging_allocate_tag(tag_label); - if (!tag) { - return NULL; - } -- strncpy(tag->label, tag_label, sizeof(tag->label) - 1); -- LIST_INSERT_HEAD(&lgtd_lifx_tags, tag, link); - dealloc_tag = true; - } - -@@ -91,8 +115,7 @@ - site = calloc(1, sizeof(*site)); - if (!site) { - if (dealloc_tag) { -- LIST_REMOVE(tag, link); -- free(tag); -+ lgtd_lifx_tagging_deallocate_tag(tag); - } - errno = ENOMEM; - return NULL; -@@ -100,9 +123,10 @@ - if (dealloc_tag) { - lgtd_info("discovered tag [%s]", tag_label); - } -- lgtd_debug( -- "tag [%s] added to gw [%s]:%hu (site %s)", -- tag_label, gw->ip_addr, gw->port, lgtd_addrtoa(gw->site.as_array) -+ lgtd_info( -+ "tag [%s] added to gw [%s]:%hu (site %s) with tag_id %d", -+ tag_label, gw->ip_addr, gw->port, -+ lgtd_addrtoa(gw->site.as_array), tag_id - ); - site->gw = gw; - site->tag_id = tag_id; -@@ -132,8 +156,7 @@ - free(site); - } - if (LIST_EMPTY(&tag->sites)) { -- LIST_REMOVE(tag, link); - lgtd_info("forgetting unused tag [%s]", tag->label); -- free(tag); -+ lgtd_lifx_tagging_deallocate_tag(tag); - } - } -diff --git a/lifx/tagging.h b/lifx/tagging.h ---- a/lifx/tagging.h -+++ b/lifx/tagging.h -@@ -39,3 +39,6 @@ - void lgtd_lifx_tagging_decref(struct lgtd_lifx_tag *, struct lgtd_lifx_gateway *); - - struct lgtd_lifx_tag *lgtd_lifx_tagging_find_tag(const char *); -+struct lgtd_lifx_tag *lgtd_lifx_tagging_allocate_tag(const char *); -+ -+void lgtd_lifx_tagging_deallocate_tag(struct lgtd_lifx_tag *); -diff --git a/lifx/timer.c b/lifx/timer.c ---- a/lifx/timer.c -+++ b/lifx/timer.c -@@ -95,7 +95,7 @@ - "closing bulb \"%.*s\" that hasn't been updated for %dms", - LGTD_LIFX_LABEL_SIZE, bulb->state.label, light_state_lag - ); -- lgtd_lifx_bulb_close(bulb); -+ lgtd_lifx_gateway_remove_and_close_bulb(bulb->gw, bulb); - start_discovery = true; - continue; - } -diff --git a/lifx/timer.h b/lifx/timer.h ---- a/lifx/timer.h -+++ b/lifx/timer.h -@@ -17,11 +17,11 @@ - - #pragma once - --enum { LGTD_LIFX_TIMER_WATCHDOG_INTERVAL_MSECS = 200 }; -+enum { LGTD_LIFX_TIMER_WATCHDOG_INTERVAL_MSECS = 500 }; - enum { LGTD_LIFX_TIMER_ACTIVE_DISCOVERY_INTERVAL_MSECS = 2000 }; - enum { LGTD_LIFX_TIMER_PASSIVE_DISCOVERY_INTERVAL_MSECS = 10000 }; --enum { LGTD_LIFX_TIMER_DEVICE_TIMEOUT_MSECS = 2000 }; --enum { LGTD_LIFX_TIMER_DEVICE_FORCE_REFRESH_MSECS = 600 }; -+enum { LGTD_LIFX_TIMER_DEVICE_TIMEOUT_MSECS = 4000 }; -+enum { LGTD_LIFX_TIMER_DEVICE_FORCE_REFRESH_MSECS = 2000 }; - - bool lgtd_lifx_timer_setup(void); - void lgtd_lifx_timer_close(void); -diff --git a/lifx/wire_proto.c b/lifx/wire_proto.c ---- a/lifx/wire_proto.c -+++ b/lifx/wire_proto.c -@@ -92,6 +92,7 @@ - .handle = lgtd_lifx_wire_null_packet_handler - - static struct lgtd_lifx_packet_infos packet_table[] = { -+ // Gateway packets: - { - REQUEST_ONLY, - NO_PAYLOAD, -@@ -108,6 +109,43 @@ - }, - { - REQUEST_ONLY, -+ .name = "SET_TAG_LABELS", -+ .type = LGTD_LIFX_SET_TAG_LABELS, -+ .size = sizeof(struct lgtd_lifx_packet_tag_labels), -+ .encode = ENCODER(lgtd_lifx_wire_encode_tag_labels) -+ }, -+ { -+ REQUEST_ONLY, -+ .name = "GET_TAG_LABELS", -+ .type = LGTD_LIFX_GET_TAG_LABELS, -+ .size = sizeof(struct lgtd_lifx_packet_tags), -+ .encode = ENCODER(lgtd_lifx_wire_encode_tags) -+ }, -+ { -+ RESPONSE_ONLY, -+ .name = "TAG_LABELS", -+ .type = LGTD_LIFX_TAG_LABELS, -+ .size = sizeof(struct lgtd_lifx_packet_tag_labels), -+ .decode = DECODER(lgtd_lifx_wire_decode_tag_labels), -+ .handle = HANDLER(lgtd_lifx_gateway_handle_tag_labels) -+ }, -+ // Bulb packets: -+ { -+ REQUEST_ONLY, -+ .name = "SET_LIGHT_COLOR", -+ .type = LGTD_LIFX_SET_LIGHT_COLOR, -+ .size = sizeof(struct lgtd_lifx_packet_light_color), -+ .encode = ENCODER(lgtd_lifx_wire_encode_light_color) -+ }, -+ { -+ REQUEST_ONLY, -+ .name = "SET_WAVEFORM", -+ .type = LGTD_LIFX_SET_WAVEFORM, -+ .size = sizeof(struct lgtd_lifx_packet_waveform), -+ .encode = ENCODER(lgtd_lifx_wire_encode_waveform) -+ }, -+ { -+ REQUEST_ONLY, - NO_PAYLOAD, - .name = "GET_LIGHT_STATUS", - .type = LGTD_LIFX_GET_LIGHT_STATE -@@ -128,6 +166,7 @@ - .type = LGTD_LIFX_SET_POWER_STATE, - }, - { -+ RESPONSE_ONLY, - .name = "POWER_STATE", - .type = LGTD_LIFX_POWER_STATE, - .size = sizeof(struct lgtd_lifx_packet_power_state), -@@ -136,32 +175,18 @@ - }, - { - REQUEST_ONLY, -- .name = "SET_LIGHT_COLOR", -- .type = LGTD_LIFX_SET_LIGHT_COLOR, -- .size = sizeof(struct lgtd_lifx_packet_light_color), -- .encode = ENCODER(lgtd_lifx_wire_encode_light_color) -- }, -- { -- REQUEST_ONLY, -- .name = "SET_WAVEFORM", -- .type = LGTD_LIFX_SET_WAVEFORM, -- .size = sizeof(struct lgtd_lifx_packet_waveform), -- .encode = ENCODER(lgtd_lifx_wire_encode_waveform) -- }, -- { -- REQUEST_ONLY, -- .name = "GET_TAG_LABELS", -- .type = LGTD_LIFX_GET_TAG_LABELS, -- .size = sizeof(struct lgtd_lifx_packet_get_tag_labels), -- .encode = lgtd_lifx_wire_null_packet_encoder_decoder -+ .name = "SET_TAGS", -+ .type = LGTD_LIFX_SET_TAGS, -+ .size = sizeof(struct lgtd_lifx_packet_tags), -+ .encode = ENCODER(lgtd_lifx_wire_encode_tags) - }, - { - RESPONSE_ONLY, -- .name = "TAG_LABELS", -- .type = LGTD_LIFX_TAG_LABELS, -- .size = sizeof(struct lgtd_lifx_packet_tag_labels), -- .decode = DECODER(lgtd_lifx_wire_decode_tag_labels), -- .handle = HANDLER(lgtd_lifx_gateway_handle_tag_labels) -+ .name = "TAGS", -+ .type = LGTD_LIFX_TAGS, -+ .size = sizeof(struct lgtd_lifx_packet_tags), -+ .decode = DECODER(lgtd_lifx_wire_decode_tags), -+ .handle = HANDLER(lgtd_lifx_gateway_handle_tags) - } - }; - -@@ -357,11 +382,19 @@ - pkt->brightness = htole16(pkt->brightness); - pkt->kelvin = htole16(pkt->kelvin); - pkt->period = htole32(pkt->period); -- pkt->cycles = lifx_wire_htolefloat(pkt->cycles); -+ pkt->cycles = lgtd_lifx_wire_htolefloat(pkt->cycles); - pkt->skew_ratio = htole16(pkt->skew_ratio); - } - - void -+lgtd_lifx_wire_encode_tag_labels(struct lgtd_lifx_packet_tag_labels *pkt) -+{ -+ assert(pkt); -+ -+ pkt->tags = htole64(pkt->tags); -+} -+ -+void - lgtd_lifx_wire_decode_tag_labels(struct lgtd_lifx_packet_tag_labels *pkt) - { - assert(pkt); -@@ -369,3 +402,19 @@ - pkt->label[sizeof(pkt->label) - 1] = '\0'; - pkt->tags = le64toh(pkt->tags); - } -+ -+void -+lgtd_lifx_wire_encode_tags(struct lgtd_lifx_packet_tags *pkt) -+{ -+ assert(pkt); -+ -+ pkt->tags = htole64(pkt->tags); -+} -+ -+void -+lgtd_lifx_wire_decode_tags(struct lgtd_lifx_packet_tags *pkt) -+{ -+ assert(pkt); -+ -+ pkt->tags = le64toh(pkt->tags); -+} -diff --git a/lifx/wire_proto.h b/lifx/wire_proto.h ---- a/lifx/wire_proto.h -+++ b/lifx/wire_proto.h -@@ -26,14 +26,14 @@ - typedef float floatle_t; - - static inline floatle_t --lifx_wire_htolefloat(float f) -+lgtd_lifx_wire_htolefloat(float f) - { - *(uint32_t *)&f = htole32(*(uint32_t *)&f); - return f; - } - - static inline floatle_t --lifx_wire_lefloattoh(float f) -+lgtd_lifx_wire_lefloattoh(float f) - { - *(uint32_t *)&f = le32toh(*(uint32_t *)&f); - return f; -@@ -238,7 +238,7 @@ - }; - - enum { LGTD_LIFX_ALL_TAGS = ~0 }; --struct lgtd_lifx_packet_get_tag_labels { -+struct lgtd_lifx_packet_tags { - uint64le_t tags; - }; - -@@ -350,4 +350,7 @@ - void lgtd_lifx_wire_encode_light_color(struct lgtd_lifx_packet_light_color *); - void lgtd_lifx_wire_encode_waveform(struct lgtd_lifx_packet_waveform *); - -+void lgtd_lifx_wire_encode_tags(struct lgtd_lifx_packet_tags *); -+void lgtd_lifx_wire_decode_tags(struct lgtd_lifx_packet_tags *); -+void lgtd_lifx_wire_encode_tag_labels(struct lgtd_lifx_packet_tag_labels *); - void lgtd_lifx_wire_decode_tag_labels(struct lgtd_lifx_packet_tag_labels *); -diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt ---- a/tests/CMakeLists.txt -+++ b/tests/CMakeLists.txt -@@ -1,1 +1,14 @@ -+FUNCTION(ADD_CORE_LIBRARY LIBNAME) -+ ADD_LIBRARY(${LIBNAME} ${ARGN}) -+ TARGET_LINK_LIBRARIES(${LIBNAME} ${TIME_MONOTONIC_LIBRARY}) -+ TARGET_INCLUDE_DIRECTORIES( -+ ${LIBNAME} PUBLIC -+ ${LIGHTSD_SOURCE_DIR}/core/ -+ ${LIGHTSD_BINARY_DIR}/core/ -+ ) -+ IF (HAVE_LIBBSD) -+ TARGET_LINK_LIBRARIES(${LIBNAME} ${LIBBSD_LIBRARY}) -+ ENDIF (HAVE_LIBBSD) -+ENDFUNCTION() -+ - ADD_ALL_SUBDIRECTORIES() -diff --git a/tests/core/CMakeLists.txt b/tests/core/CMakeLists.txt ---- a/tests/core/CMakeLists.txt -+++ b/tests/core/CMakeLists.txt -@@ -2,9 +2,11 @@ - ${LIGHTSD_SOURCE_DIR} - ${LIGHTSD_SOURCE_DIR}/core/ - ${CMAKE_CURRENT_SOURCE_DIR} -+ ${CMAKE_CURRENT_SOURCE_DIR}/../lifx - ${LIGHTSD_BINARY_DIR} - ${LIGHTSD_BINARY_DIR}/core/ - ${CMAKE_CURRENT_BINARY_DIR} -+ ${CMAKE_CURRENT_BINARY_DIR}/../lifx - ) - - ADD_ALL_SUBDIRECTORIES() -diff --git a/tests/core/jsonrpc/CMakeLists.txt b/tests/core/jsonrpc/CMakeLists.txt ---- a/tests/core/jsonrpc/CMakeLists.txt -+++ b/tests/core/jsonrpc/CMakeLists.txt -@@ -3,7 +3,7 @@ - ${CMAKE_CURRENT_BINARY_DIR} - ) - --ADD_LIBRARY( -+ADD_CORE_LIBRARY( - test_core_jsonrpc STATIC - ${LIGHTSD_SOURCE_DIR}/core/jsmn.c - ${LIGHTSD_SOURCE_DIR}/core/log.c -@@ -13,10 +13,6 @@ - ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c - ${CMAKE_CURRENT_SOURCE_DIR}/../tests_utils.c - ) --TARGET_LINK_LIBRARIES(test_core_jsonrpc ${TIME_MONOTONIC_LIBRARY}) --IF (HAVE_LIBBSD) -- TARGET_LINK_LIBRARIES(test_core_jsonrpc ${LIBBSD_LIBRARY}) --ENDIF (HAVE_LIBBSD) - - FUNCTION(ADD_JSONRPC_TEST TEST_SOURCE) - ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_core_jsonrpc) -diff --git a/tests/core/jsonrpc/test_jsonrpc_build_target_list.c b/tests/core/jsonrpc/test_jsonrpc_build_target_list.c ---- a/tests/core/jsonrpc/test_jsonrpc_build_target_list.c -+++ b/tests/core/jsonrpc/test_jsonrpc_build_target_list.c -@@ -1,6 +1,7 @@ - #include "jsonrpc.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - #include "test_jsonrpc_utils.h" - - static void -diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off.c ---- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off.c -+++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off.c -@@ -1,6 +1,7 @@ - #include "jsonrpc.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - - #define LGTD_TESTING_POWER_OFF - #include "test_jsonrpc_utils.h" -diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off_missing_target.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off_missing_target.c ---- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off_missing_target.c -+++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off_missing_target.c -@@ -1,6 +1,7 @@ - #include "jsonrpc.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - - #define LGTD_TESTING_POWER_OFF - #include "test_jsonrpc_utils.h" -diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on.c ---- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on.c -+++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on.c -@@ -1,6 +1,7 @@ - #include "jsonrpc.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - - #define LGTD_TESTING_POWER_ON - #include "test_jsonrpc_utils.h" -diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on_missing_target.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on_missing_target.c ---- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on_missing_target.c -+++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on_missing_target.c -@@ -1,6 +1,7 @@ - #include "jsonrpc.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - - #define LGTD_TESTING_POWER_ON - #include "test_jsonrpc_utils.h" -diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk.c ---- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk.c -+++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk.c -@@ -1,6 +1,7 @@ - #include "jsonrpc.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - - #define LGTD_TESTING_SET_LIGHT_FROM_HSBK - #include "test_jsonrpc_utils.h" -diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_from_array.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_from_array.c ---- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_from_array.c -+++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_from_array.c -@@ -1,6 +1,7 @@ - #include "jsonrpc.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - - #define LGTD_TESTING_SET_LIGHT_FROM_HSBK - #include "test_jsonrpc_utils.h" -diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_invalid_params.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_invalid_params.c ---- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_invalid_params.c -+++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_invalid_params.c -@@ -1,6 +1,7 @@ - #include "jsonrpc.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - - #define LGTD_TESTING_SET_LIGHT_FROM_HSBK - #include "test_jsonrpc_utils.h" -diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform.c ---- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform.c -+++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform.c -@@ -1,6 +1,7 @@ - #include "jsonrpc.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - - #define LGTD_TESTING_SET_WAVEFORM - #include "test_jsonrpc_utils.h" -diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform_invalid_params.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform_invalid_params.c ---- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform_invalid_params.c -+++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform_invalid_params.c -@@ -1,6 +1,7 @@ - #include "jsonrpc.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - - #define LGTD_TESTING_SET_WAVEFORM - #include "test_jsonrpc_utils.h" -diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_tag.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_tag.c -new file mode 100644 ---- /dev/null -+++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_tag.c -@@ -0,0 +1,65 @@ -+#include "jsonrpc.c" -+ -+#include "mock_client_buf.h" -+#include "mock_gateway.h" -+ -+#define MOCKED_LGTD_TAG -+#include "test_jsonrpc_utils.h" -+ -+static bool tag_called = false; -+ -+void -+lgtd_proto_tag(struct lgtd_client *client, -+ const struct lgtd_proto_target_list *targets, -+ const char *tag) -+{ -+ if (!client) { -+ errx(1, "missing client!"); -+ } -+ -+ if (strcmp(SLIST_FIRST(targets)->target, "*")) { -+ errx( -+ 1, "Invalid target [%s] (expected=[*])", -+ SLIST_FIRST(targets)->target -+ ); -+ } -+ -+ if (strcmp(tag, "suspensions")) { -+ errx(1, "Invalid tag [%s] (expected=[suspensions])", tag); -+ } -+ -+ tag_called = true; -+} -+ -+int -+main(void) -+{ -+ jsmntok_t tokens[32]; -+ const char json[] = ("{" -+ "\"jsonrpc\": \"2.0\"," -+ "\"method\": \"tag\"," -+ "\"params\": {\"target\": \"*\", \"tag\": \"suspensions\"}," -+ "\"id\": \"42\"" -+ "}"); -+ int parsed = parse_json( -+ tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json) -+ ); -+ -+ bool ok; -+ struct lgtd_jsonrpc_request req = TEST_REQUEST_INITIALIZER; -+ struct lgtd_client client = { -+ .io = NULL, .current_request = &req, .json = json -+ }; -+ ok = lgtd_jsonrpc_check_and_extract_request(&req, tokens, parsed, json); -+ if (!ok) { -+ errx(1, "can't parse request"); -+ } -+ -+ lgtd_jsonrpc_check_and_call_proto_tag_or_untag(&client, lgtd_proto_tag); -+ -+ if (!tag_called) { -+ errx(1, "lgtd_proto_tag wasn't called"); -+ } -+ -+ return 0; -+} -diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_tag_missing_params.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_tag_missing_params.c -new file mode 100644 ---- /dev/null -+++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_tag_missing_params.c -@@ -0,0 +1,53 @@ -+#include "jsonrpc.c" -+ -+#include "mock_client_buf.h" -+#include "mock_gateway.h" -+ -+#define MOCKED_LGTD_TAG -+#include "test_jsonrpc_utils.h" -+ -+static bool tag_called = false; -+ -+void -+lgtd_proto_tag(struct lgtd_client *client, -+ const struct lgtd_proto_target_list *targets, -+ const char *tag) -+{ -+ (void)client; -+ (void)targets; -+ (void)tag; -+ tag_called = true; -+} -+ -+int -+main(void) -+{ -+ jsmntok_t tokens[32]; -+ const char json[] = ("{" -+ "\"jsonrpc\": \"2.0\"," -+ "\"method\": \"tag\"," -+ "\"params\": {\"tag\": \"suspensions\"}," -+ "\"id\": \"42\"" -+ "}"); -+ int parsed = parse_json( -+ tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json) -+ ); -+ -+ bool ok; -+ struct lgtd_jsonrpc_request req = TEST_REQUEST_INITIALIZER; -+ struct lgtd_client client = { -+ .io = NULL, .current_request = &req, .json = json -+ }; -+ ok = lgtd_jsonrpc_check_and_extract_request(&req, tokens, parsed, json); -+ if (!ok) { -+ errx(1, "can't parse request"); -+ } -+ -+ lgtd_jsonrpc_check_and_call_proto_tag_or_untag(&client, lgtd_proto_tag); -+ -+ if (tag_called) { -+ errx(1, "lgtd_proto_tag was called"); -+ } -+ -+ return 0; -+} -diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_untag.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_untag.c -new file mode 100644 ---- /dev/null -+++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_untag.c -@@ -0,0 +1,65 @@ -+#include "jsonrpc.c" -+ -+#include "mock_client_buf.h" -+#include "mock_gateway.h" -+ -+#define MOCKED_LGTD_UNTAG -+#include "test_jsonrpc_utils.h" -+ -+static bool untag_called = false; -+ -+void -+lgtd_proto_untag(struct lgtd_client *client, -+ const struct lgtd_proto_target_list *targets, -+ const char *tag) -+{ -+ if (!client) { -+ errx(1, "missing client!"); -+ } -+ -+ if (strcmp(SLIST_FIRST(targets)->target, "#suspensions")) { -+ errx( -+ 1, "Invalid target [%s] (expected=[#suspensions])", -+ SLIST_FIRST(targets)->target -+ ); -+ } -+ -+ if (strcmp(tag, "suspensions")) { -+ errx(1, "Invalid tag [%s] (expected=[suspensions])", tag); -+ } -+ -+ untag_called = true; -+} -+ -+int -+main(void) -+{ -+ jsmntok_t tokens[32]; -+ const char json[] = ("{" -+ "\"jsonrpc\": \"2.0\"," -+ "\"method\": \"tag\"," -+ "\"params\": [[\"#suspensions\"], \"suspensions\"]," -+ "\"id\": \"42\"" -+ "}"); -+ int parsed = parse_json( -+ tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json) -+ ); -+ -+ bool ok; -+ struct lgtd_jsonrpc_request req = TEST_REQUEST_INITIALIZER; -+ struct lgtd_client client = { -+ .io = NULL, .current_request = &req, .json = json -+ }; -+ ok = lgtd_jsonrpc_check_and_extract_request(&req, tokens, parsed, json); -+ if (!ok) { -+ errx(1, "can't parse request"); -+ } -+ -+ lgtd_jsonrpc_check_and_call_proto_tag_or_untag(&client, lgtd_proto_untag); -+ -+ if (!untag_called) { -+ errx(1, "lgtd_proto_tag wasn't called"); -+ } -+ -+ return 0; -+} -diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_untag_invalid_params.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_untag_invalid_params.c -new file mode 100644 ---- /dev/null -+++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_untag_invalid_params.c -@@ -0,0 +1,53 @@ -+#include "jsonrpc.c" -+ -+#include "mock_client_buf.h" -+#include "mock_gateway.h" -+ -+#define MOCKED_LGTD_UNTAG -+#include "test_jsonrpc_utils.h" -+ -+static bool untag_called = false; -+ -+void -+lgtd_proto_untag(struct lgtd_client *client, -+ const struct lgtd_proto_target_list *targets, -+ const char *tag) -+{ -+ (void)client; -+ (void)targets; -+ (void)tag; -+ untag_called = true; -+} -+ -+int -+main(void) -+{ -+ jsmntok_t tokens[32]; -+ const char json[] = ("{" -+ "\"jsonrpc\": \"2.0\"," -+ "\"method\": \"tag\"," -+ "\"params\": [[\"#suspensions\"], [\"suspensions\"]]," -+ "\"id\": \"42\"" -+ "}"); -+ int parsed = parse_json( -+ tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json) -+ ); -+ -+ bool ok; -+ struct lgtd_jsonrpc_request req = TEST_REQUEST_INITIALIZER; -+ struct lgtd_client client = { -+ .io = NULL, .current_request = &req, .json = json -+ }; -+ ok = lgtd_jsonrpc_check_and_extract_request(&req, tokens, parsed, json); -+ if (!ok) { -+ errx(1, "can't parse request"); -+ } -+ -+ lgtd_jsonrpc_check_and_call_proto_tag_or_untag(&client, lgtd_proto_untag); -+ -+ if (untag_called) { -+ errx(1, "lgtd_proto_tag was called"); -+ } -+ -+ return 0; -+} -diff --git a/tests/core/jsonrpc/test_jsonrpc_extract_request_no_params.c b/tests/core/jsonrpc/test_jsonrpc_extract_request_no_params.c ---- a/tests/core/jsonrpc/test_jsonrpc_extract_request_no_params.c -+++ b/tests/core/jsonrpc/test_jsonrpc_extract_request_no_params.c -@@ -1,6 +1,7 @@ - #include "jsonrpc.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - #include "test_jsonrpc_utils.h" - - int -diff --git a/tests/core/jsonrpc/test_jsonrpc_extract_request_notification_no_params.c b/tests/core/jsonrpc/test_jsonrpc_extract_request_notification_no_params.c ---- a/tests/core/jsonrpc/test_jsonrpc_extract_request_notification_no_params.c -+++ b/tests/core/jsonrpc/test_jsonrpc_extract_request_notification_no_params.c -@@ -1,6 +1,7 @@ - #include "jsonrpc.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - #include "test_jsonrpc_utils.h" - - int -diff --git a/tests/core/jsonrpc/test_jsonrpc_extract_request_params_array.c b/tests/core/jsonrpc/test_jsonrpc_extract_request_params_array.c ---- a/tests/core/jsonrpc/test_jsonrpc_extract_request_params_array.c -+++ b/tests/core/jsonrpc/test_jsonrpc_extract_request_params_array.c -@@ -1,6 +1,7 @@ - #include "jsonrpc.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - #include "test_jsonrpc_utils.h" - - int -diff --git a/tests/core/jsonrpc/test_jsonrpc_extract_request_params_obj.c b/tests/core/jsonrpc/test_jsonrpc_extract_request_params_obj.c ---- a/tests/core/jsonrpc/test_jsonrpc_extract_request_params_obj.c -+++ b/tests/core/jsonrpc/test_jsonrpc_extract_request_params_obj.c -@@ -1,6 +1,7 @@ - #include "jsonrpc.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - #include "test_jsonrpc_utils.h" - - int -diff --git a/tests/core/jsonrpc/test_jsonrpc_extract_request_valid_notification.c b/tests/core/jsonrpc/test_jsonrpc_extract_request_valid_notification.c ---- a/tests/core/jsonrpc/test_jsonrpc_extract_request_valid_notification.c -+++ b/tests/core/jsonrpc/test_jsonrpc_extract_request_valid_notification.c -@@ -1,6 +1,7 @@ - #include "jsonrpc.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - #include "test_jsonrpc_utils.h" - - int -diff --git a/tests/core/jsonrpc/test_jsonrpc_send_error.c b/tests/core/jsonrpc/test_jsonrpc_send_error.c ---- a/tests/core/jsonrpc/test_jsonrpc_send_error.c -+++ b/tests/core/jsonrpc/test_jsonrpc_send_error.c -@@ -1,6 +1,7 @@ - #include "jsonrpc.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - #include "test_jsonrpc_utils.h" - - int -diff --git a/tests/core/jsonrpc/test_jsonrpc_send_response.c b/tests/core/jsonrpc/test_jsonrpc_send_response.c ---- a/tests/core/jsonrpc/test_jsonrpc_send_response.c -+++ b/tests/core/jsonrpc/test_jsonrpc_send_response.c -@@ -1,6 +1,7 @@ - #include "jsonrpc.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - #include "test_jsonrpc_utils.h" - - int -diff --git a/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_invalid.c b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_invalid.c ---- a/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_invalid.c -+++ b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_invalid.c -@@ -1,6 +1,7 @@ - #include "jsonrpc.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - #include "test_jsonrpc_utils.h" - - static void -diff --git a/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_valid.c b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_valid.c ---- a/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_valid.c -+++ b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_valid.c -@@ -1,6 +1,7 @@ - #include "jsonrpc.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - #include "test_jsonrpc_utils.h" - - static void -diff --git a/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_invalid.c b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_invalid.c ---- a/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_invalid.c -+++ b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_invalid.c -@@ -1,6 +1,7 @@ - #include "jsonrpc.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - #include "test_jsonrpc_utils.h" - - static void -diff --git a/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_valid.c b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_valid.c ---- a/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_valid.c -+++ b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_valid.c -@@ -1,6 +1,7 @@ - #include "jsonrpc.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - #include "test_jsonrpc_utils.h" - - static void -diff --git a/tests/core/jsonrpc/test_jsonrpc_type_integer.c b/tests/core/jsonrpc/test_jsonrpc_type_integer.c ---- a/tests/core/jsonrpc/test_jsonrpc_type_integer.c -+++ b/tests/core/jsonrpc/test_jsonrpc_type_integer.c -@@ -1,6 +1,7 @@ - #include "jsonrpc.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - #include "test_jsonrpc_utils.h" - - int -diff --git a/tests/core/jsonrpc/test_jsonrpc_type_integer_invalid_characters.c b/tests/core/jsonrpc/test_jsonrpc_type_integer_invalid_characters.c ---- a/tests/core/jsonrpc/test_jsonrpc_type_integer_invalid_characters.c -+++ b/tests/core/jsonrpc/test_jsonrpc_type_integer_invalid_characters.c -@@ -1,6 +1,7 @@ - #include "jsonrpc.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - #include "test_jsonrpc_utils.h" - - int -diff --git a/tests/core/jsonrpc/test_jsonrpc_type_integer_too_big.c b/tests/core/jsonrpc/test_jsonrpc_type_integer_too_big.c ---- a/tests/core/jsonrpc/test_jsonrpc_type_integer_too_big.c -+++ b/tests/core/jsonrpc/test_jsonrpc_type_integer_too_big.c -@@ -1,6 +1,7 @@ - #include "jsonrpc.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - #include "test_jsonrpc_utils.h" - - int -diff --git a/tests/core/jsonrpc/test_jsonrpc_type_integer_too_small.c b/tests/core/jsonrpc/test_jsonrpc_type_integer_too_small.c ---- a/tests/core/jsonrpc/test_jsonrpc_type_integer_too_small.c -+++ b/tests/core/jsonrpc/test_jsonrpc_type_integer_too_small.c -@@ -1,6 +1,7 @@ - #include "jsonrpc.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - #include "test_jsonrpc_utils.h" - - int -diff --git a/tests/core/jsonrpc/test_jsonrpc_uint16_range_to_float_string.c b/tests/core/jsonrpc/test_jsonrpc_uint16_range_to_float_string.c ---- a/tests/core/jsonrpc/test_jsonrpc_uint16_range_to_float_string.c -+++ b/tests/core/jsonrpc/test_jsonrpc_uint16_range_to_float_string.c -@@ -3,6 +3,7 @@ - #include "jsonrpc.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - #include "test_jsonrpc_utils.h" - - int -diff --git a/tests/core/jsonrpc/test_jsonrpc_utils.h b/tests/core/jsonrpc/test_jsonrpc_utils.h ---- a/tests/core/jsonrpc/test_jsonrpc_utils.h -+++ b/tests/core/jsonrpc/test_jsonrpc_utils.h -@@ -95,3 +95,27 @@ - (void)targets; - } - #endif -+ -+#ifndef MOCKED_LGTD_TAG -+void -+lgtd_proto_tag(struct lgtd_client *client, -+ const struct lgtd_proto_target_list *targets, -+ const char *tag_label) -+{ -+ (void)client; -+ (void)targets; -+ (void)tag_label; -+} -+#endif -+ -+#ifndef MOCKED_LGTD_UNTAG -+void -+lgtd_proto_untag(struct lgtd_client *client, -+ const struct lgtd_proto_target_list *targets, -+ const char *tag_label) -+{ -+ (void)client; -+ (void)targets; -+ (void)tag_label; -+} -+#endif -diff --git a/tests/core/proto/CMakeLists.txt b/tests/core/proto/CMakeLists.txt ---- a/tests/core/proto/CMakeLists.txt -+++ b/tests/core/proto/CMakeLists.txt -@@ -3,21 +3,18 @@ - ${CMAKE_CURRENT_BINARY_DIR} - ) - --ADD_LIBRARY( -+ADD_CORE_LIBRARY( - test_core_proto STATIC - ${LIGHTSD_SOURCE_DIR}/core/log.c - ${LIGHTSD_SOURCE_DIR}/core/jsonrpc.c - ${LIGHTSD_SOURCE_DIR}/core/stats.c - ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c -+ ${LIGHTSD_SOURCE_DIR}/lifx/tagging.c - ${LIGHTSD_SOURCE_DIR}/lifx/timer.c - ${LIGHTSD_SOURCE_DIR}/lifx/wire_proto.c - ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c - ${CMAKE_CURRENT_SOURCE_DIR}/../tests_utils.c - ) --TARGET_LINK_LIBRARIES(test_core_proto ${TIME_MONOTONIC_LIBRARY}) --IF (HAVE_LIBBSD) -- TARGET_LINK_LIBRARIES(test_core_proto ${LIBBSD_LIBRARY}) --ENDIF (HAVE_LIBBSD) - - FUNCTION(ADD_ROUTER_TEST TEST_SOURCE) - ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_core_proto) -diff --git a/tests/core/proto/test_proto_get_light_state.c b/tests/core/proto/test_proto_get_light_state.c ---- a/tests/core/proto/test_proto_get_light_state.c -+++ b/tests/core/proto/test_proto_get_light_state.c -@@ -1,6 +1,7 @@ - #include "proto.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - #include "tests_utils.h" - - #define MOCKED_ROUTER_TARGETS_TO_DEVICES -@@ -29,17 +30,20 @@ - static struct lgtd_router_device_list devices = - SLIST_HEAD_INITIALIZER(&devices); - -+ static struct lgtd_lifx_gateway gw_bulb_1 = { -+ .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_1.bulbs) -+ }; - static struct lgtd_lifx_bulb bulb_1 = { - .addr = { 1, 2, 3, 4, 5 }, -- .state = { -- .hue = 0xaaaa, -+ .state = { .hue = 0xaaaa, - .saturation = 0xffff, - .brightness = 0xbbbb, - .kelvin = 3600, - .label = "wave", - .power = LGTD_LIFX_POWER_ON, - .tags = 0 -- } -+ }, -+ .gw = &gw_bulb_1 - }; - static struct lgtd_router_device device_1 = { .device = &bulb_1 }; - SLIST_INSERT_HEAD(&devices, &device_1, link); -@@ -76,7 +80,7 @@ - int - main(void) - { -- struct lgtd_client client; -+ struct lgtd_client client = { .io = FAKE_BUFFEREVENT }; - struct lgtd_proto_target_list *targets = (void *)0x2a; - - lgtd_proto_get_light_state(&client, targets); -diff --git a/tests/core/proto/test_proto_get_light_state_empty_device_list.c b/tests/core/proto/test_proto_get_light_state_empty_device_list.c ---- a/tests/core/proto/test_proto_get_light_state_empty_device_list.c -+++ b/tests/core/proto/test_proto_get_light_state_empty_device_list.c -@@ -1,6 +1,7 @@ - #include "proto.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - #include "tests_utils.h" - - #define MOCKED_ROUTER_TARGETS_TO_DEVICES -@@ -35,7 +36,7 @@ - int - main(void) - { -- struct lgtd_client client; -+ struct lgtd_client client = { .io = FAKE_BUFFEREVENT }; - struct lgtd_proto_target_list *targets = (void *)0x2a; - - lgtd_proto_get_light_state(&client, targets); -diff --git a/tests/core/proto/test_proto_get_light_state_null_device_list.c b/tests/core/proto/test_proto_get_light_state_null_device_list.c ---- a/tests/core/proto/test_proto_get_light_state_null_device_list.c -+++ b/tests/core/proto/test_proto_get_light_state_null_device_list.c -@@ -1,6 +1,7 @@ - #include "proto.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - #include "tests_utils.h" - - #define MOCKED_ROUTER_TARGETS_TO_DEVICES -@@ -45,7 +46,7 @@ - int - main(void) - { -- struct lgtd_client client; -+ struct lgtd_client client = { .io = FAKE_BUFFEREVENT }; - struct lgtd_proto_target_list *targets = (void *)0x2a; - - lgtd_proto_get_light_state(&client, targets); -diff --git a/tests/core/proto/test_proto_power_off.c b/tests/core/proto/test_proto_power_off.c ---- a/tests/core/proto/test_proto_power_off.c -+++ b/tests/core/proto/test_proto_power_off.c -@@ -1,6 +1,7 @@ - #include "proto.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - #include "tests_utils.h" - - #define MOCKED_CLIENT_SEND_RESPONSE -@@ -38,7 +39,7 @@ - lgtd_client_send_response(struct lgtd_client *client, const char *msg) - { - if (!client) { -- errx(1, "client shouldn't ne NULL"); -+ errx(1, "client shouldn't be NULL"); - } - - if (strcmp(msg, "true")) { -@@ -52,7 +53,7 @@ - struct lgtd_proto_target_list *targets; - targets = lgtd_tests_build_target_list("*", NULL); - -- struct lgtd_client client; -+ struct lgtd_client client = { .io = FAKE_BUFFEREVENT }; - - lgtd_proto_power_off(&client, targets); - -diff --git a/tests/core/proto/test_proto_power_off_routing_error.c b/tests/core/proto/test_proto_power_off_routing_error.c ---- a/tests/core/proto/test_proto_power_off_routing_error.c -+++ b/tests/core/proto/test_proto_power_off_routing_error.c -@@ -1,6 +1,7 @@ - #include "proto.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - #include "tests_utils.h" - - #define MOCKED_CLIENT_SEND_RESPONSE -@@ -52,7 +53,7 @@ - struct lgtd_proto_target_list *targets; - targets = lgtd_tests_build_target_list("*", NULL); - -- struct lgtd_client client; -+ struct lgtd_client client = { .io = FAKE_BUFFEREVENT }; - - lgtd_proto_power_off(&client, targets); - -diff --git a/tests/core/proto/test_proto_power_on.c b/tests/core/proto/test_proto_power_on.c ---- a/tests/core/proto/test_proto_power_on.c -+++ b/tests/core/proto/test_proto_power_on.c -@@ -1,6 +1,7 @@ - #include "proto.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - #include "tests_utils.h" - - #define MOCKED_CLIENT_SEND_RESPONSE -@@ -52,7 +53,7 @@ - struct lgtd_proto_target_list *targets; - targets = lgtd_tests_build_target_list("*", NULL); - -- struct lgtd_client client; -+ struct lgtd_client client = { .io = FAKE_BUFFEREVENT }; - - lgtd_proto_power_on(&client, targets); - -diff --git a/tests/core/proto/test_proto_power_on_routing_error.c b/tests/core/proto/test_proto_power_on_routing_error.c ---- a/tests/core/proto/test_proto_power_on_routing_error.c -+++ b/tests/core/proto/test_proto_power_on_routing_error.c -@@ -1,6 +1,7 @@ - #include "proto.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - #include "tests_utils.h" - - #define MOCKED_CLIENT_SEND_RESPONSE -@@ -52,7 +53,7 @@ - struct lgtd_proto_target_list *targets; - targets = lgtd_tests_build_target_list("*", NULL); - -- struct lgtd_client client; -+ struct lgtd_client client = { .io = FAKE_BUFFEREVENT }; - - lgtd_proto_power_on(&client, targets); - -diff --git a/tests/core/proto/test_proto_set_light_from_hsbk.c b/tests/core/proto/test_proto_set_light_from_hsbk.c ---- a/tests/core/proto/test_proto_set_light_from_hsbk.c -+++ b/tests/core/proto/test_proto_set_light_from_hsbk.c -@@ -3,6 +3,7 @@ - #include "proto.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - #include "tests_utils.h" - - #define MOCKED_CLIENT_SEND_RESPONSE -diff --git a/tests/core/proto/test_proto_set_light_from_hsbk_on_routing_error.c b/tests/core/proto/test_proto_set_light_from_hsbk_on_routing_error.c ---- a/tests/core/proto/test_proto_set_light_from_hsbk_on_routing_error.c -+++ b/tests/core/proto/test_proto_set_light_from_hsbk_on_routing_error.c -@@ -3,6 +3,7 @@ - #include "proto.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - #include "tests_utils.h" - - #define MOCKED_CLIENT_SEND_RESPONSE -diff --git a/tests/core/proto/test_proto_set_waveform.c b/tests/core/proto/test_proto_set_waveform.c ---- a/tests/core/proto/test_proto_set_waveform.c -+++ b/tests/core/proto/test_proto_set_waveform.c -@@ -3,6 +3,7 @@ - #include "proto.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - #include "tests_utils.h" - - #define MOCKED_CLIENT_SEND_RESPONSE -@@ -35,7 +36,7 @@ - int brightness = le16toh(waveform->brightness); - int kelvin = le16toh(waveform->kelvin); - int period = le32toh(waveform->period); -- float cycles = lifx_wire_lefloattoh(waveform->cycles); -+ float cycles = lgtd_lifx_wire_lefloattoh(waveform->cycles); - int skew_ratio = le16toh(waveform->skew_ratio); - - if (waveform_type != LGTD_LIFX_WAVEFORM_SAW) { -diff --git a/tests/core/proto/test_proto_set_waveform_on_routing_error.c b/tests/core/proto/test_proto_set_waveform_on_routing_error.c ---- a/tests/core/proto/test_proto_set_waveform_on_routing_error.c -+++ b/tests/core/proto/test_proto_set_waveform_on_routing_error.c -@@ -3,6 +3,7 @@ - #include "proto.c" - - #include "mock_client_buf.h" -+#include "mock_gateway.h" - #include "tests_utils.h" - - #define MOCKED_CLIENT_SEND_RESPONSE -@@ -35,7 +36,7 @@ - int brightness = le16toh(waveform->brightness); - int kelvin = le16toh(waveform->kelvin); - int period = le32toh(waveform->period); -- float cycles = lifx_wire_lefloattoh(waveform->cycles); -+ float cycles = lgtd_lifx_wire_lefloattoh(waveform->cycles); - int skew_ratio = le16toh(waveform->skew_ratio); - - if (waveform_type != LGTD_LIFX_WAVEFORM_SAW) { -diff --git a/tests/core/proto/test_proto_tag_create.c b/tests/core/proto/test_proto_tag_create.c -new file mode 100644 ---- /dev/null -+++ b/tests/core/proto/test_proto_tag_create.c -@@ -0,0 +1,252 @@ -+#include "proto.c" -+ -+#include "mock_client_buf.h" -+#define MOCKED_LIFX_GATEWAY_SEND_TO_SITE -+#define MOCKED_LIFX_GATEWAY_ALLOCATE_TAG_ID -+#include "mock_gateway.h" -+#include "tests_utils.h" -+ -+#define MOCKED_ROUTER_TARGETS_TO_DEVICES -+#define MOCKED_ROUTER_SEND_TO_DEVICE -+#define MOCKED_ROUTER_DEVICE_LIST_FREE -+#include "tests_proto_utils.h" -+ -+#define FAKE_TARGET_LIST (void *)0x2a -+ -+static struct lgtd_router_device_list devices = -+ SLIST_HEAD_INITIALIZER(&devices); -+static struct lgtd_router_device_list device_1_only = -+ SLIST_HEAD_INITIALIZER(&device_1_only); -+ -+static bool send_to_device_called = false; -+ -+void -+lgtd_router_send_to_device(struct lgtd_lifx_bulb *bulb, -+ enum lgtd_lifx_packet_type pkt_type, -+ void *pkt) -+{ -+ if (!bulb) { -+ errx(1, "lgtd_router_send_to_device must be called with a bulb"); -+ } -+ -+ uint8_t expected_addr[LGTD_LIFX_ADDR_LENGTH] = { 1, 2, 3, 4, 5 }; -+ if (memcmp(bulb->addr, expected_addr, LGTD_LIFX_ADDR_LENGTH)) { -+ errx( -+ 1, "got bulb with addr %s (expected %s)", -+ lgtd_addrtoa(bulb->addr), lgtd_addrtoa(expected_addr) -+ ); -+ } -+ -+ if (pkt_type != LGTD_LIFX_SET_TAGS) { -+ errx( -+ 1, "got packet type %d (expected %d)", pkt_type, LGTD_LIFX_SET_TAGS -+ ); -+ } -+ -+ if (!pkt) { -+ errx(1, "missing SET_TAGS payload"); -+ } -+ -+ const struct lgtd_lifx_packet_tags *pkt_tags = pkt; -+ uint64_t tags = le64toh(pkt_tags->tags); -+ if (tags != 0x1) { -+ errx( -+ 1, "invalid SET_TAGS payload=%#jx (expected %#x)", -+ (uintmax_t)tags, 0x1 -+ ); -+ } -+ -+ send_to_device_called = true; -+} -+ -+static bool gateway_send_to_site_called = false; -+ -+bool -+lgtd_lifx_gateway_send_to_site(struct lgtd_lifx_gateway *gw, -+ enum lgtd_lifx_packet_type pkt_type, -+ const void *pkt) -+{ -+ if (!gw) { -+ errx(1, "missing gateway"); -+ } -+ -+ if (pkt_type != LGTD_LIFX_SET_TAG_LABELS) { -+ errx( -+ 1, "got packet type %#x (expected %#x)", -+ pkt_type, LGTD_LIFX_SET_TAG_LABELS -+ ); -+ } -+ -+ const struct lgtd_lifx_packet_tag_labels *pkt_tag_labels = pkt; -+ uint64_t tags = le64toh(pkt_tag_labels->tags); -+ if (tags != 0x1) { -+ errx(1, "got tags %#jx (expected %#x)", (uintmax_t)tags, 0x1); -+ } -+ -+ if (strcmp(pkt_tag_labels->label, "dub")) { -+ errx(1, "got label %s (expected dub)", pkt_tag_labels->label); -+ } -+ -+ gateway_send_to_site_called = true; -+ -+ return true; -+} -+ -+static bool gateway_allocate_tag_id_called = false; -+ -+int -+lgtd_lifx_gateway_allocate_tag_id(struct lgtd_lifx_gateway *gw, -+ int tag_id, -+ const char *tag_label) -+{ -+ if (gateway_allocate_tag_id_called) { -+ errx( -+ 1, "lgtd_lifx_gateway_allocate_tag_id " -+ "should have been called once only" -+ ); -+ } -+ -+ if (tag_id != -1) { -+ errx( -+ 1, "lgtd_lifx_gateway_allocate_tag_id " -+ "tag_id %d (expected -1)", tag_id -+ ); -+ } -+ -+ if (!gw) { -+ errx( -+ 1, "lgtd_lifx_gateway_allocate_tag_id " -+ "must be called with gateway" -+ ); -+ } -+ -+ if (!tag_label) { -+ errx( -+ 1, "lgtd_lifx_gateway_allocate_tag_id " -+ "must be called with a tag_label" -+ ); -+ } -+ -+ tag_id = 0; -+ -+ struct lgtd_lifx_tag *tag = lgtd_lifx_tagging_find_tag(tag_label); -+ if (!tag) { -+ errx(1, "tag %s wasn't found", tag_label); -+ } -+ lgtd_tests_add_tag_to_gw(tag, gw, tag_id); -+ -+ gateway_allocate_tag_id_called = true; -+ -+ return tag_id; -+} -+ -+static bool device_list_free_called = false; -+ -+void -+lgtd_router_device_list_free(struct lgtd_router_device_list *devices) -+{ -+ if (!devices) { -+ lgtd_errx(1, "the device list must be passed"); -+ } -+ -+ device_list_free_called = true; -+} -+ -+struct lgtd_router_device_list * -+lgtd_router_targets_to_devices(const struct lgtd_proto_target_list *targets) -+{ -+ if (targets != FAKE_TARGET_LIST) { -+ lgtd_errx(1, "unexpected targets list"); -+ } -+ -+ return &device_1_only; -+} -+ -+static void -+setup_devices(void) -+{ -+ static struct lgtd_lifx_gateway gw_bulb_1 = { -+ .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_1.bulbs) -+ }; -+ static struct lgtd_lifx_bulb bulb_1 = { -+ .addr = { 1, 2, 3, 4, 5 }, -+ .state = { -+ .hue = 0xaaaa, -+ .saturation = 0xffff, -+ .brightness = 0xbbbb, -+ .kelvin = 3600, -+ .label = "wave", -+ .power = LGTD_LIFX_POWER_ON, -+ .tags = 0 -+ }, -+ .gw = &gw_bulb_1 -+ }; -+ static struct lgtd_router_device device_1 = { .device = &bulb_1 }; -+ SLIST_INSERT_HEAD(&devices, &device_1, link); -+ SLIST_INSERT_HEAD(&device_1_only, &device_1, link); -+ -+ struct lgtd_lifx_tag *gw_2_tag_1 = lgtd_tests_insert_mock_tag("vapor"); -+ struct lgtd_lifx_tag *gw_2_tag_2 = lgtd_tests_insert_mock_tag("d^-^b"); -+ struct lgtd_lifx_tag *gw_2_tag_3 = lgtd_tests_insert_mock_tag("wave~"); -+ static struct lgtd_lifx_gateway gw_bulb_2 = { -+ .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_2.bulbs), -+ .tag_ids = 0x7 -+ }; -+ lgtd_tests_add_tag_to_gw(gw_2_tag_1, &gw_bulb_2, 0); -+ lgtd_tests_add_tag_to_gw(gw_2_tag_2, &gw_bulb_2, 1); -+ lgtd_tests_add_tag_to_gw(gw_2_tag_3, &gw_bulb_2, 2); -+ static struct lgtd_lifx_bulb bulb_2 = { -+ .addr = { 5, 4, 3, 2, 1 }, -+ .state = { -+ .hue = 0x0000, -+ .saturation = 0x0000, -+ .brightness = 0xffff, -+ .kelvin = 4000, -+ .label = "", -+ .power = LGTD_LIFX_POWER_OFF, -+ .tags = 0x3 -+ }, -+ .gw = &gw_bulb_2 -+ }; -+ static struct lgtd_router_device device_2 = { .device = &bulb_2 }; -+ SLIST_INSERT_HEAD(&devices, &device_2, link); -+} -+ -+int -+main(void) -+{ -+ struct lgtd_client client = { .io = FAKE_BUFFEREVENT }; -+ -+ setup_devices(); -+ -+ lgtd_proto_tag(&client, FAKE_TARGET_LIST, "dub"); -+ -+ const char expected[] = "true"; -+ if (client_write_buf_idx != sizeof(expected) - 1) { -+ lgtd_errx( -+ 1, -+ "%d bytes written, expected %lu " -+ "(got %.*s instead of %s)", -+ client_write_buf_idx, sizeof(expected) - 1UL, -+ client_write_buf_idx, client_write_buf, expected -+ ); -+ } -+ if (memcmp(expected, client_write_buf, sizeof(expected) - 1)) { -+ lgtd_errx( -+ 1, "got %.*s instead of %s", -+ client_write_buf_idx, client_write_buf, expected -+ ); -+ } -+ -+ if (!gateway_send_to_site_called) { -+ lgtd_errx(1, "SET_TAG_LABELS wasn't sent"); -+ } -+ if (!device_list_free_called) { -+ lgtd_errx(1, "the list of devices hasn't been freed"); -+ } -+ if (!send_to_device_called) { -+ lgtd_errx(1, "SET_TAGS wasn't send to any device"); -+ } -+ -+ return 0; -+} -diff --git a/tests/core/proto/test_proto_tag_create_lifx_gw_tag_ids_full.c b/tests/core/proto/test_proto_tag_create_lifx_gw_tag_ids_full.c -new file mode 100644 ---- /dev/null -+++ b/tests/core/proto/test_proto_tag_create_lifx_gw_tag_ids_full.c -@@ -0,0 +1,208 @@ -+#include "proto.c" -+ -+#include "mock_client_buf.h" -+#define MOCKED_LIFX_GATEWAY_SEND_TO_SITE -+#define MOCKED_LIFX_GATEWAY_ALLOCATE_TAG_ID -+#include "mock_gateway.h" -+#include "tests_utils.h" -+ -+#define MOCKED_CLIENT_SEND_ERROR -+#define MOCKED_ROUTER_TARGETS_TO_DEVICES -+#define MOCKED_ROUTER_SEND_TO_DEVICE -+#define MOCKED_ROUTER_DEVICE_LIST_FREE -+#include "tests_proto_utils.h" -+ -+#define FAKE_TARGET_LIST (void *)0x2a -+ -+static struct lgtd_router_device_list devices = -+ SLIST_HEAD_INITIALIZER(&devices); -+static struct lgtd_router_device_list device_1_only = -+ SLIST_HEAD_INITIALIZER(&device_1_only); -+ -+static bool client_send_error_called = false; -+ -+void -+lgtd_client_send_error(struct lgtd_client *client, -+ enum lgtd_client_error_code error, -+ const char *msg) -+{ -+ if (!client) { -+ errx(1, "client_send_error called without a client"); -+ } -+ -+ if (!error) { -+ errx(1, "client_send_error called without an error code"); -+ } -+ -+ if (!msg) { -+ errx(1, "client_send_error called without an error message"); -+ } -+ -+ client_send_error_called = true; -+} -+ -+static bool send_to_device_called = false; -+ -+void -+lgtd_router_send_to_device(struct lgtd_lifx_bulb *bulb, -+ enum lgtd_lifx_packet_type pkt_type, -+ void *pkt) -+{ -+ (void)bulb; -+ (void)pkt_type; -+ (void)pkt; -+ -+ send_to_device_called = true; -+} -+ -+static bool gateway_send_to_site_called = false; -+ -+bool -+lgtd_lifx_gateway_send_to_site(struct lgtd_lifx_gateway *gw, -+ enum lgtd_lifx_packet_type pkt_type, -+ const void *pkt) -+{ -+ (void)gw; -+ (void)pkt_type; -+ (void)pkt; -+ -+ gateway_send_to_site_called = true; -+ -+ return true; -+} -+ -+static bool gateway_allocate_tag_id_called = false; -+ -+int -+lgtd_lifx_gateway_allocate_tag_id(struct lgtd_lifx_gateway *gw, -+ int tag_id, -+ const char *tag_label) -+{ -+ if (gateway_allocate_tag_id_called) { -+ errx( -+ 1, "lgtd_lifx_gateway_allocate_tag_id " -+ "should have been called once only" -+ ); -+ } -+ -+ if (tag_id != -1) { -+ errx( -+ 1, "lgtd_lifx_gateway_allocate_tag_id " -+ "tag_id %d (expected -1)", tag_id -+ ); -+ } -+ -+ if (!gw) { -+ errx( -+ 1, "lgtd_lifx_gateway_allocate_tag_id " -+ "must be called with gateway" -+ ); -+ } -+ -+ if (!tag_label) { -+ errx( -+ 1, "lgtd_lifx_gateway_allocate_tag_id " -+ "must be called with a tag_label" -+ ); -+ } -+ -+ return -1; // no more tag id available -+} -+ -+static bool device_list_free_called = false; -+ -+void -+lgtd_router_device_list_free(struct lgtd_router_device_list *devices) -+{ -+ if (!devices) { -+ lgtd_errx(1, "the device list must be passed"); -+ } -+ -+ device_list_free_called = true; -+} -+ -+struct lgtd_router_device_list * -+lgtd_router_targets_to_devices(const struct lgtd_proto_target_list *targets) -+{ -+ if (targets != FAKE_TARGET_LIST) { -+ lgtd_errx(1, "unexpected targets list"); -+ } -+ -+ return &device_1_only; -+} -+ -+static void -+setup_devices(void) -+{ -+ static struct lgtd_lifx_gateway gw_bulb_1 = { -+ .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_1.bulbs) -+ }; -+ static struct lgtd_lifx_bulb bulb_1 = { -+ .addr = { 1, 2, 3, 4, 5 }, -+ .state = { -+ .hue = 0xaaaa, -+ .saturation = 0xffff, -+ .brightness = 0xbbbb, -+ .kelvin = 3600, -+ .label = "wave", -+ .power = LGTD_LIFX_POWER_ON, -+ .tags = 0 -+ }, -+ .gw = &gw_bulb_1 -+ }; -+ static struct lgtd_router_device device_1 = { .device = &bulb_1 }; -+ SLIST_INSERT_HEAD(&devices, &device_1, link); -+ SLIST_INSERT_HEAD(&device_1_only, &device_1, link); -+ -+ struct lgtd_lifx_tag *gw_2_tag_1 = lgtd_tests_insert_mock_tag("vapor"); -+ struct lgtd_lifx_tag *gw_2_tag_2 = lgtd_tests_insert_mock_tag("d^-^b"); -+ struct lgtd_lifx_tag *gw_2_tag_3 = lgtd_tests_insert_mock_tag("wave~"); -+ static struct lgtd_lifx_gateway gw_bulb_2 = { -+ .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_2.bulbs), -+ .tag_ids = 0x7 -+ }; -+ lgtd_tests_add_tag_to_gw(gw_2_tag_1, &gw_bulb_2, 0); -+ lgtd_tests_add_tag_to_gw(gw_2_tag_2, &gw_bulb_2, 1); -+ lgtd_tests_add_tag_to_gw(gw_2_tag_3, &gw_bulb_2, 2); -+ static struct lgtd_lifx_bulb bulb_2 = { -+ .addr = { 5, 4, 3, 2, 1 }, -+ .state = { -+ .hue = 0x0000, -+ .saturation = 0x0000, -+ .brightness = 0xffff, -+ .kelvin = 4000, -+ .label = "", -+ .power = LGTD_LIFX_POWER_OFF, -+ .tags = 0x3 -+ }, -+ .gw = &gw_bulb_2 -+ }; -+ static struct lgtd_router_device device_2 = { .device = &bulb_2 }; -+ SLIST_INSERT_HEAD(&devices, &device_2, link); -+} -+ -+int -+main(void) -+{ -+ struct lgtd_client client = { .io = FAKE_BUFFEREVENT }; -+ -+ setup_devices(); -+ -+ lgtd_proto_tag(&client, FAKE_TARGET_LIST, "dub"); -+ -+ -+ if (gateway_send_to_site_called) { -+ lgtd_errx(1, "SET_TAG_LABELS shouldn't have been sent"); -+ } -+ if (!device_list_free_called) { -+ lgtd_errx(1, "the list of devices hasn't been freed"); -+ } -+ if (send_to_device_called) { -+ lgtd_errx(1, "SET_TAGS shouldn't have been to any device"); -+ } -+ if (!client_send_error_called) { -+ lgtd_errx(1, "client_send_error should have been called"); -+ } -+ -+ return 0; -+} -diff --git a/tests/core/proto/test_proto_tag_update.c b/tests/core/proto/test_proto_tag_update.c -new file mode 100644 ---- /dev/null -+++ b/tests/core/proto/test_proto_tag_update.c -@@ -0,0 +1,282 @@ -+#include "proto.c" -+ -+#include "mock_client_buf.h" -+#define MOCKED_LIFX_GATEWAY_SEND_TO_SITE -+#define MOCKED_LIFX_GATEWAY_ALLOCATE_TAG_ID -+#include "mock_gateway.h" -+#include "tests_utils.h" -+ -+#define MOCKED_ROUTER_TARGETS_TO_DEVICES -+#define MOCKED_ROUTER_SEND_TO_DEVICE -+#define MOCKED_ROUTER_DEVICE_LIST_FREE -+#include "tests_proto_utils.h" -+ -+#define FAKE_TARGET_LIST (void *)0x2a -+ -+static struct lgtd_router_device_list devices = -+ SLIST_HEAD_INITIALIZER(&devices); -+ -+static bool send_to_device_called = false; -+ -+void -+lgtd_router_send_to_device(struct lgtd_lifx_bulb *bulb, -+ enum lgtd_lifx_packet_type pkt_type, -+ void *pkt) -+{ -+ if (send_to_device_called) { -+ errx(1, "lgtd_router_send_to_device should have been called once only"); -+ } -+ -+ if (!bulb) { -+ errx(1, "lgtd_router_send_to_device must be called with a bulb"); -+ } -+ -+ uint8_t expected_addr[LGTD_LIFX_ADDR_LENGTH] = { 5, 4, 3, 2, 1 }; -+ if (memcmp(bulb->addr, expected_addr, LGTD_LIFX_ADDR_LENGTH)) { -+ errx( -+ 1, "got bulb with addr %s (expected %s)", -+ lgtd_addrtoa(bulb->addr), lgtd_addrtoa(expected_addr) -+ ); -+ } -+ -+ if (pkt_type != LGTD_LIFX_SET_TAGS) { -+ errx( -+ 1, "got packet type %d (expected %d)", pkt_type, LGTD_LIFX_SET_TAGS -+ ); -+ } -+ -+ if (!pkt) { -+ errx(1, "missing SET_TAGS payload"); -+ } -+ -+ const struct lgtd_lifx_packet_tags *pkt_tags = pkt; -+ uint64_t tags = le64toh(pkt_tags->tags); -+ -+ if (tags != 0x7) { -+ errx( -+ 1, "invalid SET_TAGS payload=%#jx (expected %#x)", -+ (uintmax_t)tags, 0x7 -+ ); -+ } -+ -+ send_to_device_called = true; -+} -+ -+static bool gateway_send_to_site_called_for_gw_1 = false; -+static bool gateway_send_to_site_called_for_gw_2 = false; -+ -+bool -+lgtd_lifx_gateway_send_to_site(struct lgtd_lifx_gateway *gw, -+ enum lgtd_lifx_packet_type pkt_type, -+ const void *pkt) -+{ -+ if (!gw) { -+ errx(1, "missing gateway"); -+ } -+ -+ if (pkt_type != LGTD_LIFX_SET_TAG_LABELS) { -+ errx( -+ 1, "got packet type %#x (expected %#x)", -+ pkt_type, LGTD_LIFX_SET_TAG_LABELS -+ ); -+ } -+ -+ const struct lgtd_lifx_packet_tag_labels *pkt_tag_labels = pkt; -+ uint64_t tags = le64toh(pkt_tag_labels->tags); -+ -+ if (strcmp(pkt_tag_labels->label, "dub")) { -+ errx(1, "got label %s (expected dub)", pkt_tag_labels->label); -+ } -+ -+ if (gw->site.as_integer == 42) { -+ if (tags != 0x1) { -+ errx(1, "got tags %#jx (expected %#x)", (uintmax_t)tags, 0x1); -+ } -+ if (gateway_send_to_site_called_for_gw_1) { -+ errx(1, "LGTD_LIFX_SET_TAG_LABELS already called for gw 1"); -+ } -+ gateway_send_to_site_called_for_gw_1 = true; -+ } else if (gw->site.as_integer == 44) { -+ if (tags != 0x4) { -+ errx(1, "got tags %#jx (expected %#x)", (uintmax_t)tags, 0x4); -+ } -+ if (gateway_send_to_site_called_for_gw_2) { -+ errx(1, "LGTD_LIFX_SET_TAG_LABELS already called for gw 2"); -+ } -+ gateway_send_to_site_called_for_gw_2 = true; -+ } else { -+ errx(1, "LGTD_LIFX_SET_TAG_LABELS received an invalid gateway"); -+ } -+ -+ return true; -+} -+ -+static bool gateway_allocate_tag_id_called = false; -+ -+int -+lgtd_lifx_gateway_allocate_tag_id(struct lgtd_lifx_gateway *gw, -+ int tag_id, -+ const char *tag_label) -+{ -+ if (gateway_allocate_tag_id_called) { -+ errx( -+ 1, "lgtd_lifx_gateway_allocate_tag_id " -+ "should have been called once only" -+ ); -+ } -+ -+ if (tag_id != -1) { -+ errx( -+ 1, "lgtd_lifx_gateway_allocate_tag_id " -+ "tag_id %d (expected -1)", tag_id -+ ); -+ } -+ -+ if (!gw) { -+ errx( -+ 1, "lgtd_lifx_gateway_allocate_tag_id " -+ "must be called with gateway" -+ ); -+ } -+ -+ if (!tag_label) { -+ errx( -+ 1, "lgtd_lifx_gateway_allocate_tag_id " -+ "must be called with a tag_label" -+ ); -+ } -+ -+ if (gw->site.as_integer != 44) { -+ errx( -+ 1, "lgtd_lifx_gateway_allocate_tag_id got the wrong gateway " -+ "%#jx (expected %d)", (uintmax_t)gw->site.as_integer, 44 -+ ); -+ } -+ -+ tag_id = 2; -+ -+ struct lgtd_lifx_tag *tag = lgtd_lifx_tagging_find_tag(tag_label); -+ if (!tag) { -+ errx(1, "tag %s wasn't found", tag_label); -+ } -+ lgtd_tests_add_tag_to_gw(tag, gw, tag_id); -+ -+ gateway_allocate_tag_id_called = true; -+ -+ return tag_id; -+} -+ -+static bool device_list_free_called = false; -+ -+void -+lgtd_router_device_list_free(struct lgtd_router_device_list *devices) -+{ -+ if (!devices) { -+ lgtd_errx(1, "the device list must be passed"); -+ } -+ -+ device_list_free_called = true; -+} -+ -+struct lgtd_router_device_list * -+lgtd_router_targets_to_devices(const struct lgtd_proto_target_list *targets) -+{ -+ if (targets != FAKE_TARGET_LIST) { -+ lgtd_errx(1, "unexpected targets list"); -+ } -+ -+ return &devices; -+} -+ -+static void -+setup_devices(void) -+{ -+ static struct lgtd_lifx_gateway gw_bulb_1 = { -+ .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_1.bulbs), -+ .site = { .as_integer = 42 } -+ }; -+ static struct lgtd_lifx_bulb bulb_1 = { -+ .addr = { 1, 2, 3, 4, 5 }, -+ .state = { -+ .hue = 0xaaaa, -+ .saturation = 0xffff, -+ .brightness = 0xbbbb, -+ .kelvin = 3600, -+ .label = "wave", -+ .power = LGTD_LIFX_POWER_ON, -+ .tags = 1 -+ }, -+ .gw = &gw_bulb_1 -+ }; -+ static struct lgtd_router_device device_1 = { .device = &bulb_1 }; -+ SLIST_INSERT_HEAD(&devices, &device_1, link); -+ struct lgtd_lifx_tag *gw_1_tag_1 = lgtd_tests_insert_mock_tag("dub"); -+ lgtd_tests_add_tag_to_gw(gw_1_tag_1, &gw_bulb_1, 0); -+ -+ struct lgtd_lifx_tag *gw_2_tag_1 = lgtd_tests_insert_mock_tag("vapor"); -+ struct lgtd_lifx_tag *gw_2_tag_2 = lgtd_tests_insert_mock_tag("d^-^b"); -+ static struct lgtd_lifx_gateway gw_bulb_2 = { -+ .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_2.bulbs), -+ .site = { .as_integer = 44 }, -+ .tag_ids = 0x3 -+ }; -+ lgtd_tests_add_tag_to_gw(gw_2_tag_1, &gw_bulb_2, 0); -+ lgtd_tests_add_tag_to_gw(gw_2_tag_2, &gw_bulb_2, 1); -+ static struct lgtd_lifx_bulb bulb_2 = { -+ .addr = { 5, 4, 3, 2, 1 }, -+ .state = { -+ .hue = 0x0000, -+ .saturation = 0x0000, -+ .brightness = 0xffff, -+ .kelvin = 4000, -+ .label = "", -+ .power = LGTD_LIFX_POWER_OFF, -+ .tags = 0x3 -+ }, -+ .gw = &gw_bulb_2 -+ }; -+ static struct lgtd_router_device device_2 = { .device = &bulb_2 }; -+ SLIST_INSERT_HEAD(&devices, &device_2, link); -+} -+ -+int -+main(void) -+{ -+ struct lgtd_client client = { .io = FAKE_BUFFEREVENT }; -+ -+ setup_devices(); -+ -+ lgtd_proto_tag(&client, FAKE_TARGET_LIST, "dub"); -+ -+ const char expected[] = "true"; -+ if (client_write_buf_idx != sizeof(expected) - 1) { -+ lgtd_errx( -+ 1, -+ "%d bytes written, expected %lu " -+ "(got %.*s instead of %s)", -+ client_write_buf_idx, sizeof(expected) - 1UL, -+ client_write_buf_idx, client_write_buf, expected -+ ); -+ } -+ if (memcmp(expected, client_write_buf, sizeof(expected) - 1)) { -+ lgtd_errx( -+ 1, "got %.*s instead of %s", -+ client_write_buf_idx, client_write_buf, expected -+ ); -+ } -+ -+ if (!gateway_send_to_site_called_for_gw_1) { -+ lgtd_errx(1, "SET_TAG_LABELS wasn't sent to gw 1"); -+ } -+ if (!gateway_send_to_site_called_for_gw_2) { -+ lgtd_errx(1, "SET_TAG_LABELS wasn't sent to gw 2"); -+ } -+ if (!device_list_free_called) { -+ lgtd_errx(1, "the list of devices hasn't been freed"); -+ } -+ if (!send_to_device_called) { -+ lgtd_errx(1, "SET_TAGS wasn't send to any device"); -+ } -+ -+ return 0; -+} -diff --git a/tests/core/proto/test_proto_untag.c b/tests/core/proto/test_proto_untag.c -new file mode 100644 ---- /dev/null -+++ b/tests/core/proto/test_proto_untag.c -@@ -0,0 +1,169 @@ -+#include "proto.c" -+ -+#include "mock_client_buf.h" -+#include "mock_gateway.h" -+#include "tests_utils.h" -+ -+#define MOCKED_ROUTER_TARGETS_TO_DEVICES -+#define MOCKED_ROUTER_SEND_TO_DEVICE -+#define MOCKED_ROUTER_DEVICE_LIST_FREE -+#include "tests_proto_utils.h" -+ -+static bool device_list_free_called = false; -+ -+void -+lgtd_router_device_list_free(struct lgtd_router_device_list *devices) -+{ -+ if (device_list_free_called) { -+ errx(1, "the device list should have been freed once"); -+ } -+ -+ if (!devices) { -+ errx(1, "the device list must be passed"); -+ } -+ -+ device_list_free_called = true; -+} -+ -+static struct lgtd_lifx_tag *tag_vapor = NULL; -+ -+struct lgtd_router_device_list * -+lgtd_router_targets_to_devices(const struct lgtd_proto_target_list *targets) -+{ -+ if (targets != (void *)0x2a) { -+ lgtd_errx(1, "unexpected targets list"); -+ } -+ -+ static struct lgtd_router_device_list devices = -+ SLIST_HEAD_INITIALIZER(&devices); -+ -+ static struct lgtd_lifx_gateway gw_bulb_1 = { -+ .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_1.bulbs) -+ }; -+ static struct lgtd_lifx_bulb bulb_1 = { -+ .addr = { 1, 2, 3, 4, 5 }, -+ .state = { -+ .hue = 0xaaaa, -+ .saturation = 0xffff, -+ .brightness = 0xbbbb, -+ .kelvin = 3600, -+ .label = "wave", -+ .power = LGTD_LIFX_POWER_ON, -+ .tags = 0 -+ }, -+ .gw = &gw_bulb_1 -+ }; -+ static struct lgtd_router_device device_1 = { .device = &bulb_1 }; -+ SLIST_INSERT_HEAD(&devices, &device_1, link); -+ -+ struct lgtd_lifx_tag *gw_2_tag_2 = lgtd_tests_insert_mock_tag("d^-^b"); -+ struct lgtd_lifx_tag *gw_2_tag_3 = lgtd_tests_insert_mock_tag("wave~"); -+ static struct lgtd_lifx_gateway gw_bulb_2 = { -+ .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_2.bulbs), -+ .tag_ids = 0x7 -+ }; -+ lgtd_tests_add_tag_to_gw(tag_vapor, &gw_bulb_2, 0); -+ lgtd_tests_add_tag_to_gw(gw_2_tag_2, &gw_bulb_2, 1); -+ lgtd_tests_add_tag_to_gw(gw_2_tag_3, &gw_bulb_2, 2); -+ static struct lgtd_lifx_bulb bulb_2 = { -+ .addr = { 5, 4, 3, 2, 1 }, -+ .state = { -+ .hue = 0x0000, -+ .saturation = 0x0000, -+ .brightness = 0xffff, -+ .kelvin = 4000, -+ .label = "", -+ .power = LGTD_LIFX_POWER_OFF, -+ .tags = 0x3 -+ }, -+ .gw = &gw_bulb_2 -+ }; -+ static struct lgtd_router_device device_2 = { .device = &bulb_2 }; -+ SLIST_INSERT_HEAD(&devices, &device_2, link); -+ -+ return &devices; -+} -+ -+static bool send_to_device_called = false; -+ -+void -+lgtd_router_send_to_device(struct lgtd_lifx_bulb *bulb, -+ enum lgtd_lifx_packet_type pkt_type, -+ void *pkt) -+{ -+ if (send_to_device_called) { -+ errx(1, "lgtd_router_send_to_device should have been called once"); -+ } -+ -+ if (!bulb) { -+ errx(1, "lgtd_router_send_to_device must be called with a bulb"); -+ } -+ -+ uint8_t expected_addr[LGTD_LIFX_ADDR_LENGTH] = { 5, 4, 3, 2, 1 }; -+ if (memcmp(bulb->addr, expected_addr, LGTD_LIFX_ADDR_LENGTH)) { -+ errx( -+ 1, "got bulb with addr %s (expected %s)", -+ lgtd_addrtoa(bulb->addr), lgtd_addrtoa(expected_addr) -+ ); -+ } -+ -+ if (pkt_type != LGTD_LIFX_SET_TAGS) { -+ errx( -+ 1, "got packet type %d (expected %d)", pkt_type, LGTD_LIFX_SET_TAGS -+ ); -+ } -+ -+ if (!pkt) { -+ errx(1, "missing SET_TAGS payload"); -+ } -+ -+ struct lgtd_lifx_packet_tags *pkt_tags = pkt; -+ if (pkt_tags->tags != 0x2) { -+ errx( -+ 1, "invalid SET_TAGS payload=%#jx (expected %#x)", -+ (uintmax_t)pkt_tags->tags, 0x2 -+ ); -+ } -+ -+ send_to_device_called = true; -+} -+ -+int -+main(void) -+{ -+ struct lgtd_client client = { .io = FAKE_BUFFEREVENT }; -+ -+ struct lgtd_proto_target_list *targets = (void *)0x2a; -+ -+ tag_vapor = lgtd_tests_insert_mock_tag("vapor"); -+ -+ lgtd_proto_untag(&client, targets, "vapor"); -+ -+ const char expected[] = "true"; -+ -+ if (client_write_buf_idx != sizeof(expected) - 1) { -+ lgtd_errx( -+ 1, -+ "%d bytes written, expected %lu " -+ "(got %.*s instead of %s)", -+ client_write_buf_idx, sizeof(expected) - 1UL, -+ client_write_buf_idx, client_write_buf, expected -+ ); -+ } -+ -+ if (memcmp(expected, client_write_buf, sizeof(expected) - 1)) { -+ lgtd_errx( -+ 1, "got %.*s instead of %s", -+ client_write_buf_idx, client_write_buf, expected -+ ); -+ } -+ -+ if (!device_list_free_called) { -+ lgtd_errx(1, "the list of devices hasn't been freed"); -+ } -+ if (!send_to_device_called) { -+ lgtd_errx(1, "nothing was send to any device"); -+ } -+ -+ return 0; -+} -diff --git a/tests/core/proto/test_proto_untag_tag_does_not_exist.c b/tests/core/proto/test_proto_untag_tag_does_not_exist.c -new file mode 100644 ---- /dev/null -+++ b/tests/core/proto/test_proto_untag_tag_does_not_exist.c -@@ -0,0 +1,89 @@ -+#include "proto.c" -+ -+#include "mock_client_buf.h" -+#include "mock_gateway.h" -+#include "tests_utils.h" -+ -+#define MOCKED_ROUTER_TARGETS_TO_DEVICES -+#define MOCKED_ROUTER_SEND_TO_DEVICE -+#define MOCKED_ROUTER_DEVICE_LIST_FREE -+#include "tests_proto_utils.h" -+ -+static bool device_list_free_called = false; -+ -+void -+lgtd_router_device_list_free(struct lgtd_router_device_list *devices) -+{ -+ (void)devices; -+ -+ device_list_free_called = true; -+} -+ -+static bool targets_to_devices_called = false; -+ -+struct lgtd_router_device_list * -+lgtd_router_targets_to_devices(const struct lgtd_proto_target_list *targets) -+{ -+ (void)targets; -+ -+ targets_to_devices_called = true; -+ -+ static struct lgtd_router_device_list devices = -+ SLIST_HEAD_INITIALIZER(&devices); -+ -+ return &devices; -+} -+ -+static bool send_to_device_called = false; -+ -+void -+lgtd_router_send_to_device(struct lgtd_lifx_bulb *bulb, -+ enum lgtd_lifx_packet_type pkt_type, -+ void *pkt) -+{ -+ (void)bulb; -+ (void)pkt_type; -+ (void)pkt; -+ send_to_device_called = true; -+} -+ -+int -+main(void) -+{ -+ struct lgtd_client client = { .io = FAKE_BUFFEREVENT }; -+ -+ struct lgtd_proto_target_list *targets; -+ targets = lgtd_tests_build_target_list("*", NULL); -+ -+ lgtd_proto_untag(&client, targets, "vapor"); -+ -+ const char expected[] = "true"; -+ -+ if (client_write_buf_idx != sizeof(expected) - 1) { -+ lgtd_errx( -+ 1, -+ "%d bytes written, expected %lu " -+ "(got %.*s instead of %s)", -+ client_write_buf_idx, sizeof(expected) - 1UL, -+ client_write_buf_idx, client_write_buf, expected -+ ); -+ } -+ if (memcmp(expected, client_write_buf, sizeof(expected) - 1)) { -+ lgtd_errx( -+ 1, "got %.*s instead of %s", -+ client_write_buf_idx, client_write_buf, expected -+ ); -+ } -+ -+ if (targets_to_devices_called) { -+ lgtd_errx(1, "unexpected call to targets_to_devices"); -+ } -+ if (device_list_free_called) { -+ lgtd_errx(1, "nothing should have been freed"); -+ } -+ if (send_to_device_called) { -+ lgtd_errx(1, "nothing should have been sent to any device"); -+ } -+ -+ return 0; -+} -diff --git a/tests/core/proto/tests_proto_utils.h b/tests/core/proto/tests_proto_utils.h ---- a/tests/core/proto/tests_proto_utils.h -+++ b/tests/core/proto/tests_proto_utils.h -@@ -1,5 +1,7 @@ - #pragma once - -+#define FAKE_BUFFEREVENT (void *)0xfeed -+ - void - lgtd_client_start_send_response(struct lgtd_client *client) - { -@@ -32,6 +34,18 @@ - } - #endif - -+#ifndef MOCKED_ROUTER_SEND_TO_DEVICE -+void -+lgtd_router_send_to_device(struct lgtd_lifx_bulb *bulb, -+ enum lgtd_lifx_packet_type pkt_type, -+ void *pkt) -+{ -+ (void)bulb; -+ (void)pkt_type; -+ (void)pkt; -+} -+#endif -+ - #ifndef MOCKED_ROUTER_SEND - bool - lgtd_router_send(const struct lgtd_proto_target_list *targets, -diff --git a/tests/core/router/CMakeLists.txt b/tests/core/router/CMakeLists.txt ---- a/tests/core/router/CMakeLists.txt -+++ b/tests/core/router/CMakeLists.txt -@@ -3,24 +3,20 @@ - ${CMAKE_CURRENT_BINARY_DIR} - ) - --ADD_LIBRARY( -+ADD_CORE_LIBRARY( - test_core_router STATIC - ${LIGHTSD_SOURCE_DIR}/core/log.c - ${LIGHTSD_SOURCE_DIR}/core/proto.c - ${LIGHTSD_SOURCE_DIR}/core/stats.c - ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c -+ ${LIGHTSD_SOURCE_DIR}/lifx/tagging.c - ${LIGHTSD_SOURCE_DIR}/lifx/timer.c - ${LIGHTSD_SOURCE_DIR}/lifx/wire_proto.c - ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c - ${CMAKE_CURRENT_SOURCE_DIR}/../tests_utils.c - ) - --TARGET_LINK_LIBRARIES( -- test_core_router ${EVENT2_CORE_LIBRARY} ${TIME_MONOTONIC_LIBRARY} --) --IF (HAVE_LIBBSD) -- TARGET_LINK_LIBRARIES(test_core_router ${LIBBSD_LIBRARY}) --ENDIF (HAVE_LIBBSD) -+TARGET_LINK_LIBRARIES(test_core_router ${EVENT2_CORE_LIBRARY}) - - FUNCTION(ADD_ROUTER_TEST TEST_SOURCE) - ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_core_router) -diff --git a/tests/core/router/test_router_send_to_broadcast.c b/tests/core/router/test_router_send_to_broadcast.c ---- a/tests/core/router/test_router_send_to_broadcast.c -+++ b/tests/core/router/test_router_send_to_broadcast.c -@@ -1,6 +1,7 @@ - #include "router.c" - - #include "tests_utils.h" -+ - #include "tests_router_utils.h" - - int -diff --git a/tests/core/router/tests_router_utils.h b/tests/core/router/tests_router_utils.h ---- a/tests/core/router/tests_router_utils.h -+++ b/tests/core/router/tests_router_utils.h -@@ -1,5 +1,7 @@ - #pragma once - -+#include "mock_gateway.h" -+ - int lgtd_tests_gw_pkt_queue_size = 0; - struct { - struct lgtd_lifx_gateway *gw; -diff --git a/tests/core/tests_shims.c b/tests/core/tests_shims.c ---- a/tests/core/tests_shims.c -+++ b/tests/core/tests_shims.c -@@ -32,52 +32,3 @@ - lgtd_cleanup(void) - { - } -- -- --void lgtd_lifx_gateway_handle_pan_gateway(struct lgtd_lifx_gateway *gw, -- const struct lgtd_lifx_packet_header *hdr, -- const struct lgtd_lifx_packet_pan_gateway *pkt) --{ -- (void)gw; -- (void)hdr; -- (void)pkt; --} -- --void lgtd_lifx_gateway_handle_light_status(struct lgtd_lifx_gateway *gw, -- const struct lgtd_lifx_packet_header *hdr, -- const struct lgtd_lifx_packet_light_status *pkt) --{ -- (void)gw; -- (void)hdr; -- (void)pkt; --} -- --void lgtd_lifx_gateway_handle_power_state(struct lgtd_lifx_gateway *gw, -- const struct lgtd_lifx_packet_header *hdr, -- const struct lgtd_lifx_packet_power_state *pkt) --{ -- (void)gw; -- (void)hdr; -- (void)pkt; --} -- --void lgtd_lifx_gateway_handle_tag_labels(struct lgtd_lifx_gateway *gw, -- const struct lgtd_lifx_packet_header *hdr, -- const struct lgtd_lifx_packet_tag_labels *pkt) --{ -- (void)gw; -- (void)hdr; -- (void)pkt; --} -- --struct lgtd_lifx_tag * --lgtd_lifx_tagging_find_tag(const char *tag_label) --{ -- struct lgtd_lifx_tag *tag = NULL; -- LIST_FOREACH(tag, &lgtd_lifx_tags, link) { -- if (!strcmp(tag->label, tag_label)) { -- break; -- } -- } -- return tag; --} -diff --git a/tests/core/tests_utils.c b/tests/core/tests_utils.c ---- a/tests/core/tests_utils.c -+++ b/tests/core/tests_utils.c -@@ -26,9 +26,6 @@ - struct lgtd_lifx_gateway_list lgtd_lifx_gateways = - LIST_HEAD_INITIALIZER(&lgtd_lifx_gateways); - --struct lgtd_lifx_tag_list lgtd_lifx_tags = -- LIST_HEAD_INITIALIZER(&lgtd_lifx_tags); -- - struct lgtd_lifx_gateway * - lgtd_tests_insert_mock_gateway(int id) - { -@@ -108,7 +105,10 @@ - struct lgtd_lifx_site *site = calloc(1, sizeof(*site)); - site->gw = gw; - site->tag_id = tag_id; -+ LIST_INSERT_HEAD(&tag->sites, site, link); -+ - gw->tags[tag_id] = tag; -- LIST_INSERT_HEAD(&tag->sites, site, link); -+ gw->tag_ids |= LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id); -+ - return site; - } -diff --git a/tests/lifx/bulb/CMakeLists.txt b/tests/lifx/bulb/CMakeLists.txt -new file mode 100644 ---- /dev/null -+++ b/tests/lifx/bulb/CMakeLists.txt -@@ -0,0 +1,29 @@ -+INCLUDE_DIRECTORIES( -+ ${CMAKE_CURRENT_SOURCE_DIR} -+ ${CMAKE_CURRENT_BINARY_DIR} -+) -+ -+ADD_CORE_LIBRARY( -+ test_lifx_bulb_core STATIC -+ ${LIGHTSD_SOURCE_DIR}/core/log.c -+ ${LIGHTSD_SOURCE_DIR}/core/router.c -+ ${LIGHTSD_SOURCE_DIR}/core/stats.c -+ ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c -+) -+ -+ADD_LIBRARY( -+ test_lifx_bulb STATIC -+ ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c -+ ${LIGHTSD_SOURCE_DIR}/lifx/wire_proto.c -+) -+ -+FUNCTION(ADD_BULB_TEST TEST_SOURCE) -+ ADD_TEST_FROM_C_SOURCES( -+ ${TEST_SOURCE} test_lifx_bulb_core test_lifx_bulb -+ ) -+ENDFUNCTION() -+ -+FILE(GLOB TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test_*.c") -+FOREACH(TEST ${TESTS}) -+ ADD_BULB_TEST(${TEST}) -+ENDFOREACH() -diff --git a/tests/lifx/bulb/test_bulb_close.c b/tests/lifx/bulb/test_bulb_close.c -new file mode 100644 ---- /dev/null -+++ b/tests/lifx/bulb/test_bulb_close.c -@@ -0,0 +1,33 @@ -+#include "bulb.c" -+ -+#include "mock_gateway.h" -+ -+int -+main(void) -+{ -+ struct lgtd_lifx_gateway gw; -+ uint8_t bulb_addr[LGTD_LIFX_ADDR_LENGTH] = { 5, 4, 3, 2, 1, 0 }; -+ struct lgtd_lifx_bulb *bulb = lgtd_lifx_bulb_open(&gw, bulb_addr); -+ -+ bulb->state.power = LGTD_LIFX_POWER_ON; -+ LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(bulbs_powered_on, 1); -+ -+ lgtd_lifx_bulb_close(bulb); -+ -+ if (!RB_EMPTY(&lgtd_lifx_bulbs_table)) { -+ errx(1, "The bulbs table should be empty!"); -+ } -+ -+ if (LGTD_STATS_GET(bulbs) != 0) { -+ errx(1, "The bulbs counter is %d (expected 0)", LGTD_STATS_GET(bulbs)); -+ } -+ -+ if (LGTD_STATS_GET(bulbs_powered_on) != 0) { -+ errx( -+ 1, "The powered on bulbs counter is %d (expected 0)", -+ LGTD_STATS_GET(bulbs_powered_on) -+ ); -+ } -+ -+ return 0; -+} -diff --git a/tests/lifx/bulb/test_bulb_open.c b/tests/lifx/bulb/test_bulb_open.c -new file mode 100644 ---- /dev/null -+++ b/tests/lifx/bulb/test_bulb_open.c -@@ -0,0 +1,44 @@ -+#include "bulb.c" -+ -+#include "mock_gateway.h" -+ -+int -+main(void) -+{ -+ struct lgtd_lifx_gateway gw; -+ uint8_t bulb_addr[LGTD_LIFX_ADDR_LENGTH] = { 5, 4, 3, 2, 1, 0 }; -+ lgtd_time_mono_t now = lgtd_time_monotonic_msecs(); -+ struct lgtd_lifx_bulb *bulb = lgtd_lifx_bulb_open(&gw, bulb_addr); -+ -+ if (!bulb) { -+ errx(1, "lgtd_lifx_bulb_open didn't return any bulb"); -+ } -+ -+ if (memcmp(bulb->addr, bulb_addr, LGTD_LIFX_ADDR_LENGTH)) { -+ errx( -+ 1, "got bulb addr %s (expected %s)", -+ lgtd_addrtoa(bulb->addr), lgtd_addrtoa(bulb_addr) -+ ); -+ } -+ -+ if (bulb->gw != &gw) { -+ errx(1, "got bulb gateway %p (expected %p)", bulb->gw, &gw); -+ } -+ -+ if (lgtd_lifx_bulb_get(bulb_addr) != bulb) { -+ errx(1, "the new bulb can't be found"); -+ } -+ -+ if (bulb->last_light_state_at < now) { -+ errx( -+ 1, "got bulb->last_light_state_at %ju (expected >= %ju)", -+ bulb->last_light_state_at, (uintmax_t)now -+ ); -+ } -+ -+ if (LGTD_STATS_GET(bulbs) != 1) { -+ errx(1, "bulbs counter is %d (expected 1)", LGTD_STATS_GET(bulbs)); -+ } -+ -+ return 0; -+} -diff --git a/tests/lifx/bulb/test_bulb_set_light_state.c b/tests/lifx/bulb/test_bulb_set_light_state.c -new file mode 100644 ---- /dev/null -+++ b/tests/lifx/bulb/test_bulb_set_light_state.c -@@ -0,0 +1,92 @@ -+#include "bulb.c" -+ -+#define MOCKED_LGTD_LIFX_GATEWAY_UPDATE_TAG_REFCOUNTS -+#include "mock_gateway.h" -+ -+static int update_tag_refcouts_call_counts = 0; -+ -+void -+lgtd_lifx_gateway_update_tag_refcounts(struct lgtd_lifx_gateway *gw, -+ uint64_t bulb_tags, -+ uint64_t pkt_tags) -+{ -+ if (gw != (void *)0xdeaf) { -+ errx(1, "got wrong gw %p (expected 0xdeaf)", gw); -+ } -+ -+ if (pkt_tags != 0xfeed) { -+ errx(1, "got pkt_tags %#jx (expected 0xfeed)", (uintmax_t)pkt_tags); -+ } -+ -+ if (!update_tag_refcouts_call_counts) { -+ if (bulb_tags != 0x2a) { -+ errx(1, "got bulb_tags %#jx (expected 0x2a)", (uintmax_t)bulb_tags); -+ } -+ } else { -+ if (bulb_tags != 0xfeed) { -+ errx(1, "got bulb_tags %#jx (expected 0xfeed)", (uintmax_t)bulb_tags); -+ } -+ } -+ -+ update_tag_refcouts_call_counts++; -+} -+ -+int -+main(void) -+{ -+ struct lgtd_lifx_bulb bulb = { -+ .state = { -+ .hue = 54321, -+ .brightness = UINT16_MAX, -+ .kelvin = 12345, -+ .dim = 808, -+ .power = LGTD_LIFX_POWER_OFF, -+ .label = "lair", -+ .tags = 0x2a -+ }, -+ .gw = (void *)0xdeaf -+ }; -+ -+ struct lgtd_lifx_light_state new_state = { -+ .hue = 22222, -+ .brightness = UINT16_MAX / 2, -+ .kelvin = 54321, -+ .dim = 303, -+ .power = LGTD_LIFX_POWER_ON, -+ .label = "caverne", -+ .tags = 0xfeed -+ }; -+ -+ lgtd_lifx_bulb_set_light_state(&bulb, &new_state, 2015); -+ if (memcmp(&bulb.state, &new_state, sizeof(new_state))) { -+ errx(1, "new light state incorrectly set"); -+ } -+ if (LGTD_STATS_GET(bulbs_powered_on) != 1) { -+ errx( -+ 1, "unexpected bulbs_powered_on counter value %d (expected 1)", -+ LGTD_STATS_GET(bulbs_powered_on) -+ ); -+ } -+ if (bulb.last_light_state_at != 2015) { -+ errx( -+ 1, "got bulb.last_light_state = %jx (expected 2015)", -+ (uintmax_t)bulb.last_light_state_at -+ ); -+ } -+ if (update_tag_refcouts_call_counts != 1) { -+ errx(1, "lgtd_lifx_gateway_update_tag_refcounts wasn't called"); -+ } -+ -+ lgtd_lifx_bulb_set_light_state(&bulb, &new_state, 2015); -+ if (update_tag_refcouts_call_counts != 2) { -+ errx(1, "lgtd_lifx_gateway_update_tag_refcounts wasn't called"); -+ } -+ if (LGTD_STATS_GET(bulbs_powered_on) != 1) { -+ errx( -+ 1, "unexpected bulbs_powered_on counter value %d (expected 1)", -+ LGTD_STATS_GET(bulbs_powered_on) -+ ); -+ } -+ -+ return 0; -+} -diff --git a/tests/lifx/bulb/test_bulb_set_power_state.c b/tests/lifx/bulb/test_bulb_set_power_state.c -new file mode 100644 ---- /dev/null -+++ b/tests/lifx/bulb/test_bulb_set_power_state.c -@@ -0,0 +1,39 @@ -+#include "bulb.c" -+ -+#include "mock_gateway.h" -+ -+int -+main(void) -+{ -+ struct lgtd_lifx_bulb bulb = { -+ .state = { -+ .hue = 54321, -+ .brightness = UINT16_MAX, -+ .kelvin = 12345, -+ .dim = 808, -+ .power = LGTD_LIFX_POWER_OFF, -+ .label = "lair", -+ .tags = 0x2a -+ }, -+ .gw = (void *)0xdeaf -+ }; -+ struct lgtd_lifx_light_state new_state; -+ memcpy(&new_state, &bulb.state, sizeof(new_state)); -+ new_state.power = LGTD_LIFX_POWER_ON; -+ -+ -+ for (int i = 0; i != 2; i++) { -+ lgtd_lifx_bulb_set_power_state(&bulb, LGTD_LIFX_POWER_ON); -+ if (memcmp(&bulb.state, &new_state, sizeof(new_state))) { -+ errx(1, "new light state incorrectly set"); -+ } -+ if (LGTD_STATS_GET(bulbs_powered_on) != 1) { -+ errx( -+ 1, "unexpected bulbs_powered_on counter value %d (expected 1)", -+ LGTD_STATS_GET(bulbs_powered_on) -+ ); -+ } -+ } -+ -+ return 0; -+} -diff --git a/tests/lifx/bulb/test_bulb_set_tags.c b/tests/lifx/bulb/test_bulb_set_tags.c -new file mode 100644 ---- /dev/null -+++ b/tests/lifx/bulb/test_bulb_set_tags.c -@@ -0,0 +1,50 @@ -+#include "bulb.c" -+ -+#define MOCKED_LGTD_LIFX_GATEWAY_UPDATE_TAG_REFCOUNTS -+#include "mock_gateway.h" -+ -+static bool update_tag_refcouts_called = false; -+ -+void -+lgtd_lifx_gateway_update_tag_refcounts(struct lgtd_lifx_gateway *gw, -+ uint64_t bulb_tags, -+ uint64_t pkt_tags) -+{ -+ if (gw != (void *)0xdeaf) { -+ errx(1, "got wrong gw %p (expected 0xdeaf)", gw); -+ } -+ -+ if (bulb_tags != 0x2a) { -+ errx(1, "got bulb_tags %#jx (expected 0x2a)", (uintmax_t)bulb_tags); -+ } -+ -+ if (pkt_tags != 0xfeed) { -+ errx(1, "got pkt_tags %#jx (expected 0xfeed)", (uintmax_t)pkt_tags); -+ } -+ -+ update_tag_refcouts_called = true; -+} -+ -+int -+main(void) -+{ -+ struct lgtd_lifx_bulb bulb = { -+ .state = { .tags = 0x2a }, -+ .gw = (void *)0xdeaf -+ }; -+ -+ lgtd_lifx_bulb_set_tags(&bulb, 0xfeed); -+ -+ if (bulb.state.tags != 0xfeed) { -+ errx( -+ 1, "got bulb.state.tags = %#jx (expected 0xfeed)", -+ (uintmax_t)bulb.state.tags -+ ); -+ } -+ -+ if (!update_tag_refcouts_called) { -+ errx(1, "lgtd_lifx_gateway_update_tag_refcounts wasn't called"); -+ } -+ -+ return 0; -+} -diff --git a/tests/lifx/gateway/CMakeLists.txt b/tests/lifx/gateway/CMakeLists.txt ---- a/tests/lifx/gateway/CMakeLists.txt -+++ b/tests/lifx/gateway/CMakeLists.txt -@@ -3,25 +3,27 @@ - ${CMAKE_CURRENT_BINARY_DIR} - ) - --ADD_LIBRARY( -- test_lifx_gateway STATIC -+ADD_CORE_LIBRARY( -+ test_lifx_gateway_core STATIC - ${LIGHTSD_SOURCE_DIR}/core/log.c - ${LIGHTSD_SOURCE_DIR}/core/proto.c - ${LIGHTSD_SOURCE_DIR}/core/router.c - ${LIGHTSD_SOURCE_DIR}/core/stats.c -+ ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c -+) -+ -+ADD_LIBRARY( -+ test_lifx_gateway STATIC - ${LIGHTSD_SOURCE_DIR}/lifx/broadcast.c - ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c - ${LIGHTSD_SOURCE_DIR}/lifx/timer.c - ${LIGHTSD_SOURCE_DIR}/lifx/wire_proto.c -- ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c - ) --TARGET_LINK_LIBRARIES(test_lifx_gateway ${TIME_MONOTONIC_LIBRARY}) --IF (HAVE_LIBBSD) -- TARGET_LINK_LIBRARIES(test_lifx_gateway ${LIBBSD_LIBRARY}) --ENDIF (HAVE_LIBBSD) - - FUNCTION(ADD_GATEWAY_TEST TEST_SOURCE) -- ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_lifx_gateway) -+ ADD_TEST_FROM_C_SOURCES( -+ ${TEST_SOURCE} test_lifx_gateway_core test_lifx_gateway -+ ) - ENDFUNCTION() - - FILE(GLOB TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test_*.c") -diff --git a/tests/lifx/gateway/test_gateway_allocate_tag_id.c b/tests/lifx/gateway/test_gateway_allocate_tag_id.c -new file mode 100644 ---- /dev/null -+++ b/tests/lifx/gateway/test_gateway_allocate_tag_id.c -@@ -0,0 +1,102 @@ -+#include "gateway.c" -+ -+#include <string.h> -+ -+#define MOCKED_LIFX_TAGGING_INCREF -+#include "test_gateway_utils.h" -+ -+static bool tagging_incref_called = false; -+ -+struct lgtd_lifx_tag * -+lgtd_lifx_tagging_incref(const char *label, -+ struct lgtd_lifx_gateway *gw, -+ int tag_id) -+{ -+ if (!label) { -+ errx(1, "missing tag label"); -+ } -+ if (!gw) { -+ errx(1, "missing gateway"); -+ } -+ if (tag_id > 2) { -+ errx(1, "got tag_id %d but expected < 3", tag_id); -+ } -+ -+ struct lgtd_lifx_tag *tag = lgtd_lifx_tagging_find_tag(label); -+ if (!tag) { -+ tag = calloc(1, sizeof(*tag)); -+ strcpy(tag->label, label); -+ struct lgtd_lifx_site *site = calloc(1, sizeof(*site)); -+ site->gw = gw; -+ site->tag_id = tag_id; -+ LIST_INSERT_HEAD(&tag->sites, site, link); -+ } -+ -+ tagging_incref_called = true; -+ -+ return tag; -+} -+ -+int -+main(void) -+{ -+ lgtd_lifx_wire_load_packet_infos_map(); -+ -+ struct lgtd_lifx_gateway gw; -+ memset(&gw, 0, sizeof(gw)); -+ -+ struct lgtd_lifx_packet_header hdr; -+ memset(&hdr, 0, sizeof(hdr)); -+ -+ uint64_t expected_tag_ids = LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(0); -+ -+ lgtd_lifx_gateway_allocate_tag_id(&gw, 0, "test"); -+ if (!gw.tags[0]) { -+ errx(1, "gw.tag_ids[0] shouldn't be NULL"); -+ } -+ if (strcmp(gw.tags[0]->label, "test")) { -+ errx( -+ 1, "unexpected tag %.*s (expected test)", -+ (int)sizeof(gw.tags[0]->label), gw.tags[0]->label -+ ); -+ } -+ if (gw.tag_ids != LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(0)) { -+ errx( -+ 1, "tag_ids = %jx (expected %jx)", -+ (uintmax_t)gw.tag_ids, (uintmax_t)expected_tag_ids -+ ); -+ } -+ if (!tagging_incref_called) { -+ errx(1, "lgtd_lifx_tagging_incref should have been called"); -+ } -+ tagging_incref_called = false; -+ -+ for (int i = 1; i != 3; i++) { -+ int tag_id = lgtd_lifx_gateway_allocate_tag_id(&gw, -1, "lounge"); -+ if (tag_id < 1) { -+ errx(1, "no tag_id was allocated (received tag_id %d)", tag_id); -+ } -+ if (!gw.tags[tag_id]) { -+ errx(1, "gw.tag_ids[%d] shouldn't be NULL", i); -+ } -+ if (strcmp(gw.tags[tag_id]->label, "lounge")) { -+ errx( -+ 1, "unexpected tag %.*s (expected lounge)", -+ (int)sizeof(gw.tags[tag_id]->label), gw.tags[tag_id]->label -+ ); -+ } -+ expected_tag_ids |= LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id); -+ if (gw.tag_ids != expected_tag_ids) { -+ errx( -+ 1, "tag_ids = %jx (expected %jx)", -+ (uintmax_t)gw.tag_ids, (uintmax_t)expected_tag_ids -+ ); -+ } -+ if (!tagging_incref_called) { -+ errx(1, "lgtd_lifx_tagging_incref should have been called"); -+ } -+ tagging_incref_called = false; -+ } -+ -+ return 0; -+} -diff --git a/tests/lifx/gateway/test_gateway_allocate_tag_id_no_tag_id_left.c b/tests/lifx/gateway/test_gateway_allocate_tag_id_no_tag_id_left.c -new file mode 100644 ---- /dev/null -+++ b/tests/lifx/gateway/test_gateway_allocate_tag_id_no_tag_id_left.c -@@ -0,0 +1,89 @@ -+ -+#include <string.h> -+ -+#include "gateway.c" -+ -+#define MOCKED_LIFX_TAGGING_INCREF -+#include "test_gateway_utils.h" -+ -+static bool tagging_incref_called = false; -+ -+struct lgtd_lifx_tag * -+lgtd_lifx_tagging_incref(const char *label, -+ struct lgtd_lifx_gateway *gw, -+ int tag_id) -+{ -+ if (!label) { -+ errx(1, "missing tag label"); -+ } -+ if (!gw) { -+ errx(1, "missing gateway"); -+ } -+ if (tag_id < 0) { -+ errx(1, "got tag_id %d but expected >= 0", tag_id); -+ } -+ -+ struct lgtd_lifx_tag *tag = lgtd_lifx_tagging_find_tag(label); -+ if (!tag) { -+ tag = calloc(1, sizeof(*tag)); -+ strcpy(tag->label, label); -+ struct lgtd_lifx_site *site = calloc(1, sizeof(*site)); -+ site->gw = gw; -+ site->tag_id = tag_id; -+ LIST_INSERT_HEAD(&tag->sites, site, link); -+ } -+ -+ tagging_incref_called = true; -+ -+ return tag; -+} -+ -+int -+main(void) -+{ -+ lgtd_lifx_wire_load_packet_infos_map(); -+ -+ struct lgtd_lifx_gateway gw; -+ memset(&gw, 0, sizeof(gw)); -+ -+ struct lgtd_lifx_packet_header hdr; -+ memset(&hdr, 0, sizeof(hdr)); -+ -+ uint64_t expected_tag_ids = 0; -+ for (int i = 0; i != LGTD_LIFX_GATEWAY_MAX_TAGS; i++) { -+ int tag_id = lgtd_lifx_gateway_allocate_tag_id(&gw, -1, "lounge"); -+ if (tag_id < 0) { -+ errx(1, "no tag_id was allocated (received tag_id %d)", tag_id); -+ } -+ if (!gw.tags[tag_id]) { -+ errx(1, "gw.tag_ids[%d] shouldn't be NULL", i); -+ } -+ if (strcmp(gw.tags[tag_id]->label, "lounge")) { -+ errx( -+ 1, "unexpected tag %.*s (expected lounge)", -+ (int)sizeof(gw.tags[tag_id]->label), gw.tags[tag_id]->label -+ ); -+ } -+ expected_tag_ids |= LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id); -+ if (gw.tag_ids != expected_tag_ids) { -+ errx( -+ 1, "tag_ids = %jx (expected %jx)", -+ (uintmax_t)gw.tag_ids, (uintmax_t)expected_tag_ids -+ ); -+ } -+ if (!tagging_incref_called) { -+ errx(1, "lgtd_lifx_tagging_incref should have been called"); -+ } -+ tagging_incref_called = false; -+ } -+ -+ int tag_id = lgtd_lifx_gateway_allocate_tag_id(&gw, -1, "lounge"); -+ if (tag_id != -1) { -+ errx(1, "tag_ids full but tag_id %d was allocated", tag_id); -+ } -+ if (tagging_incref_called) { -+ errx(1, "lgtd_lifx_tagging_incref should not have been called"); -+ } -+ -+ return 0; -+} -diff --git a/tests/lifx/gateway/test_gateway_update_tag_refcounts.c b/tests/lifx/gateway/test_gateway_update_tag_refcounts.c -new file mode 100644 ---- /dev/null -+++ b/tests/lifx/gateway/test_gateway_update_tag_refcounts.c -@@ -0,0 +1,106 @@ -+#include "gateway.c" -+ -+#include "test_gateway_utils.h" -+ -+int -+main(void) -+{ -+ lgtd_lifx_wire_load_packet_infos_map(); -+ -+ struct lgtd_lifx_gateway gw; -+ memset(&gw, 0, sizeof(gw)); -+ -+ lgtd_lifx_gateway_update_tag_refcounts(&gw, 0, 0); -+ for (int i = 0; i != LGTD_LIFX_GATEWAY_MAX_TAGS; i++) { -+ if (gw.tag_refcounts[i]) { -+ errx( -+ 1, "gw.tag_refcounts[%d] was %d, (expected 0)", -+ i, gw.tag_refcounts[i] -+ ); -+ } -+ } -+ -+ for (int n = 1; n != 3; n++) { -+ lgtd_lifx_gateway_update_tag_refcounts(&gw, 0, 1); -+ if (gw.tag_refcounts[0] != n) { -+ errx( -+ 1, "gw.tag_refcounts[0] was %d (expected %d)", -+ gw.tag_refcounts[0], n -+ ); -+ } -+ for (int i = 1; i != LGTD_LIFX_GATEWAY_MAX_TAGS; i++) { -+ if (gw.tag_refcounts[i]) { -+ errx( -+ 1, "gw.tag_refcounts[%d] was %d (expected 0)", -+ i, gw.tag_refcounts[i] -+ ); -+ } -+ } -+ } -+ -+ lgtd_lifx_gateway_update_tag_refcounts(&gw, 0, 2); -+ gw.tag_ids = 0x2; -+ -+ for (int n = 1; n >= 0; n--) { -+ lgtd_lifx_gateway_update_tag_refcounts(&gw, 1, 0); -+ if (gw.tag_refcounts[0] != n) { -+ errx( -+ 1, "gw.tag_refcounts[0] was %d (expected %d)", -+ gw.tag_refcounts[0], n - 1 -+ ); -+ } -+ if (gw.tag_refcounts[1] != 1) { -+ errx( -+ 1, "gw.tag_refcounts[1] was %d (expected 1)", -+ gw.tag_refcounts[1] -+ ); -+ } -+ for (int i = 2; i != LGTD_LIFX_GATEWAY_MAX_TAGS; i++) { -+ if (gw.tag_refcounts[i]) { -+ errx( -+ 1, "gw.tag_refcounts[%d] was %d (expected 0)", -+ i, gw.tag_refcounts[i] -+ ); -+ } -+ } -+ } -+ if (gw.pkt_ring[0].type != LGTD_LIFX_SET_TAG_LABELS) { -+ errx(1, "SET_TAG_LABELS should have been enqueued on the gateway"); -+ } -+ -+ struct lgtd_lifx_packet_tag_labels *pkt = -+ (void *)&gw_write_buf[sizeof(struct lgtd_lifx_packet_header)]; -+ uint64_t tags = le64toh(pkt->tags); -+ if (tags != ~2ULL) { -+ errx( -+ 1, "tags on LGTD_LIFX_SET_TAG_LABELS was %#jx (expected %#jx)", -+ (uintmax_t)tags, (uintmax_t)~2ULL -+ ); -+ } -+ const char blank_label[LGTD_LIFX_LABEL_SIZE] = { 0 }; -+ if (memcmp(pkt->label, blank_label, LGTD_LIFX_LABEL_SIZE)) { -+ errx( -+ 1, "label on LGTD_LIFX_SET_TAG_LABELS should be " -+ "all zero but got %.*s", LGTD_LIFX_LABEL_SIZE, pkt->label -+ ); -+ } -+ -+ for (int n = 0; n != UINT8_MAX; n++) { -+ lgtd_lifx_gateway_update_tag_refcounts(&gw, 0, 4); -+ } -+ if (gw.tag_refcounts[2] != UINT8_MAX) { -+ errx( -+ 1, "gw.tag_refcounts[2] was %d (expected %d)", -+ gw.tag_refcounts[2], UINT8_MAX -+ ); -+ } -+ lgtd_lifx_gateway_update_tag_refcounts(&gw, 0, 4); -+ if (gw.tag_refcounts[2] != UINT8_MAX) { -+ errx( -+ 1, "gw.tag_refcounts[2] was %d (expected %d)", -+ gw.tag_refcounts[2], UINT8_MAX -+ ); -+ } -+ -+ return 0; -+} -diff --git a/tests/lifx/mock_gateway.h b/tests/lifx/mock_gateway.h -new file mode 100644 ---- /dev/null -+++ b/tests/lifx/mock_gateway.h -@@ -0,0 +1,131 @@ -+#pragma once -+ -+#include "core/time_monotonic.h" -+#include "lifx/bulb.h" -+#include "lifx/gateway.h" -+ -+struct lgtd_lifx_tag; -+struct lgtd_lifx_gateway; -+ -+#ifndef MOCKED_LIFX_GATEWAY_SEND_TO_SITE -+bool -+lgtd_lifx_gateway_send_to_site(struct lgtd_lifx_gateway *gw, -+ enum lgtd_lifx_packet_type pkt_type, -+ const void *pkt) -+{ -+ (void)gw; -+ (void)pkt_type; -+ (void)pkt; -+ return false; -+} -+#endif -+ -+#ifndef MOCKED_LIFX_GATEWAY_ALLOCATE_TAG_ID -+int -+lgtd_lifx_gateway_allocate_tag_id(struct lgtd_lifx_gateway *gw, -+ int tag_id, -+ const char *tag_label) -+{ -+ (void)gw; -+ (void)tag_id; -+ (void)tag_label; -+ return -1; -+} -+#endif -+ -+#ifndef MOCKED_LGTD_LIFX_GATEWAY_HANDLE_PAN_GATEWAY -+void -+lgtd_lifx_gateway_handle_pan_gateway(struct lgtd_lifx_gateway *gw, -+ const struct lgtd_lifx_packet_header *hdr, -+ const struct lgtd_lifx_packet_pan_gateway *pkt) -+{ -+ (void)gw; -+ (void)hdr; -+ (void)pkt; -+} -+#endif -+ -+#ifndef MOCKED_LGTD_LIFX_GATEWAY_HANDLE_LIGHT_STATUS -+void -+lgtd_lifx_gateway_handle_light_status(struct lgtd_lifx_gateway *gw, -+ const struct lgtd_lifx_packet_header *hdr, -+ const struct lgtd_lifx_packet_light_status *pkt) -+{ -+ (void)gw; -+ (void)hdr; -+ (void)pkt; -+} -+#endif -+ -+#ifndef MOCKED_LGTD_LIFX_GATEWAY_HANDLE_POWER_STATE -+void -+lgtd_lifx_gateway_handle_power_state(struct lgtd_lifx_gateway *gw, -+ const struct lgtd_lifx_packet_header *hdr, -+ const struct lgtd_lifx_packet_power_state *pkt) -+{ -+ (void)gw; -+ (void)hdr; -+ (void)pkt; -+} -+#endif -+ -+#ifndef MOCKED_LGTD_LIFX_GATEWAY_HANDLE_TAG_LABELS -+void -+lgtd_lifx_gateway_handle_tag_labels(struct lgtd_lifx_gateway *gw, -+ const struct lgtd_lifx_packet_header *hdr, -+ const struct lgtd_lifx_packet_tag_labels *pkt) -+{ -+ (void)gw; -+ (void)hdr; -+ (void)pkt; -+} -+#endif -+ -+#ifndef MOCKED_LGTD_LIFX_GATEWAY_HANDLE_TAGS -+void -+lgtd_lifx_gateway_handle_tags(struct lgtd_lifx_gateway *gw, -+ const struct lgtd_lifx_packet_header *hdr, -+ const struct lgtd_lifx_packet_tags *pkt) -+{ -+ (void)gw; -+ (void)hdr; -+ (void)pkt; -+} -+#endif -+ -+#ifndef MOCKED_LGTD_LIFX_GATEWAY_DEALLOCATE_TAG_ID -+void -+lgtd_lifx_gateway_deallocate_tag_id(struct lgtd_lifx_gateway *gw, int tag_id) -+{ -+ (void)gw; -+ (void)tag_id; -+} -+#endif -+ -+#ifndef MOCKED_LGTD_LIFX_GATEWAY_GET_TAG_ID -+int -+lgtd_lifx_gateway_get_tag_id(const struct lgtd_lifx_gateway *gw, -+ const struct lgtd_lifx_tag *tag) -+{ -+ int tag_id; -+ LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, gw->tag_ids) { -+ if (gw->tags[tag_id] == tag) { -+ return tag_id; -+ } -+ } -+ -+ return -1; -+} -+#endif -+ -+#ifndef MOCKED_LGTD_LIFX_GATEWAY_UPDATE_TAG_REFCOUNTS -+void -+lgtd_lifx_gateway_update_tag_refcounts(struct lgtd_lifx_gateway *gw, -+ uint64_t bulb_tags, -+ uint64_t pkt_tags) -+{ -+ (void)gw; -+ (void)bulb_tags; -+ (void)pkt_tags; -+} -+#endif -diff --git a/tests/lifx/wire_proto/CMakeLists.txt b/tests/lifx/wire_proto/CMakeLists.txt ---- a/tests/lifx/wire_proto/CMakeLists.txt -+++ b/tests/lifx/wire_proto/CMakeLists.txt -@@ -3,18 +3,15 @@ - ${CMAKE_CURRENT_BINARY_DIR} - ) - --ADD_LIBRARY( -- test_lifx_wire_proto STATIC -+ADD_CORE_LIBRARY( -+ test_lifx_wire_proto_core STATIC - ${LIGHTSD_SOURCE_DIR}/core/log.c - ${LIGHTSD_SOURCE_DIR}/core/stats.c - ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c - ) --IF (HAVE_LIBBSD) -- TARGET_LINK_LIBRARIES(test_lifx_wire_proto ${LIBBSD_LIBRARY}) --ENDIF (HAVE_LIBBSD) - - FUNCTION(ADD_WIRE_PROTO_TEST TEST_SOURCE) -- ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_lifx_wire_proto) -+ ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_lifx_wire_proto_core) - ENDFUNCTION() - - FILE(GLOB TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test_*.c") -diff --git a/tests/lifx/wire_proto/test_wire_proto_utils.h b/tests/lifx/wire_proto/test_wire_proto_utils.h ---- a/tests/lifx/wire_proto/test_wire_proto_utils.h -+++ b/tests/lifx/wire_proto/test_wire_proto_utils.h -@@ -35,3 +35,12 @@ - (void)hdr; - (void)pkt; - } -+ -+void lgtd_lifx_gateway_handle_tags(struct lgtd_lifx_gateway *gw, -+ const struct lgtd_lifx_packet_header *hdr, -+ const struct lgtd_lifx_packet_tags *pkt) -+{ -+ (void)gw; -+ (void)hdr; -+ (void)pkt; -+} -diff --git a/tests/lightsc b/tests/lightsc ---- a/tests/lightsc -+++ b/tests/lightsc -@@ -72,6 +72,14 @@ - def get_light_state(socket, target): - return jsonrpc_call(socket, "get_light_state", [target]) - -+ -+def tag(socket, target, tag): -+ return jsonrpc_call(socket, "tag", [target, tag]) -+ -+ -+def untag(socket, target, tag): -+ return jsonrpc_call(socket, "untag", [target, tag]) -+ - if __name__ == "__main__": - s = socket.create_connection(("localhost", 1234)) - h = 0
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/update_readme.patch Mon Aug 03 01:18:55 2015 -0700 @@ -0,0 +1,29 @@ +# HG changeset patch +# Parent 04a46ad71a58fa6486754a8e97da9f06436d5a01 + +diff --git a/README.rst b/README.rst +--- a/README.rst ++++ b/README.rst +@@ -103,4 +103,22 @@ + + Use the ``-f`` option to run lightsd in the foreground. + ++Known issues ++------------ ++ ++The grouping (tagging) code of the LIFX White 800 is bugged: after a tagging ++operation the LIFX White 800 keep saying it has no tags. Reboot the bulb to make ++the tags appears. ++ ++Power ON/OFF are the only commands with auto-retry, i.e: lightsd will keep ++sending the command to the bulb until its state changes. This is not implemented ++(yet) for ``set_light_from_hsbk`` and ``set_waveform``. ++ ++While lighsd appears to be pretty stable, if you want to run lightsd in the ++background, I recommend doing it in a processor supervisor (e.g: Supervisor_) ++that can restart lightsd in case of crash. Otherwise, please send me your crash ++reports! ++ ++.. _Supervisor: http://www.supervisord.org/ ++ + .. vim: set tw=80 spelllang=en spell: