diff --git a/applications/async_blink/targets/Makefile b/applications/async_blink/targets/Makefile index 228c445ca..dd450c0a7 100644 --- a/applications/async_blink/targets/Makefile +++ b/applications/async_blink/targets/Makefile @@ -1,7 +1,6 @@ SUBDIRS = linux.x86 \ linux.armv7a \ linux.llvm \ - mach.x86 \ mach.x86_64 \ freertos.armv7m.ek-lm4f120xl \ freertos.armv7m.ek-tm4c123gxl \ diff --git a/applications/async_blink/targets/mach.x86/.gitignore b/applications/async_blink/targets/mach.x86/.gitignore deleted file mode 100644 index 43148b79f..000000000 --- a/applications/async_blink/targets/mach.x86/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -async_blink -*_test diff --git a/applications/async_blink/targets/mach.x86/NodeId.cxx b/applications/async_blink/targets/mach.x86/NodeId.cxx deleted file mode 100644 index 3cd6d1db7..000000000 --- a/applications/async_blink/targets/mach.x86/NodeId.cxx +++ /dev/null @@ -1,4 +0,0 @@ -#include "openlcb/If.hxx" - -extern const openlcb::NodeID NODE_ID; -const openlcb::NodeID NODE_ID = 0x050101011410ULL; diff --git a/applications/async_blink/targets/mach.x86_64/Makefile b/applications/async_blink/targets/mach.x86_64/Makefile index 3bb6034b2..c6386dac3 100644 --- a/applications/async_blink/targets/mach.x86_64/Makefile +++ b/applications/async_blink/targets/mach.x86_64/Makefile @@ -1 +1,2 @@ +-include ../../config.mk include $(OPENMRNPATH)/etc/prog.mk diff --git a/applications/clinic_app/targets/Makefile b/applications/clinic_app/targets/Makefile index f999fd393..8b3bed0a4 100644 --- a/applications/clinic_app/targets/Makefile +++ b/applications/clinic_app/targets/Makefile @@ -1,11 +1,11 @@ SUBDIRS = \ - freertos.armv7m.ek-tm4c123gxl \ - freertos.armv7m.ek-tm4c1294xl \ - freertos.armv6m.st-stm32f072b-discovery \ - freertos.armv7m.st-stm32f303-discovery \ + freertos.armv7m.ek-tm4c123gxl \ + freertos.armv7m.ek-tm4c1294xl \ + freertos.armv6m.st-stm32f072b-discovery \ + freertos.armv7m.st-stm32f303-discovery \ linux.armv7a \ linux.x86 \ - mach.x86 + mach.x86_64 # freertos.armv7m.lpc1768-mbed \ diff --git a/applications/clinic_app/targets/mach.x86/.gitignore b/applications/clinic_app/targets/mach.x86_64/.gitignore similarity index 100% rename from applications/clinic_app/targets/mach.x86/.gitignore rename to applications/clinic_app/targets/mach.x86_64/.gitignore diff --git a/applications/clinic_app/targets/mach.x86/Makefile b/applications/clinic_app/targets/mach.x86_64/Makefile similarity index 100% rename from applications/clinic_app/targets/mach.x86/Makefile rename to applications/clinic_app/targets/mach.x86_64/Makefile diff --git a/applications/clinic_app/targets/mach.x86/config.hxx b/applications/clinic_app/targets/mach.x86_64/config.hxx similarity index 100% rename from applications/clinic_app/targets/mach.x86/config.hxx rename to applications/clinic_app/targets/mach.x86_64/config.hxx diff --git a/applications/clinic_app/targets/mach.x86/main.cxx b/applications/clinic_app/targets/mach.x86_64/main.cxx similarity index 100% rename from applications/clinic_app/targets/mach.x86/main.cxx rename to applications/clinic_app/targets/mach.x86_64/main.cxx diff --git a/applications/hub/main.cxx b/applications/hub/main.cxx index 599710baf..67315b3ea 100644 --- a/applications/hub/main.cxx +++ b/applications/hub/main.cxx @@ -39,13 +39,15 @@ #include -#include "os/os.h" -#include "utils/constants.hxx" -#include "utils/Hub.hxx" -#include "utils/GcTcpHub.hxx" -#include "utils/ClientConnection.hxx" #include "executor/Executor.hxx" #include "executor/Service.hxx" +#include "os/os.h" +#include "utils/ClientConnection.hxx" +#include "utils/GcTcpHub.hxx" +#include "utils/Hub.hxx" +#include "utils/HubDeviceSelect.hxx" +#include "utils/SocketCan.hxx" +#include "utils/constants.hxx" Executor<1> g_executor("g_executor", 0, 1024); Service g_service(&g_executor); @@ -58,6 +60,7 @@ OVERRIDE_CONST(gridconnect_buffer_delay_usec, 2000); int port = 12021; const char *device_path = nullptr; +const char *socket_can_path = nullptr; int upstream_port = 12021; const char *upstream_host = nullptr; bool timestamped = false; @@ -67,18 +70,28 @@ bool printpackets = false; void usage(const char *e) { - fprintf(stderr, "Usage: %s [-p port] [-d device_path] [-u upstream_host] " - "[-q upstream_port] [-m] [-n mdns_name] [-t] [-l]\n\n", - e); - fprintf(stderr, "GridConnect CAN HUB.\nListens to a specific TCP port, " - "reads CAN packets from the incoming connections using " - "the GridConnect protocol, and forwards all incoming " - "packets to all other participants.\n\nArguments:\n"); + fprintf(stderr, + "Usage: %s [-p port] [-d device_path] [-u upstream_host] " + "[-q upstream_port] [-m] [-n mdns_name] " +#if defined(__linux__) + "[-s socketcan_interface] " +#endif + "[-t] [-l]\n\n", + e); + fprintf(stderr, + "GridConnect CAN HUB.\nListens to a specific TCP port, " + "reads CAN packets from the incoming connections using " + "the GridConnect protocol, and forwards all incoming " + "packets to all other participants.\n\nArguments:\n"); fprintf(stderr, "\t-p port specifies the port number to listen on, " "default is 12021.\n"); fprintf(stderr, "\t-d device is a path to a physical device doing " "serial-CAN or USB-CAN. If specified, opens device and " "adds it to the hub.\n"); +#if defined(__linux__) + fprintf(stderr, "\t-s socketcan_interface is a socketcan device (e.g. 'can0'). " + "If specified, opens device and adds it to the hub.\n"); +#endif fprintf(stderr, "\t-u upstream_host is the host name for an upstream " "hub. If specified, this hub will connect to an upstream " "hub.\n"); @@ -100,7 +113,7 @@ void usage(const char *e) void parse_args(int argc, char *argv[]) { int opt; - while ((opt = getopt(argc, argv, "hp:d:u:q:tlmn:")) >= 0) + while ((opt = getopt(argc, argv, "hp:d:s:u:q:tlmn:")) >= 0) { switch (opt) { @@ -110,6 +123,11 @@ void parse_args(int argc, char *argv[]) case 'd': device_path = optarg; break; +#if defined(__linux__) + case 's': + socket_can_path = optarg; + break; +#endif case 'p': port = atoi(optarg); break; @@ -160,12 +178,28 @@ int appl_main(int argc, char *argv[]) void mdns_client_start(); void mdns_publish(const char *name, uint16_t port); - if (export_mdns) { + if (export_mdns) + { mdns_client_start(); mdns_publish(mdns_name, port); } #endif - +#if defined(__linux__) + if (socket_can_path) + { + int s = socketcan_open(socket_can_path, 1); + if (s >= 0) + { + new HubDeviceSelect(&can_hub0, s); + fprintf(stderr, "Opened SocketCan %s: fd %d\n", socket_can_path, s); + } + else + { + fprintf(stderr, "Failed to open SocketCan %s.\n", socket_can_path); + } + } +#endif + if (upstream_host) { connections.emplace_back(new UpstreamConnectionClient( diff --git a/applications/hub/targets/Makefile b/applications/hub/targets/Makefile index 3d075d4b8..5ff3034dd 100644 --- a/applications/hub/targets/Makefile +++ b/applications/hub/targets/Makefile @@ -1,5 +1,5 @@ SUBDIRS = linux.x86 \ linux.armv7a \ - mach.x86 + mach.x86_64 include $(OPENMRNPATH)/etc/recurse.mk diff --git a/applications/async_blink/targets/mach.x86/Makefile b/applications/hub/targets/linux.aarch64/Makefile similarity index 57% rename from applications/async_blink/targets/mach.x86/Makefile rename to applications/hub/targets/linux.aarch64/Makefile index 3bb6034b2..ed0c910ab 100644 --- a/applications/async_blink/targets/mach.x86/Makefile +++ b/applications/hub/targets/linux.aarch64/Makefile @@ -1 +1,3 @@ +-include ../../config.mk include $(OPENMRNPATH)/etc/prog.mk + diff --git a/applications/hub/targets/linux.rpi1/AvaHiMDNS.cxx b/applications/hub/targets/linux.rpi1/AvaHiMDNS.cxx new file mode 120000 index 000000000..71132b521 --- /dev/null +++ b/applications/hub/targets/linux.rpi1/AvaHiMDNS.cxx @@ -0,0 +1 @@ +../linux.x86/AvaHiMDNS.cxx \ No newline at end of file diff --git a/applications/hub/targets/linux.rpi1/README.md b/applications/hub/targets/linux.rpi1/README.md new file mode 100644 index 000000000..b2c866806 --- /dev/null +++ b/applications/hub/targets/linux.rpi1/README.md @@ -0,0 +1,36 @@ +# Adding SocketCAN support on the Raspberry Pi 4 + +These are my notes from adding SocketCAN support and building the **hub** application in a Raspberry Pi. + +- Start with the pre-built JMRI - RPI disk image from: [M Steve Todd's - JMRI RaspberryPi as Access Point](https://mstevetodd.com/rpi) + +- Install the MCP2517 CAN interface hardware from: [2-Channel CAN-BUS(FD) Shield for Raspberry Pi](https://www.seeedstudio.com/2-Channel-CAN-BUS-FD-Shield-for-Raspberry-Pi-p-4072.html) and followed their instructions for building the MCP2517 kernel device driver on their [Support Wiki](http://wiki.seeedstudio.com/2-Channel-CAN-BUS-FD-Shield-for-Raspberry-Pi/#software) page. + +- Install some needed packages on the RPi: + + `sudo apt install git doxygen libavahi-client-dev` + +- Download the OpenMRN source code to the RPi: + + `cd ~` + + `git clone https://github.com/bakerstu/openmrn.git` + +- Build the **hub** application: + + `cd openmrn/applications/hub/targets/linux.rpi1/` + + `make` + +- Configure the **can0** interface for 125,000 bps and run the **hub** application at system at start-up by creating the file: `/etc/network/interfaces.d/can0` with the following lines: + ``` + allow-hotplug can0 + iface can0 can static + bitrate 125000 + up /home/pi/openmrn/applications/hub/targets/linux.rpi1/hub -s $IFACE & + ``` + +- Configure the LCC Layout Connection in JMRI to use + - System Connection: `CAN via GridConnect Network Interface` + - IP Address/Host Name: `localhost` + - TCP/UDP Port: `12021` diff --git a/applications/hub/targets/mach.x86/lib/Makefile b/applications/hub/targets/mach.x86/lib/Makefile deleted file mode 100644 index a414ed98e..000000000 --- a/applications/hub/targets/mach.x86/lib/Makefile +++ /dev/null @@ -1 +0,0 @@ -include $(OPENMRNPATH)/etc/app_target_lib.mk diff --git a/applications/hub/targets/mach.x86/.gitignore b/applications/hub/targets/mach.x86_64/.gitignore similarity index 100% rename from applications/hub/targets/mach.x86/.gitignore rename to applications/hub/targets/mach.x86_64/.gitignore diff --git a/applications/hub/targets/mach.x86/Makefile b/applications/hub/targets/mach.x86_64/Makefile similarity index 100% rename from applications/hub/targets/mach.x86/Makefile rename to applications/hub/targets/mach.x86_64/Makefile diff --git a/applications/async_blink/targets/mach.x86/lib/Makefile b/applications/hub/targets/mach.x86_64/lib/Makefile similarity index 100% rename from applications/async_blink/targets/mach.x86/lib/Makefile rename to applications/hub/targets/mach.x86_64/lib/Makefile diff --git a/applications/simple_client/targets/Makefile b/applications/simple_client/targets/Makefile index 722e7127c..090bf476c 100644 --- a/applications/simple_client/targets/Makefile +++ b/applications/simple_client/targets/Makefile @@ -1,3 +1,3 @@ -SUBDIRS = linux.x86 mach.x86 +SUBDIRS = linux.x86 mach.x86_64 -include config.mk include $(OPENMRNPATH)/etc/recurse.mk diff --git a/applications/simple_client/targets/mach.x86_64/.gitignore b/applications/simple_client/targets/mach.x86_64/.gitignore new file mode 100644 index 000000000..7d1a13c95 --- /dev/null +++ b/applications/simple_client/targets/mach.x86_64/.gitignore @@ -0,0 +1 @@ +simple_client diff --git a/applications/simple_client/targets/mach.x86/Makefile b/applications/simple_client/targets/mach.x86_64/Makefile similarity index 100% rename from applications/simple_client/targets/mach.x86/Makefile rename to applications/simple_client/targets/mach.x86_64/Makefile diff --git a/applications/simple_client/targets/mach.x86/hardware.mk b/applications/simple_client/targets/mach.x86_64/hardware.mk similarity index 100% rename from applications/simple_client/targets/mach.x86/hardware.mk rename to applications/simple_client/targets/mach.x86_64/hardware.mk diff --git a/bin/revision.py b/bin/revision.py index f011cd8c0..2cfce0451 100755 --- a/bin/revision.py +++ b/bin/revision.py @@ -93,6 +93,8 @@ outputcxx += ' "' + gcc + '",\n' outputhxx += '"' + gcc + '\\n"\n' +main_git_hash = None + for x in inputs : print x # go into the root of the repo @@ -121,17 +123,19 @@ outputcxx += ':' + os.path.split(os.path.abspath(x))[1] outputhxx += '"' + git_hash + ':' + os.path.split(os.path.abspath(x))[1] + hashopts = "" + if dirty or untracked : - outputcxx += ':' - outputhxx += ':' + hashopts += ':' if dirty : - outputcxx += '-d' - outputhxx += '-d' + hashopts += '-d' if untracked : - outputcxx += '-u' - outputhxx += '-u' - outputcxx += '",\n' - outputhxx += '\\n"\n' + hashopts += '-u' + outputcxx += hashopts + '",\n' + outputhxx += hashopts + '\\n"\n' + + if main_git_hash is None: + main_git_hash = git_hash + hashopts outputcxx += ' nullptr\n' outputcxx += '};\n' @@ -142,6 +146,9 @@ outputhxx += '));\n' outputhxx += 'CDI_GROUP_END();\n' +if main_git_hash is not None: + outputhxx += '\n#define REVISION_GIT_HASH "' + main_git_hash + '"\n' + os.chdir(orig_dir) # generate the *.cxxout style content @@ -165,14 +172,18 @@ "-I", """Thu, """, "-I", """Fri, """, "-I", """Sat, """, options.output + 'Try.hxxout', - options.output + '.hxxout'], stdout=f_null) + options.output + '.hxxout'], + stdout=f_null, stderr=f_null) diffcxx = subprocess.call(['diff', "-I", """Sun, """, "-I", """Mon, """, "-I", """Tue, """, "-I", """Wed, """, "-I", """Thu, """, "-I", """Fri, """, "-I", """Sat, """, options.output + 'Try.cxxout', - options.output + '.cxxout'], stdout=f_null) + options.output + '.cxxout'], + stdout=f_null, stderr=f_null) +# disable this because we are not actually writing a cxx file above. +diffcxx = 0 if diffhxx != 0 : os.system('rm -f ' + options.output + '.hxxout') diff --git a/boards/armv7m/default_handlers.h b/boards/armv7m/default_handlers.h index 3ba22438d..f6616ce5e 100644 --- a/boards/armv7m/default_handlers.h +++ b/boards/armv7m/default_handlers.h @@ -149,6 +149,7 @@ __attribute__((__naked__)) static void hard_fault_handler(void) " mrsne r0, psp\n" " mov sp, r0 \n" " bkpt #1 \n" + " bx lr \n" ); #endif #if 0 diff --git a/boards/bracz-railcom/HwInit.cxx b/boards/bracz-railcom/HwInit.cxx index 56808c353..d955d7451 100644 --- a/boards/bracz-railcom/HwInit.cxx +++ b/boards/bracz-railcom/HwInit.cxx @@ -99,7 +99,7 @@ static TivaCan can0("/dev/can0", CAN0_BASE, INT_RESOLVE(INT_CAN0_, 0)); const unsigned TivaEEPROMEmulation::FAMILY = TM4C123; const size_t EEPROMEmulation::SECTOR_SIZE = 4 * 1024; -static TivaEEPROMEmulation eeprom("/dev/eeprom", 1024); +static TivaEEPROMEmulation eeprom("/dev/eeprom", 1524); const uint32_t RailcomDefs::UART_BASE[] = RAILCOM_BASE; const uint32_t RailcomDefs::UART_PERIPH[] = RAILCOM_PERIPH; @@ -107,7 +107,7 @@ const uint32_t RailcomDefs::UART_PERIPH[] = RAILCOM_PERIPH; TivaDAC dac; TivaGNDControl gnd_control; TivaBypassControl bypass_control; - +unsigned DCCDecode::sampleCount_ = 0; uint8_t RailcomDefs::feedbackChannel_ = 0xff; uint8_t dac_next_packet_mode = 0; diff --git a/boards/bracz-railcom/hardware.hxx b/boards/bracz-railcom/hardware.hxx index 35a485f46..57ffab6c1 100644 --- a/boards/bracz-railcom/hardware.hxx +++ b/boards/bracz-railcom/hardware.hxx @@ -15,6 +15,7 @@ #include "BlinkerGPIO.hxx" #include "utils/Debouncer.hxx" +#define HARDWARE_REVA GPIO_PIN(SW1, GpioInputPU, F, 4); GPIO_PIN(SW2, GpioInputPU, F, 0); @@ -128,6 +129,8 @@ struct Debug typedef DummyPin DccDecodeInterrupts; //typedef LED_GREEN_Pin DccDecodeInterrupts; + typedef DummyPin DccPacketFinishedHook; + // Flips every timer capture interrupt from the dcc deocder flow. // typedef DBG_SIGNAL_Pin RailcomE0; //typedef LED_GREEN_Pin RailcomE0; @@ -176,6 +179,7 @@ struct RailcomDefs static bool need_ch1_cutout() { return true; } + static void middle_cutout_hook() {} static void enable_measurement(bool); static void disable_measurement(); @@ -444,8 +448,6 @@ struct DCCDecode HWREG(TIMER_BASE + TIMER_O_TAMR) |= (TIMER_TAMR_TAMIE); } - static void cap_event_hook() {} - static const auto RCOM_TIMER = TIMER_A; static const auto SAMPLE_PERIOD_CLOCKS = 60000; //static const auto SAMPLE_TIMER_TIMEOUT = TIMER_TIMA_TIMEOUT; @@ -466,6 +468,10 @@ struct DCCDecode static inline void dcc_before_cutout_hook(); static inline void dcc_packet_finished_hook(); static inline void after_feedback_hook(); + + /// counts how many edges / transitions we had on the DCC signal. + static unsigned sampleCount_; + static inline void cap_event_hook() { ++sampleCount_; } }; #endif // ! pindefs_only diff --git a/boards/ti-bracz-acc3/hardware.v3.hxx b/boards/ti-bracz-acc3/hardware.v3.hxx index fe875b6ea..414315132 100644 --- a/boards/ti-bracz-acc3/hardware.v3.hxx +++ b/boards/ti-bracz-acc3/hardware.v3.hxx @@ -174,7 +174,8 @@ struct RailcomHw static bool need_ch1_cutout() { return true; } static uint8_t get_feedback_channel() { return 0xff; } - + static void middle_cutout_hook() {} + /// @returns a bitmask telling which pins are active. Bit 0 will be set if /// channel 0 is active (drawing current). static uint8_t sample() { diff --git a/boards/ti-ek-tm4c123gxl-launchpad/HwInit.cxx b/boards/ti-ek-tm4c123gxl-launchpad/HwInit.cxx index 4a113c0a2..15f7b211a 100644 --- a/boards/ti-ek-tm4c123gxl-launchpad/HwInit.cxx +++ b/boards/ti-ek-tm4c123gxl-launchpad/HwInit.cxx @@ -172,6 +172,7 @@ struct RailcomDefs static void disable_measurement() {} static bool need_ch1_cutout() { return true; } static uint8_t get_feedback_channel() { return 0xff; } + static void middle_cutout_hook() {} /** @returns a bitmask telling which pins are active. Bit 0 will be set if * channel 0 is active (drawing current).*/ diff --git a/etc/cov.mk b/etc/cov.mk index db1f5d37c..37c865f8c 100644 --- a/etc/cov.mk +++ b/etc/cov.mk @@ -18,17 +18,19 @@ HOST_TARGET := 1 STARTGROUP := -Wl,--start-group ENDGROUP := -Wl,--end-group +TESTOPTIMIZATION=-O0 + ifdef SKIP_COVERAGE -ARCHOPTIMIZATION = -g -O0 +ARCHOPTIMIZATION = -g $(TESTOPTIMIZATION) else -ARCHOPTIMIZATION = -g -O0 -fprofile-arcs -ftest-coverage +ARCHOPTIMIZATION = -g $(TESTOPTIMIZATION) -fprofile-arcs -ftest-coverage endif CSHAREDFLAGS = -c -frandom-seed=$(shell echo $(abspath $<) | md5sum | sed 's/\(.*\) .*/\1/') $(ARCHOPTIMIZATION) $(INCLUDES) -Wall -Werror -Wno-unknown-pragmas -MD -MP -fno-stack-protector -D_GNU_SOURCE -DGTEST CFLAGS = $(CSHAREDFLAGS) -std=gnu99 $(CFLAGSEXTRA) -CXXFLAGS = $(CSHAREDFLAGS) -std=c++1y -D__STDC_FORMAT_MACROS \ +CXXFLAGS = $(CSHAREDFLAGS) -std=c++14 -D__STDC_FORMAT_MACROS \ -D__STDC_LIMIT_MACROS $(CXXFLAGSEXTRA) #-D__LINEAR_MAP__ @@ -36,6 +38,15 @@ LDFLAGS = $(ARCHOPTIMIZATION) -Wl,-Map="$(@:%=%.map)" SYSLIB_SUBDIRS += SYSLIBRARIES = -lrt -lpthread -lavahi-client -lavahi-common $(SYSLIBRARIESEXTRA) +ifdef RUN_GPERF +CXXFLAGS += -DWITHGPERFTOOLS +LDFLAGS += -DWITHGPERFTOOLS +SYSLIBRARIES += -lprofiler +TESTOPTIMIZATION = -O3 +SKIP_COVERAGE = 1 +endif + + ifndef SKIP_COVERAGE LDFLAGS += -pg SYSLIBRARIES += -lgcov diff --git a/etc/linux.aarch64.mk b/etc/linux.aarch64.mk new file mode 100644 index 000000000..fb497c9d4 --- /dev/null +++ b/etc/linux.aarch64.mk @@ -0,0 +1,45 @@ +# Get the toolchain paths for openmrn +include $(OPENMRNPATH)/etc/path.mk + + +ifndef TOOLPATH +#TOOLPATHCOMMAND := $(shell \ +#sh -c "which aarch64-linux-gnu-gcc" \ +#) +TOOLPATH := $(AARCH64LINUXGCCPATH) +endif + +$(info armv7alinux toolpath '$(TOOLPATH)') + +# Get the $(CFLAGSENV), $(CXXFLAGSENV), $(LDFLAGSENV) +include $(OPENMRNPATH)/etc/env.mk + +CC = $(TOOLPATH)/aarch64-linux-gnu-gcc +CXX = $(TOOLPATH)/aarch64-linux-gnu-g++ +AR = $(TOOLPATH)/aarch64-linux-gnu-ar +LD = $(TOOLPATH)/aarch64-linux-gnu-g++ +OBJDUMP = $(TOOLPATH)/aarch64-linux-gnu-objdump + +AROPTS=D + +HOST_TARGET := 1 + +STARTGROUP := -Wl,--start-group +ENDGROUP := -Wl,--end-group + +ARCHOPTIMIZATION = -g3 -O0 -march=armv8-a + +CSHAREDFLAGS = -c $(ARCHOPTIMIZATION) -Wall -Werror -Wno-unknown-pragmas \ + -MD -MP -fno-stack-protector -D_GNU_SOURCE + +CFLAGS = $(CSHAREDFLAGS) -std=gnu99 + +CXXFLAGS = $(CSHAREDFLAGS) -std=c++0x -D__STDC_FORMAT_MACROS \ + -D__STDC_LIMIT_MACROS -D__USE_LIBSTDCPP__ + +LDFLAGS = $(ARCHOPTIMIZATION) -Wl,-Map="$(@:%=%.map)" +SYSLIB_SUBDIRS += +SYSLIBRARIES = -lrt -lpthread + +EXTENTION = + diff --git a/etc/linux.x86.mk b/etc/linux.x86.mk index bd3f9f5e6..27f940908 100644 --- a/etc/linux.x86.mk +++ b/etc/linux.x86.mk @@ -35,7 +35,7 @@ CFLAGS = $(CSHAREDFLAGS) -std=gnu99 \ $(CFLAGSENV) $(CFLAGSEXTRA) \ -CXXFLAGS = $(CSHAREDFLAGS) -std=c++0x -D__STDC_FORMAT_MACROS \ +CXXFLAGS = $(CSHAREDFLAGS) -std=c++14 -D__STDC_FORMAT_MACROS \ -D__STDC_LIMIT_MACROS $(CXXFLAGSENV) \ $(CXXFLAGSENV) $(CXXFLAGSEXTRA) \ diff --git a/etc/mach.x86.mk b/etc/mach.x86.mk deleted file mode 100644 index b94225e19..000000000 --- a/etc/mach.x86.mk +++ /dev/null @@ -1,30 +0,0 @@ -ifndef TOOLPATH -TOOLPATH := $(shell \ -sh -c "if [ -d /usr/include/mach ]; then echo /usr/bin; \ - else echo; fi" \ -) -endif - -# Get the $(CFLAGSENV), $(CXXFLAGSENV), $(LDFLAGSENV) -include $(OPENMRNPATH)/etc/env.mk - -CC = gcc -CXX = g++ -AR = ar -LD = g++ - -STARTGROUP := -ENDGROUP := - -INCLUDES += -I$(OPENMRNPATH)/include/mach - -CFLAGS = -c -g -O0 -Wall -Werror -MD -MP -std=gnu99 -m32 -fno-stack-protector \ - -D_GNU_SOURCE -CXXFLAGS = -c -g -O0 -Wall -Werror -MD -MP -std=c++0x -m32 -fno-stack-protector \ - -D_GNU_SOURCE -D__STDC_FORMAT_MACROS - -LDFLAGS = -g -m32 -SYSLIBRARIES = -lpthread - -EXTENTION = - diff --git a/etc/mach.x86_64.mk b/etc/mach.x86_64.mk index 5842b51db..72ef97bb1 100644 --- a/etc/mach.x86_64.mk +++ b/etc/mach.x86_64.mk @@ -1,8 +1,8 @@ -ifndef TOOLPATH -TOOLPATH := $(shell \ -sh -c "if [ -d /usr/include/mach ]; then echo /usr/bin; \ - else echo; fi" \ -) +# Get the toolchain path +include $(OPENMRNPATH)/etc/path.mk + +ifeq ($(shell uname -sm),Darwin x86_64) +TOOLPATH := $(HOSTCLANGPPPATH) endif $(info mach toolpath '$(TOOLPATH)') @@ -10,19 +10,19 @@ $(info mach toolpath '$(TOOLPATH)') # Get the $(CFLAGSENV), $(CXXFLAGSENV), $(LDFLAGSENV) include $(OPENMRNPATH)/etc/env.mk -CC = gcc -CXX = g++ +CC = clang +CXX = clang++ AR = ar -LD = g++ +LD = clang++ STARTGROUP := ENDGROUP := INCLUDES += -I$(OPENMRNPATH)/include/mach -CFLAGS = -c -g -O0 -Wall -Werror -MD -MP -std=gnu99 -fno-stack-protector \ +CFLAGS = -c -g -O0 -Wall -Werror -MD -MP -std=c99 -fno-stack-protector \ -D_GNU_SOURCE -CXXFLAGS = -c -g -O0 -Wall -Werror -MD -MP -std=c++0x -fno-stack-protector \ +CXXFLAGS = -c -g -O0 -Wall -Werror -MD -MP -std=c++14 -fno-stack-protector \ -D_GNU_SOURCE LDFLAGS = -g diff --git a/etc/path.mk b/etc/path.mk index 2d6a1b05d..261a95eff 100644 --- a/etc/path.mk +++ b/etc/path.mk @@ -301,6 +301,17 @@ ARMLINUXGCCPATH:=$(TRYPATH) endif endif #ARMLINUXGCCPATH +################### AARCH64-LINUX GCC PATH ##################### +ifndef AARCH64LINUXGCCPATH +SEARCHPATH := \ + /usr/bin \ + +TRYPATH:=$(call findfirst,aarch64-linux-gnu-gcc,$(SEARCHPATH)) +ifneq ($(TRYPATH),) +AARCH64LINUXGCCPATH:=$(TRYPATH) +endif +endif #AARCH64LINUXGCCPATH + ################### TI-CC3200-SDK ##################### ifndef TICC3200SDKPATH SEARCHPATH := \ @@ -571,6 +582,18 @@ CLANGPPPATH:=$(TRYPATH) endif endif #CLANGPPPATH +##################### HOSTCLANGPP ###################### +ifndef HOSTCLANGPPPATH +SEARCHPATH := \ + /usr/bin \ + + +TRYPATH:=$(call findfirst,clang++,$(SEARCHPATH)) +ifneq ($(TRYPATH),) +HOSTCLANGPPPATH:=$(TRYPATH) +endif +endif #HOSTCLANGPPPATH + ##################### NODEJS ###################### ifndef NODEJSPATH SEARCHPATH := \ diff --git a/etc/prog.mk b/etc/prog.mk index 70bc0e46b..7b1e2f00c 100644 --- a/etc/prog.mk +++ b/etc/prog.mk @@ -244,7 +244,7 @@ cg.svg: $(EXECUTABLE).ndlst $(OPENMRNPATH)/bin/callgraph.py .SUFFIXES: .SUFFIXES: .o .c .cxx .cpp .S .xml .cout .cxxout -.xml.o: $(OPENMRNPATH)/bin/build_cdi.py +%.xml: %.o $(OPENMRNPATH)/bin/build_cdi.py $(OPENMRNPATH)/bin/build_cdi.py -i $< -o $*.cxxout $(CXX) $(CXXFLAGS) -x c++ $*.cxxout -o $@ $(CXX) -MM $(CXXFLAGS) -x c++ $*.cxxout > $*.d diff --git a/etc/test.mk b/etc/test.mk index 7ca2fe721..e60514e6c 100644 --- a/etc/test.mk +++ b/etc/test.mk @@ -45,11 +45,13 @@ LIBS = $(STARTGROUP) \ $(ENDGROUP) \ $(LINKCORELIBS) +TESTOPTIMIZATION=-O0 + INCLUDES += -I$(GTESTPATH)/include -I$(GMOCKPATH)/include -I$(GMOCKPATH) \ -I$(OPENMRNPATH)/src -I$(OPENMRNPATH)/include -CFLAGS += -DGTEST $(INCLUDES) -Wno-unused-but-set-variable -fprofile-arcs -ftest-coverage -O0 -CXXFLAGS += -DGTEST $(INCLUDES) -Wno-unused-but-set-variable -fprofile-arcs -ftest-coverage -O0 -SYSLIBRARIES += -lgcov -fprofile-arcs -ftest-coverage -O0 +CFLAGS += -DGTEST $(INCLUDES) -Wno-unused-but-set-variable -fprofile-arcs -ftest-coverage $(TESTOPTIMIZATION) +CXXFLAGS += -DGTEST $(INCLUDES) -Wno-unused-but-set-variable -fprofile-arcs -ftest-coverage $(TESTOPTIMIZATION) +SYSLIBRARIES += -lgcov -fprofile-arcs -ftest-coverage $(TESTOPTIMIZATION) LDFLAGS += -L$(LIBDIR) .SUFFIXES: diff --git a/include/nmranet_config.h b/include/nmranet_config.h index e23a20adb..1e44f39e3 100644 --- a/include/nmranet_config.h +++ b/include/nmranet_config.h @@ -122,6 +122,10 @@ DECLARE_CONST(remote_alias_cache_size); /** Number of entries in the local alias cache */ DECLARE_CONST(local_alias_cache_size); +/** Keep this many allocated but unused aliases around. (Currently supported + * values are 0 or 1.) */ +DECLARE_CONST(reserve_unused_alias_count); + /** Maximum number of local nodes */ DECLARE_CONST(local_nodes_count); @@ -146,5 +150,14 @@ DECLARE_CONST(enable_all_memory_space); * standard. */ DECLARE_CONST(node_init_identify); +/** How many CAN frames should the bulk alias allocator be sending at the same + * time. */ +DECLARE_CONST(bulk_alias_num_can_frames); + +/** Stack size for @ref SocketListener threads. */ +DECLARE_CONST(socket_listener_stack_size); + +/** Number of sockets to allow for @ref SocketListener backlog. */ +DECLARE_CONST(socket_listener_backlog); #endif /* _nmranet_config_h_ */ diff --git a/include/openmrn_features.h b/include/openmrn_features.h index 870b97a40..8a619a0f6 100644 --- a/include/openmrn_features.h +++ b/include/openmrn_features.h @@ -48,6 +48,13 @@ #define OPENMRN_FEATURE_REENT 1 #endif +#if defined(__linux__) || defined(__MACH__) || defined(__WINNT__) || defined(ESP32) || defined(OPENMRN_FEATURE_DEVTAB) +/// Enables the code using ::open ::close ::read ::write for non-volatile +/// storage, FileMemorySpace for the configuration space, and +/// SNIP_DYNAMIC_FILE_NAME for node names. +#define OPENMRN_HAVE_POSIX_FD 1 +#endif + /// @todo this should probably be a whitelist: __linux__ || __MACH__. #if !defined(__FreeRTOS__) && !defined(__WINNT__) && !defined(ESP32) && \ !defined(ARDUINO) && !defined(ESP_NONOS) diff --git a/src/console/Console.hxx b/src/console/Console.hxx index 4444e5c40..41d8cf0b7 100644 --- a/src/console/Console.hxx +++ b/src/console/Console.hxx @@ -369,7 +369,9 @@ private: */ HASSERT(fdIn == fdOut); fclose(fp); - close(fdIn); + /* There is no need for a "close(fdIn)" because the "fclose(fp)" + * will already have completed that operation. + */ free(line); } diff --git a/src/dcc/Defs.hxx b/src/dcc/Defs.hxx index 375c1b70b..f7ddc1754 100644 --- a/src/dcc/Defs.hxx +++ b/src/dcc/Defs.hxx @@ -35,6 +35,8 @@ #ifndef _DCC_DEFS_HXX_ #define _DCC_DEFS_HXX_ +#include + namespace dcc { /// Which address type this legacy train node uses. These address types diff --git a/src/dcc/Loco.cxx b/src/dcc/Loco.cxx index cad1533b7..78462a1b1 100644 --- a/src/dcc/Loco.cxx +++ b/src/dcc/Loco.cxx @@ -110,7 +110,8 @@ void DccTrain::get_next_packet(unsigned code, Packet *packet) { case FUNCTION0: { - packet->add_dcc_function0_4(this->p.fn_ & 0x1F); + packet->add_dcc_function0_4( + (this->p.fn_ & 0x1E) | this->get_effective_f0()); return; } case FUNCTION5: @@ -171,7 +172,7 @@ MMOldTrain::~MMOldTrain() void MMOldTrain::get_next_packet(unsigned code, Packet *packet) { packet->start_mm_packet(); - packet->add_mm_address(MMAddress(p.address_), p.fn_ & 1); + packet->add_mm_address(MMAddress(p.address_), get_effective_f0()); if (code == ESTOP) { @@ -210,7 +211,7 @@ MMNewTrain::~MMNewTrain() void MMNewTrain::get_next_packet(unsigned code, Packet *packet) { packet->start_mm_packet(); - packet->add_mm_address(MMAddress(p.address_), p.fn_ & 1); + packet->add_mm_address(MMAddress(p.address_), get_effective_f0()); if (code == REFRESH) { diff --git a/src/dcc/Loco.hxx b/src/dcc/Loco.hxx index 425f13329..caed8643b 100644 --- a/src/dcc/Loco.hxx +++ b/src/dcc/Loco.hxx @@ -35,12 +35,18 @@ #ifndef _DCC_LOCO_HXX_ #define _DCC_LOCO_HXX_ +#include "dcc/Defs.hxx" #include "dcc/Packet.hxx" #include "dcc/PacketSource.hxx" #include "dcc/UpdateLoop.hxx" -#include "dcc/Defs.hxx" +#include "utils/constants.hxx" #include "utils/logging.h" +/// At this function number there will be three virtual functions available on +/// the OpenLCB TrainImpl, controlling advanced functions related to the light +/// (f0) function of trains. +DECLARE_CONST(dcc_virtual_f0_offset); + namespace dcc { @@ -111,10 +117,12 @@ public: return; } p.lastSetSpeed_ = new_speed; + unsigned previous_light = get_effective_f0(); if (speed.direction() != p.direction_) { p.directionChanged_ = 1; p.direction_ = speed.direction(); + update_f0_direction_changed(); } float f_speed = speed.mph(); if (f_speed > 0) @@ -131,7 +139,18 @@ public: { p.speed_ = 0; } + unsigned light = get_effective_f0(); + if (previous_light && !light) + { + // Turns off light first then sends speed packet. + packet_processor_notify_update(this, p.get_fn_update_code(0)); + } packet_processor_notify_update(this, SPEED); + if (light && !previous_light) + { + // Turns on light after sending speed packets. + packet_processor_notify_update(this, p.get_fn_update_code(0)); + } } /// @return the last set speed. @@ -169,6 +188,59 @@ public: /// (0..28), @param value is 0 for funciton OFF, 1 for function ON. void set_fn(uint32_t address, uint16_t value) OVERRIDE { + const uint32_t virtf0 = config_dcc_virtual_f0_offset(); + if (address == 0 && p.f0SetDirectional_) + { + if (p.direction_ == 0) + { + p.f0OnForward_ = value ? 1 : 0; + } + else + { + p.f0OnReverse_ = value ? 1 : 0; + } + // continue into the handling of f0. + } + else if (address == virtf0 + VIRTF0_DIRECTIONAL_ENABLE) + { + if (value) + { + p.f0SetDirectional_ = 1; + // Populates new state of separate f0 forward and f0 + // reverse. + if (p.direction_ == 0) + { + p.f0OnForward_ = p.fn_ & 1; + p.f0OnReverse_ = 0; + } + else + { + p.f0OnReverse_ = p.fn_ & 1; + p.f0OnForward_ = 0; + } + } + else + { + p.f0SetDirectional_ = 0; + // whatever value we have in fn_[0] now is going to be the + // new state, so we don't change anything. + } + // This command never changes f0, so no packets need to be sent to + // the track. + return; + } + else if (address == virtf0 + VIRTF0_BLANK_FWD) + { + p.f0BlankForward_ = value ? 1 : 0; + packet_processor_notify_update(this, p.get_fn_update_code(0)); + return; + } + else if (address == virtf0 + VIRTF0_BLANK_REV) + { + p.f0BlankReverse_ = value ? 1 : 0; + packet_processor_notify_update(this, p.get_fn_update_code(0)); + return; + } if (address > p.get_max_fn()) { // Ignore. @@ -189,6 +261,19 @@ public: /// not known. @param address is the function address. uint16_t get_fn(uint32_t address) OVERRIDE { + const uint32_t virtf0 = config_dcc_virtual_f0_offset(); + if (address == virtf0 + VIRTF0_DIRECTIONAL_ENABLE) + { + return p.f0SetDirectional_; + } + else if (address == virtf0 + VIRTF0_BLANK_FWD) + { + return p.f0BlankForward_; + } + else if (address == virtf0 + VIRTF0_BLANK_REV) + { + return p.f0BlankReverse_; + } if (address > p.get_max_fn()) { // Unknown. @@ -208,6 +293,56 @@ public: } protected: + /// Function number of "enable directional F0". Offset from config option + /// dcc_virtual_f0_offset. When this function is enabled, F0 is set and + /// cleared separately for forward and reverse drive. + static constexpr unsigned VIRTF0_DIRECTIONAL_ENABLE = 0; + /// Function number of "Blank F0 Forward". Offset from config option + /// dcc_virtual_f0_offset. When this function is enabled, F0 on the track + /// packet will turn off when direction==forward, even if function 0 is + /// set. + static constexpr unsigned VIRTF0_BLANK_FWD = 1; + /// Function number of "Blank F0 Reverse". Offset from config option + /// dcc_virtual_f0_offset. When this function is enabled, F0 on the track + /// packet will turn off when direction==reverse, even if function 0 is + /// set. + static constexpr unsigned VIRTF0_BLANK_REV = 2; + + /// @return the currently applicable value of F0 to be sent out to the + /// packets (1 if on, 0 if off). + unsigned get_effective_f0() + { + unsigned is_on = p.f0SetDirectional_ == 0 ? (p.fn_ & 1) + : p.direction_ == 0 ? p.f0OnForward_ + : p.f0OnReverse_; + if (p.direction_ == 0 && p.f0BlankForward_) + { + is_on = 0; + } + if (p.direction_ == 1 && p.f0BlankReverse_) + { + is_on = 0; + } + return is_on; + } + + /// Updates the f0 states after a direction change occurred. + void update_f0_direction_changed() + { + if (p.f0SetDirectional_) + { + p.fn_ &= ~1; + if (p.direction_ == 0 && p.f0OnForward_) + { + p.fn_ |= 1; + } + if (p.direction_ == 1 && p.f0OnReverse_) + { + p.fn_ |= 1; + } + } + } + /// Payload -- actual data we know about the train. P p; }; @@ -235,6 +370,16 @@ struct Dcc28Payload unsigned speed_ : 5; /// Whether the direction change packet still needs to go out. unsigned directionChanged_ : 1; + /// 1 if the F0 function should be set/get in a directional way. + unsigned f0SetDirectional_ : 1; + /// 1 if directional f0 is used and f0 is on for F. + unsigned f0OnForward_ : 1; + /// 1 if directional f0 is used and f0 is on for R. + unsigned f0OnReverse_ : 1; + /// 1 if F0 should be turned off when dir==forward. + unsigned f0BlankForward_ : 1; + /// 1 if F0 should be turned off when dir==reverse. + unsigned f0BlankReverse_ : 1; /** @return the number of speed steps (in float). */ static unsigned get_speed_steps() @@ -328,6 +473,17 @@ struct Dcc128Payload /// Whether the direction change packet still needs to go out. unsigned directionChanged_ : 1; + /// 1 if the F0 function should be set/get in a directional way. + unsigned f0SetDirectional_ : 1; + /// 1 if directional f0 is used and f0 is on for F. + unsigned f0OnForward_ : 1; + /// 1 if directional f0 is used and f0 is on for R. + unsigned f0OnReverse_ : 1; + /// 1 if F0 should be turned off when dir==forward. + unsigned f0BlankForward_ : 1; + /// 1 if F0 should be turned off when dir==reverse. + unsigned f0BlankReverse_ : 1; + /** @return the number of speed steps (the largest valid speed step). */ static unsigned get_speed_steps() { @@ -393,6 +549,17 @@ struct MMOldPayload /// Speed step we last set. unsigned speed_ : 4; + /// 1 if the F0 function should be set/get in a directional way. + unsigned f0SetDirectional_ : 1; + /// 1 if directional f0 is used and f0 is on for F. + unsigned f0OnForward_ : 1; + /// 1 if directional f0 is used and f0 is on for R. + unsigned f0OnReverse_ : 1; + /// 1 if F0 should be turned off when dir==forward. + unsigned f0BlankForward_ : 1; + /// 1 if F0 should be turned off when dir==reverse. + unsigned f0BlankReverse_ : 1; + /** @return the number of speed steps (in float). */ unsigned get_speed_steps() { @@ -459,6 +626,17 @@ struct MMNewPayload /// internal refresh cycle state machine unsigned nextRefresh_ : 3; + /// 1 if the F0 function should be set/get in a directional way. + unsigned f0SetDirectional_ : 1; + /// 1 if directional f0 is used and f0 is on for F. + unsigned f0OnForward_ : 1; + /// 1 if directional f0 is used and f0 is on for R. + unsigned f0OnReverse_ : 1; + /// 1 if F0 should be turned off when dir==forward. + unsigned f0BlankForward_ : 1; + /// 1 if F0 should be turned off when dir==reverse. + unsigned f0BlankReverse_ : 1; + /** @return the number of speed steps (in float). */ unsigned get_speed_steps() { diff --git a/src/dcc/RailcomPortDebug.hxx b/src/dcc/RailcomPortDebug.hxx index dc2e61eb2..c41d224bd 100644 --- a/src/dcc/RailcomPortDebug.hxx +++ b/src/dcc/RailcomPortDebug.hxx @@ -129,6 +129,72 @@ private: dcc::RailcomHubFlow *parent_; }; +/// This flow listens to Railcom packets coming from the hub, and if they are +/// correctly decoded, pulses the given GPIO output. Correctly decoded is +/// defined as having every single byte be a correct 4/8 codepoint. +class RailcomToGpioFlow : public dcc::RailcomHubPortInterface +{ +public: + /// Constructor. + /// @param source is the railcom hub to listen to. + /// @param output + RailcomToGpioFlow(dcc::RailcomHubFlow *source, const Gpio *output) + : parent_(source) + , output_(output) + { + source->register_port(this); + } + + ~RailcomToGpioFlow() + { + parent_->unregister_port(this); + } + +private: + /// Incoming railcom data. + /// + /// @param d railcom buffer. + /// @param prio priority + void send(Buffer *d, unsigned prio) OVERRIDE + { + AutoReleaseBuffer rb(d); + dcc::Feedback &fb = *d->data(); + if (fb.channel >= 0xfe) + { + // Occupancy feedback, not railcom data. + return; + } + unsigned correct = 0; + unsigned total = 0; + for (unsigned i = 0; i < fb.ch1Size; i++) + { + ++total; + correct += (dcc::railcom_decode[fb.ch1Data[i]] != RailcomDefs::INV) + ? 1 + : 0; + } + for (unsigned i = 0; i < fb.ch2Size; i++) + { + ++total; + correct += (dcc::railcom_decode[fb.ch2Data[i]] != RailcomDefs::INV) + ? 1 + : 0; + } + if (total > 0 && correct == total) + { + // Produces a short pulse on the output + output_->write(true); + for (volatile int i = 0; i < 3000; i++) { } + output_->write(false); + } + } + + /// Flow to which we are registered. + dcc::RailcomHubFlow *parent_; + /// Output gpio to toggle. + const Gpio *output_; +}; // RailcomToGpioFlow + } // namespace dcc namespace openlcb diff --git a/src/dcc/dcc_constants.cxx b/src/dcc/dcc_constants.cxx new file mode 100644 index 000000000..e4070dd28 --- /dev/null +++ b/src/dcc/dcc_constants.cxx @@ -0,0 +1,37 @@ +/** \copyright + * Copyright (c) 2014, Balazs Racz + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * \file dcc_constants.cxx + * + * Default values of constants from the dcc package. + * + * @author Balazs Racz + * @date 10 May 2014 + */ + +#include "utils/constants.hxx" + +DEFAULT_CONST(dcc_virtual_f0_offset, 100); diff --git a/src/executor/Executor.cxx b/src/executor/Executor.cxx index ed8394dd2..3fcc69317 100644 --- a/src/executor/Executor.cxx +++ b/src/executor/Executor.cxx @@ -37,6 +37,7 @@ #include "executor/Executor.hxx" +#include "openmrn_features.h" #include #ifdef __WINNT__ @@ -228,14 +229,6 @@ void *ExecutorBase::entry() return nullptr; } -#elif defined(ARDUINO) && !defined(ESP32) - -void *ExecutorBase::entry() -{ - DIE("Arduino code should not start the executor."); - return nullptr; -} - #elif defined(ESP_NONOS) #define EXECUTOR_TASK_PRIO USER_TASK_PRIO_0 @@ -285,6 +278,14 @@ void ICACHE_FLASH_ATTR *ExecutorBase::entry() return nullptr; } +#elif OPENMRN_FEATURE_SINGLE_THREADED + +void *ExecutorBase::entry() +{ + DIE("Arduino code should not start the executor."); + return nullptr; +} + #else /** Thread entry point. * @return Should never return diff --git a/src/freertos_drivers/arduino/ArduinoFs.hxx b/src/freertos_drivers/arduino/ArduinoFs.hxx new file mode 100644 index 000000000..2b223c8ab --- /dev/null +++ b/src/freertos_drivers/arduino/ArduinoFs.hxx @@ -0,0 +1,304 @@ +/** \copyright + * Copyright (c) 2020, Balazs Racz + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * \file ArduinoFs.hxx + * + * Single-file POSIX compatible filesystem using Arduino's EEPROM + * implementation for STM32. + * + * This file needs to be included into the .ino of the arduino sketch. It may + * only be compiled once as it has definitions of C library functions. + * + * @author Balazs Racz + * @date 18 July 2020 + */ + +#ifndef _FREERTOS_DRIVERS_ARDUINO_ARDUINOFS_HXX_ +#define _FREERTOS_DRIVERS_ARDUINO_ARDUINOFS_HXX_ + +#ifndef ARDUINO_ARCH_STM32 +#error This module only works for STM32 boards. +#endif + +#ifndef OPENMRN_HAVE_POSIX_FD +#error You must add a file build_opt.h to the sketch directory and add -DOPENMRN_HAVE_POSIX_FD to it to make use of this module. +#endif + +#include "stm32_eeprom.h" +#include + +#define EEPROM_FILENAME "/dev/eeprom" + +/// Class that holds information and code for the single-file filesystem for +/// emulating eeprom. +class FsStatic +{ +public: + /// Number of possible open file desriptors. + static constexpr unsigned MAX_FD = 8; + /// Offset markng that a file descriptor is not in use. + static constexpr off_t UNUSED_FILE = (off_t)-1; + + /// We have one of these for each open file descriptor. + struct FileInfo + { + /// POSIX file offset. + off_t offset = UNUSED_FILE; + + /// @return true if this file descriptor is in use. + bool in_use() + { + return offset != UNUSED_FILE; + } + + /// Marks the file descriptor to be in use. + void open() + { + offset = 0; + } + + /// Marks the file descriptor to be not in use. + void close() + { + offset = UNUSED_FILE; + } + }; + + /// Stores all file descriptors. + static FileInfo fds[MAX_FD]; + + /// Lookup a file descriptor. + /// @param fd the file descriptor. + /// @return nullptr if fd is an invalid file descriptor (in which case also + /// sets errno), otherwise the file descriptor structure. + static FileInfo *get_file(int fd) + { + if (fd >= MAX_FD || !fds[fd].in_use()) + { + errno = EBADF; + return nullptr; + } + return &fds[fd]; + } + + /// Allocates a new file descriptor. + /// @return new fd. If there is no free file descriptor, returns -1 and + /// sets errno. + static int new_fd() + { + for (int fd = 0; fd < MAX_FD; ++fd) + { + if (!fds[fd].in_use()) + { + fds[fd].open(); + return fd; + } + } + errno = ENFILE; + return -1; + } + + /// If there is unflushed writes, performs the flash write. + static void flush_if_dirty() + { + if (dirty_) + { + eeprom_buffer_flush(); + dirty_ = 0; + } + } + + /// 1 if we have filled the eeprom buffer at least once since startup. + static uint8_t loaded_; + /// 1 if we have unflushed written data in the eeprom buffer. + static uint8_t dirty_; +}; + +FsStatic::FileInfo FsStatic::fds[FsStatic::MAX_FD]; +uint8_t FsStatic::loaded_ = 0; +uint8_t FsStatic::dirty_ = 0; + +extern "C" +{ + +int _open_r(struct _reent *reent, const char *path, int flags, int mode) +{ + if (strcmp(path, EEPROM_FILENAME) != 0) + { + errno = ENOENT; + return -1; + } + if (!FsStatic::loaded_) + { + eeprom_buffer_fill(); + FsStatic::loaded_ = 1; + } + return FsStatic::new_fd(); +} + +int _close_r(struct _reent *reent, int fd) +{ + FsStatic::FileInfo *finfo = FsStatic::get_file(fd); + if (!finfo) + { + return -1; + } + finfo->close(); + FsStatic::flush_if_dirty(); + return 0; +} + +ssize_t _read_r(struct _reent *reent, int fd, void *buf, size_t count) +{ + FsStatic::FileInfo *finfo = FsStatic::get_file(fd); + if (!finfo) + { + return -1; + } + ssize_t ret = 0; + uint8_t *dst = (uint8_t *)buf; + int left = (int)E2END - (int)finfo->offset; + if (left < 0) + { + left = 0; + } + if (left < count) + { + count = left; + } + while (count > 0) + { + *dst = eeprom_buffered_read_byte(finfo->offset); + ++dst; + ++finfo->offset; + --count; + ++ret; + } + return ret; +} + +ssize_t _write_r(struct _reent *reent, int fd, const void *buf, size_t count) +{ + FsStatic::FileInfo *finfo = FsStatic::get_file(fd); + if (!finfo) + { + return -1; + } + ssize_t ret = 0; + const uint8_t *src = (const uint8_t *)buf; + int left = (int)E2END - (int)finfo->offset; + if (left < 0) + { + left = 0; + } + if (left < count) + { + count = left; + } + if (count) + { + FsStatic::dirty_ = 1; + } + while (count > 0) + { + eeprom_buffered_write_byte(finfo->offset, *src); + ++src; + ++finfo->offset; + --count; + ++ret; + } + return ret; +} + +int fsync(int fd) +{ + FsStatic::FileInfo *finfo = FsStatic::get_file(fd); + if (!finfo) + { + return -1; + } + FsStatic::flush_if_dirty(); + return 0; +} + +int _stat_r(struct _reent *reent, const char *path, struct stat *stat) +{ + if (strcmp(path, EEPROM_FILENAME) != 0) + { + errno = ENOENT; + return -1; + } + memset(stat, 0, sizeof(*stat)); + stat->st_size = E2END; + return 0; +} + +int _fstat_r(struct _reent *reent, int fd, struct stat *stat) +{ + FsStatic::FileInfo *finfo = FsStatic::get_file(fd); + if (!finfo) + { + return -1; + } + memset(stat, 0, sizeof(*stat)); + stat->st_size = E2END; + return 0; +} + +_off_t _lseek_r(struct _reent *reent, int fd, _off_t offset, int whence) +{ + FsStatic::FileInfo *finfo = FsStatic::get_file(fd); + if (!finfo) + { + return -1; + } + off_t new_offset = finfo->offset; + switch (whence) + { + case SEEK_SET: + new_offset = offset; + break; + case SEEK_CUR: + new_offset += offset; + break; + case SEEK_END: + new_offset = E2END + offset; + break; + default: + new_offset = E2END + 1; + } + if (new_offset > E2END) + { + errno = EINVAL; + return -1; + } + finfo->offset = new_offset; + return new_offset; +} + +} // extern "C" + +#endif // _FREERTOS_DRIVERS_ARDUINO_ARDUINOFS_HXX_ diff --git a/src/freertos_drivers/common/Device.cxx b/src/freertos_drivers/common/Device.cxx index 5530fdab9..8b0347945 100644 --- a/src/freertos_drivers/common/Device.cxx +++ b/src/freertos_drivers/common/Device.cxx @@ -144,9 +144,11 @@ int Device::close(struct _reent *reent, int fd) // stdin, stdout, and stderr never get closed return 0; } + files[fd].inshdn = true; int result = f->dev->close(f); if (result < 0) { + files[fd].inshdn = false; errno = -result; return -1; } diff --git a/src/freertos_drivers/common/DeviceBuffer.cxx b/src/freertos_drivers/common/DeviceBuffer.cxx index 3516e65d1..249690619 100644 --- a/src/freertos_drivers/common/DeviceBuffer.cxx +++ b/src/freertos_drivers/common/DeviceBuffer.cxx @@ -34,7 +34,9 @@ #include "DeviceBuffer.hxx" -#ifndef ARDUINO +#include "openmrn_features.h" + +#ifdef OPENMRN_FEATURE_DEVTAB #include @@ -61,4 +63,4 @@ void DeviceBufferBase::block_until_condition(File *file, bool read) ::select(fd + 1, read ? &fds : NULL, read ? NULL : &fds, NULL, NULL); } -#endif +#endif // OPENMRN_FEATURE_DEVTAB diff --git a/src/freertos_drivers/common/DeviceBuffer.hxx b/src/freertos_drivers/common/DeviceBuffer.hxx index 7d4fe8b74..ac7d70688 100644 --- a/src/freertos_drivers/common/DeviceBuffer.hxx +++ b/src/freertos_drivers/common/DeviceBuffer.hxx @@ -39,32 +39,34 @@ #include #include #include + +#include "openmrn_features.h" #include "utils/macros.h" -#ifndef ARDUINO +#ifdef OPENMRN_FEATURE_DEVTAB #include "Devtab.hxx" -#endif +#endif // OPENMRN_FEATURE_DEVTAB /** Helper for DeviceBuffer which allows for methods to not be inlined. */ class DeviceBufferBase { public: -#ifndef ARDUINO +#ifdef OPENMRN_FEATURE_DEVTAB /** Wait for blocking condition to become true. * @param file file to wait on * @param read true if this is a read operation, false for write operation */ static void block_until_condition(File *file, bool read); -#endif +#endif // OPENMRN_FEATURE_DEVTAB /** Signal the wakeup condition. This will also wakeup select. */ void signal_condition() { -#ifndef ARDUINO +#ifdef OPENMRN_FEATURE_DEVTAB Device::select_wakeup(&selectInfo); -#endif +#endif // OPENMRN_FEATURE_DEVTAB } /** Signal the wakeup condition from an ISR context. This will also @@ -72,10 +74,10 @@ public: */ void signal_condition_from_isr() { -#ifndef ARDUINO +#ifdef OPENMRN_FEATURE_DEVTAB int woken = 0; Device::select_wakeup_from_isr(&selectInfo, &woken); -#endif +#endif // OPENMRN_FEATURE_DEVTAB } /** flush all the data out of the buffer and reset the buffer. It is @@ -110,9 +112,9 @@ public: */ void select_insert() { -#ifndef ARDUINO +#ifdef OPENMRN_FEATURE_DEVTAB return Device::select_insert(&selectInfo); -#endif +#endif // OPENMRN_FEATURE_DEVTAB } /** Remove a number of items from the buffer by advancing the readIndex. @@ -180,10 +182,10 @@ protected: { } -#ifndef ARDUINO +#ifdef OPENMRN_FEATURE_DEVTAB /** Metadata for select() logic */ Device::SelectInfo selectInfo; -#endif +#endif // OPENMRN_FEATURE_DEVTAB /** level of space required in buffer in order to wakeup, 0 if unused */ uint16_t level; diff --git a/src/freertos_drivers/common/Devtab.hxx b/src/freertos_drivers/common/Devtab.hxx index e86d2e4c6..c807ed58f 100644 --- a/src/freertos_drivers/common/Devtab.hxx +++ b/src/freertos_drivers/common/Devtab.hxx @@ -46,13 +46,6 @@ class FileSystem; class Notifiable; class DeviceBufferBase; -#ifdef TARGET_LPC11Cxx -#define NUM_OPEN_FILES 4 -#else -/// How many concurrently open fd we support. -#define NUM_OPEN_FILES 20 //12 -#endif - /** File information. */ struct File @@ -69,6 +62,7 @@ struct File off_t offset; /**< current offset within file */ int flags; /**< open flags */ uint8_t inuse : 1; /**< true if this is an open fd. */ + uint8_t inshdn : 1; /**< true if this fd is in shutdown. */ uint8_t device : 1; /**< true if this is a device, false if file system */ uint8_t dir : 1; /**< true if this is a directory, else false */ uint8_t dirty : 1; /**< true if this file is dirty and needs flush */ @@ -259,6 +253,10 @@ protected: */ static int fd_lookup(File *file); + /** @return the maximum number of open file descriptors possible (the size + * of the files[] array. */ + static const unsigned int numOpenFiles; + /** File descriptor pool */ static File files[]; diff --git a/src/freertos_drivers/common/Fileio.cxx b/src/freertos_drivers/common/Fileio.cxx index 9120abf62..d6c64b692 100644 --- a/src/freertos_drivers/common/Fileio.cxx +++ b/src/freertos_drivers/common/Fileio.cxx @@ -39,20 +39,19 @@ #include #include - OSMutex FileIO::mutex; -File FileIO::files[NUM_OPEN_FILES]; /** Allocate a free file descriptor. * @return file number on success, else -1 on failure */ int FileIO::fd_alloc(void) { - for (unsigned int i = 0; i < NUM_OPEN_FILES; i++) + for (unsigned int i = 0; i < numOpenFiles; i++) { if (files[i].inuse == false) { files[i].inuse = true; + files[i].inshdn = false; files[i].device = true; files[i].dir = false; files[i].dirty = false; @@ -82,12 +81,12 @@ void FileIO::fd_free(int fd) */ File* FileIO::file_lookup(int fd) { - if (fd < 0 || fd >= NUM_OPEN_FILES) + if (fd < 0 || fd >= (int)numOpenFiles) { errno = EBADF; return nullptr; } - if (files[fd].inuse == 0) + if (files[fd].inuse == 0 || files[fd].inshdn == 1) { errno = EBADF; return nullptr; @@ -101,7 +100,7 @@ File* FileIO::file_lookup(int fd) */ int FileIO::fd_lookup(File *file) { - HASSERT(file >= files && file <= (files + NUM_OPEN_FILES) && file->inuse); + HASSERT(file >= files && file <= (files + numOpenFiles) && file->inuse); return (file - files); } diff --git a/src/freertos_drivers/common/FileioWeak.cxx b/src/freertos_drivers/common/FileioWeak.cxx new file mode 100644 index 000000000..0976075fd --- /dev/null +++ b/src/freertos_drivers/common/FileioWeak.cxx @@ -0,0 +1,41 @@ +/** \copyright + * Copyright (c) 2020, Balazs Racz + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * \file FileioWeak.cxx + * + * Weak definitions for FileIO. These can be overridden by client applications. + * + * @author Balazs Racz + * @date 18 September 2020 + */ + +#include "Devtab.hxx" + +// Override both of these symbols in a .cxx file in your application (without +// the weak attribute) if you want to change the nuber of open files. + +const unsigned int __attribute__((weak)) FileIO::numOpenFiles = 20; +File __attribute__((weak)) FileIO::files[FileIO::numOpenFiles]; diff --git a/src/freertos_drivers/common/RailcomDriver.hxx b/src/freertos_drivers/common/RailcomDriver.hxx index fbda26b6b..c09bbec3a 100644 --- a/src/freertos_drivers/common/RailcomDriver.hxx +++ b/src/freertos_drivers/common/RailcomDriver.hxx @@ -58,6 +58,9 @@ public: /** Instructs the driver that the railcom cutout is over now. The driver * will use this information to disable the UART receiver. */ virtual void end_cutout() = 0; + /** Called instead of start/mid/end-cutout at the end of the current packet + * if there was no cutout requested. */ + virtual void no_cutout() = 0; /** Specifies the feedback key to write into the received railcom data * packets. This feedback key is used by the application layer to correlate * the stream of DCC packets to the stream of Railcom packets. This method @@ -74,6 +77,7 @@ class NoRailcomDriver : public RailcomDriver { void start_cutout() OVERRIDE {} void middle_cutout() OVERRIDE {} void end_cutout() OVERRIDE {} + void no_cutout() OVERRIDE {} void set_feedback_key(uint32_t key) OVERRIDE {} }; diff --git a/src/freertos_drivers/common/WifiDefs.hxx b/src/freertos_drivers/common/WifiDefs.hxx index 751bc5071..3005737a2 100644 --- a/src/freertos_drivers/common/WifiDefs.hxx +++ b/src/freertos_drivers/common/WifiDefs.hxx @@ -32,9 +32,10 @@ enum class WlanState : uint8_t */ enum class WlanRole : uint8_t { - UNKNOWN = 0, /**< Wi-Fi station mode */ - STA, /**< Wi-Fi station mode */ - AP /**< Wi-Fi access point mode */ + UNKNOWN = 0, /**< Default mode (from stored configuration) */ + DEFAULT_ROLE = UNKNOWN, /**< Default mode (from stored configuration) */ + STA, /**< Wi-Fi station mode */ + AP /**< Wi-Fi access point mode */ }; enum class CountryCode : uint8_t diff --git a/src/freertos_drivers/esp32/Esp32WiFiManager.cxx b/src/freertos_drivers/esp32/Esp32WiFiManager.cxx index a7c36cc14..1381f5702 100644 --- a/src/freertos_drivers/esp32/Esp32WiFiManager.cxx +++ b/src/freertos_drivers/esp32/Esp32WiFiManager.cxx @@ -48,17 +48,42 @@ #include #include -// ESP-IDF v4+ has a slightly different directory structure to previous -// versions. -#ifdef ESP_IDF_VERSION_MAJOR -// ESP-IDF v4+ +// Starting in ESP-IDF v4.0 a few header files have been relocated so we need +// to adjust the include paths accordingly. If the __has_include preprocessor +// directive is defined we can use it to find the appropriate header files. +// If it is not usable then we will default the older header filenames. +#if defined(__has_include) + +// rom/crc.h was relocated to esp32/rom/crc.h in ESP-IDF v4.0 +// TODO: This will need to be platform specific in IDF v4.1 since this is +// exposed in unique header paths for each supported platform. Detecting the +// operating platform (ESP32, ESP32-S2, ESP32-S3, etc) can be done by checking +// for the presence of one of the following defines: +// CONFIG_IDF_TARGET_ESP32 -- ESP32 +// CONFIG_IDF_TARGET_ESP32S2 -- ESP32-S2 +// CONFIG_IDF_TARGET_ESP32S3 -- ESP32-S3 +// If none of these are defined it means the ESP-IDF version is v4.0 or +// earlier. +#if __has_include("esp32/rom/crc.h") #include +#else +#include +#endif + +// esp_wifi_internal.h was relocated to esp_private/wifi.h in ESP-IDF v4.0 +#if __has_include("esp_private/wifi.h") #include #else -// ESP-IDF v3.x +#include +#endif + +#else + +// We are unable to use __has_include, default to the old include paths. #include #include -#endif // ESP_IDF_VERSION_MAJOR + +#endif // defined __has_include using openlcb::NodeID; using openlcb::SimpleCanStack; diff --git a/src/freertos_drivers/net_cc32xx/CC32xxSocket.cxx b/src/freertos_drivers/net_cc32xx/CC32xxSocket.cxx index 276b3cd31..51ae25256 100644 --- a/src/freertos_drivers/net_cc32xx/CC32xxSocket.cxx +++ b/src/freertos_drivers/net_cc32xx/CC32xxSocket.cxx @@ -755,8 +755,8 @@ int CC32xxSocket::close(File *file) portENTER_CRITICAL(); remove_instance_from_sd(sd); portEXIT_CRITICAL(); - delete this; sl_Close(sd); + delete this; } else { diff --git a/src/freertos_drivers/net_cc32xx/CC32xxWiFi.cxx b/src/freertos_drivers/net_cc32xx/CC32xxWiFi.cxx index fe54b2018..bb5dcb399 100644 --- a/src/freertos_drivers/net_cc32xx/CC32xxWiFi.cxx +++ b/src/freertos_drivers/net_cc32xx/CC32xxWiFi.cxx @@ -565,6 +565,14 @@ void CC32xxWiFi::wlan_mac(uint8_t mac[6]) sl_NetCfgGet(SL_NETCFG_MAC_ADDRESS_GET, nullptr, &len, mac); } +/* + * CC32xxWiFi::wlan_set_mac() + */ +void CC32xxWiFi::wlan_set_mac(uint8_t mac[6]) +{ + sl_NetCfgSet(SL_NETCFG_MAC_ADDRESS_SET, 1, 6, mac); +} + /* * CC32xxWiFi::test_mode_start() */ @@ -765,13 +773,14 @@ void CC32xxWiFi::wlan_setup_ap(const char *ssid, const char *security_key, (uint8_t*)ssid); if (wlanRole == WlanRole::AP) { - strncpy(this->ssid, ssid, sizeof(this->ssid)); + str_populate(this->ssid, ssid); } sl_WlanSet(SL_WLAN_CFG_AP_ID, SL_WLAN_AP_OPT_SECURITY_TYPE, 1, (uint8_t*)&sec_type); - if (sec_type == SL_WLAN_SEC_TYPE_OPEN) + if (sec_type == SL_WLAN_SEC_TYPE_OPEN || + security_key == nullptr) { return; } @@ -780,6 +789,46 @@ void CC32xxWiFi::wlan_setup_ap(const char *ssid, const char *security_key, strlen(security_key), (uint8_t*)security_key); } +/* + * CC32xxWiFi::wlan_get_ap_config() + */ +void CC32xxWiFi::wlan_get_ap_config(string *ssid, SecurityType *security_type) +{ + if (ssid) + { + // Reads AP SSID configuration from NWP. + ssid->clear(); + ssid->resize(33); + uint16_t len = ssid->size(); + uint16_t config_opt = SL_WLAN_AP_OPT_SSID; + sl_WlanGet(SL_WLAN_CFG_AP_ID, &config_opt, &len, (_u8*) &(*ssid)[0]); + ssid->resize(len); + } + if (security_type) + { + uint16_t len = sizeof(*security_type); + uint16_t config_opt = SL_WLAN_AP_OPT_SECURITY_TYPE; + sl_WlanGet(SL_WLAN_CFG_AP_ID, &config_opt, &len, (_u8*) security_type); + } +} + +int CC32xxWiFi::wlan_get_ap_station_count() +{ + if (wlanRole != WlanRole::AP) + { + return 0; + } + uint8_t num_connected = 0; + uint16_t len = sizeof(num_connected); + auto status = sl_NetCfgGet( + SL_NETCFG_AP_STATIONS_NUM_CONNECTED, NULL, &len, &num_connected); + if (status) + { + return -1; + } + return num_connected; +} + void CC32xxWiFi::connecting_update_blinker() { if (!connected) @@ -805,8 +854,10 @@ void CC32xxWiFi::set_default_state() } SlCheckError(result); - if (wlanRole == WlanRole::AP) + if (wlanRole == WlanRole::AP || + (wlanRole == WlanRole::UNKNOWN && result == ROLE_AP)) { + wlanRole = WlanRole::AP; if (result != ROLE_AP) { sl_WlanSetMode(ROLE_AP); @@ -821,6 +872,7 @@ void CC32xxWiFi::set_default_state() } else { + wlanRole = WlanRole::STA; if (wlan_profile_test_none()) { /* no profiles saved, add the default profile */ @@ -845,6 +897,24 @@ void CC32xxWiFi::set_default_state() started = true; } +/* + * CC32xxWiFi::wlan_set_role() + */ +void CC32xxWiFi::wlan_set_role(WlanRole new_role) +{ + switch (new_role) + { + case WlanRole::STA: + sl_WlanSetMode(ROLE_STA); + break; + case WlanRole::AP: + sl_WlanSetMode(ROLE_AP); + break; + default: + DIE("Unsupported wlan role"); + } +} + /* * CC32xxWiFi::wlan_task() */ @@ -1192,8 +1262,6 @@ void CC32xxWiFi::net_app_event_handler(NetAppEvent *event) // event_data = &event->EventData.ipLeased; // - SlIpLeasedAsync_t *ip_leased = &event->Data.IpLeased; - ipAddress = ip_leased->IpAddress; break; } case SL_NETAPP_EVENT_IP_COLLISION: diff --git a/src/freertos_drivers/net_cc32xx/CC32xxWiFi.hxx b/src/freertos_drivers/net_cc32xx/CC32xxWiFi.hxx index 54267d73b..52abc7db3 100644 --- a/src/freertos_drivers/net_cc32xx/CC32xxWiFi.hxx +++ b/src/freertos_drivers/net_cc32xx/CC32xxWiFi.hxx @@ -44,6 +44,7 @@ #include "freertos_drivers/common/WifiDefs.hxx" class CC32xxSocket; +class NetworkSpace; /** Interface that aids in unit testing. */ @@ -211,6 +212,18 @@ public: void wlan_setup_ap(const char *ssid, const char *security_key, SecurityType security_type) override; + /** Retrieve current AP config. + * @param ssid will be filled with the SSID of the AP + * @param security_type will be filled with the security type + */ + void wlan_get_ap_config(string *ssid, SecurityType *security_type); + + /** Retrieves how many stations are connected to the wifi in AP mode. + * @return number of connected stations (0 to 4). If not in AP mode, + * returns 0. + */ + int wlan_get_ap_station_count(); + /** @return true if the wlan interface is ready to establish outgoing * connections. */ bool wlan_ready() @@ -226,6 +239,13 @@ public: return wlanRole; } + /** Change the default Wlan Role. This will be used in the next start(...) + * if the UNKNOWN role is specified. The new setting takes effect when the + * device is restarted (either via reboot or stop + start). + * @param role new role. Must not be UNKNOWN + */ + void wlan_set_role(WlanRole new_role); + /** @return 0 if !wlan_ready, else a debugging status code. */ WlanState wlan_startup_state() { @@ -359,6 +379,15 @@ public: */ void wlan_mac(uint8_t mac[6]); + /** Sets the device MAC address. WARNING. The MAC address will be + * persistently set to the value indicated. Only a factory reset of the + * device can undo this operation. After calling this API there is no way + * to recover the factory MAC address. Make sure not to call this API too + * many times in the lifetime of the product, as flash wear is a concern. + * @param mac 6 byte array which holds the desired MAC address. + */ + void wlan_set_mac(uint8_t mac[6]); + /** Get the assigned IP address. * @return assigned IP address, else 0 if not assigned */ @@ -481,24 +510,26 @@ public: static std::string get_version(); private: + friend class ::NetworkSpace; + /** Translates the SecurityType enum to the internal SimpleLink code. * @param sec_type security type * @return simplelink security type */ - uint8_t security_type_to_simplelink(SecurityType sec_type); + static uint8_t security_type_to_simplelink(SecurityType sec_type); /** Translates the SimpleLink code to SecurityType enum. * @param sec_type simplelink security type * @return security type */ - SecurityType security_type_from_simplelink(uint8_t sec_type); + static SecurityType security_type_from_simplelink(uint8_t sec_type); /** Translates the SimpleLink code from the network scan to SecurityType * enum. * @param sec_type simplelink network scan security result * @return security type */ - SecurityType security_type_from_scan(unsigned sec_type); + static SecurityType security_type_from_scan(unsigned sec_type); /** Set the CC32xx to its default state, including station mode. */ diff --git a/src/freertos_drivers/sources b/src/freertos_drivers/sources index 1851ce7e1..a7e08f519 100644 --- a/src/freertos_drivers/sources +++ b/src/freertos_drivers/sources @@ -4,6 +4,7 @@ VPATH := $(SRCDIR): \ CSRCS += CXXSRCS += Fileio.cxx \ + FileioWeak.cxx \ Device.cxx \ FileSystem.cxx \ DeviceBuffer.cxx \ diff --git a/src/freertos_drivers/spiffs/SPIFFS.hxx b/src/freertos_drivers/spiffs/SPIFFS.hxx index ce7035fb4..85f5d2b8e 100644 --- a/src/freertos_drivers/spiffs/SPIFFS.hxx +++ b/src/freertos_drivers/spiffs/SPIFFS.hxx @@ -101,7 +101,7 @@ public: void flush_cache() { mutex.lock(); - for (unsigned int i = 0; i < NUM_OPEN_FILES; i++) + for (unsigned int i = 0; i < numOpenFiles; i++) { if (files[i].inuse && files[i].dev == this && files[i].dirty) { diff --git a/src/freertos_drivers/ti/TivaDCC.hxx b/src/freertos_drivers/ti/TivaDCC.hxx index 997c19634..35d5a62eb 100644 --- a/src/freertos_drivers/ti/TivaDCC.hxx +++ b/src/freertos_drivers/ti/TivaDCC.hxx @@ -571,6 +571,7 @@ inline void TivaDCC::interrupt_handler() } else { + railcomDriver_->no_cutout(); current_bit = DCC_ONE; state_ = DCC_LEADOUT; } @@ -724,6 +725,8 @@ inline void TivaDCC::interrupt_handler() } break; case MM_LEADOUT: + // MM packets never have a cutout. + railcomDriver_->no_cutout(); current_bit = MM_PREAMBLE; if (++preamble_count >= 2) { get_next_packet = true; diff --git a/src/freertos_drivers/ti/TivaRailcom.hxx b/src/freertos_drivers/ti/TivaRailcom.hxx index 19657bcef..ecabfde77 100644 --- a/src/freertos_drivers/ti/TivaRailcom.hxx +++ b/src/freertos_drivers/ti/TivaRailcom.hxx @@ -394,12 +394,14 @@ private: } HWREG(HW::UART_BASE[i] + UART_O_CTL) |= UART_CTL_RXE; } + HW::middle_cutout_hook(); Debug::RailcomDriverCutout::set(true); } void end_cutout() OVERRIDE { HW::disable_measurement(); + bool have_packets = false; for (unsigned i = 0; i < ARRAYSIZE(HW::UART_BASE); ++i) { while (MAP_UARTCharsAvail(HW::UART_BASE[i])) @@ -430,15 +432,46 @@ private: Debug::RailcomRxActivate::set(false); //HWREGBITW(HW::UART_BASE[i] + UART_O_CTL, UART_CTL_RXE) = 0; if (returnedPackets_[i]) { + have_packets = true; this->feedbackQueue_.commit_back(); Debug::RailcomPackets::toggle(); returnedPackets_[i] = nullptr; MAP_IntPendSet(HW::OS_INTERRUPT); } } + if (!have_packets) + { + // Ensures that at least one feedback packet is sent back even when + // it is with no railcom payload. + auto *p = this->alloc_new_packet(0); + if (p) + { + this->feedbackQueue_.commit_back(); + Debug::RailcomPackets::toggle(); + MAP_IntPendSet(HW::OS_INTERRUPT); + } + } Debug::RailcomCh2Data::set(false); Debug::RailcomDriverCutout::set(false); } + + void no_cutout() OVERRIDE + { + for (unsigned i = 0; i < ARRAYSIZE(HW::UART_BASE); ++i) + { + if (!returnedPackets_[i]) + { + returnedPackets_[i] = this->alloc_new_packet(i); + } + if (returnedPackets_[i]) + { + this->feedbackQueue_.commit_back(); + Debug::RailcomPackets::toggle(); + returnedPackets_[i] = nullptr; + MAP_IntPendSet(HW::OS_INTERRUPT); + } + } + } }; #endif // _FREERTOS_DRIVERS_TI_TIVARAILCOM_HXX_ diff --git a/src/openlcb/AliasAllocator.cxx b/src/openlcb/AliasAllocator.cxx index 17f386545..e81f91f61 100644 --- a/src/openlcb/AliasAllocator.cxx +++ b/src/openlcb/AliasAllocator.cxx @@ -33,6 +33,7 @@ */ #include "openlcb/AliasAllocator.hxx" +#include "nmranet_config.h" #include "openlcb/CanDefs.hxx" namespace openlcb @@ -47,6 +48,7 @@ AliasAllocator::AliasAllocator(NodeID if_id, IfCan *if_can) , if_id_(if_id) , cid_frame_sequence_(0) , conflict_detected_(0) + , reserveUnusedAliases_(config_reserve_unused_alias_count()) { reinit_seed(); // Moves all the allocated alias buffers over to the input queue for @@ -77,6 +79,50 @@ void seed_alias_allocator(AliasAllocator* aliases, Pool* pool, int n) { } } +/** @return the number of aliases that are reserved and available for new + * virtual nodes to use. */ +unsigned AliasAllocator::num_reserved_aliases() +{ + unsigned cnt = 0; + NodeID found_id = CanDefs::get_reserved_alias_node_id(0); + NodeAlias found_alias = 0; + do + { + if (if_can()->local_aliases()->next_entry( + found_id, &found_id, &found_alias) && + CanDefs::is_reserved_alias_node_id(found_id)) + { + ++cnt; + } + else + { + break; + } + } while (true); + return cnt; +} + +/** Removes all aliases that are reserved but not yet used. */ +void AliasAllocator::clear_reserved_aliases() +{ + do + { + NodeID found_id = CanDefs::get_reserved_alias_node_id(0); + NodeAlias found_alias = 0; + if (if_can()->local_aliases()->next_entry( + CanDefs::get_reserved_alias_node_id(0), &found_id, + &found_alias) && + CanDefs::is_reserved_alias_node_id(found_id)) + { + if_can()->local_aliases()->remove(found_alias); + } + else + { + break; + } + } while (true); +} + void AliasAllocator::return_alias(NodeID id, NodeAlias alias) { // This is synchronous allocation, which is not nice. @@ -89,15 +135,68 @@ void AliasAllocator::return_alias(NodeID id, NodeAlias alias) if_can()->frame_write_flow()->send(b); } - // This is synchronous allocation, which is not nice. + add_allocated_alias(alias); +} + +void AliasAllocator::add_allocated_alias(NodeAlias alias) +{ + // Note: We leak aliases here in case of eviction by the AliasCache + // object. This is okay for two reasons: 1) Generally the local alias cache + // size should be about equal to the local nodes count. 2) OpenLCB alias + // allocation algorithm is able to reuse aliases that were allocated by + // nodes that are not on the network anymore. + if_can()->local_aliases()->add( + CanDefs::get_reserved_alias_node_id(alias), alias); + if (!waitingClients_.empty()) { - auto* b = alloc(); - b->data()->reset(); - b->data()->alias = alias; + // Wakes up exactly one executable that is waiting for an alias. + Executable *w = static_cast(waitingClients_.next().item); + // This schedules a state flow onto its executor. + w->alloc_result(nullptr); + } +} + +NodeAlias AliasAllocator::get_allocated_alias( + NodeID destination_id, Executable *done) +{ + NodeID found_id; + NodeAlias found_alias = 0; + bool allocate_new = false; + bool found = if_can()->local_aliases()->next_entry( + CanDefs::get_reserved_alias_node_id(0), &found_id, &found_alias); + if (found) + { + found = (found_id == CanDefs::get_reserved_alias_node_id(found_alias)); + } + if (found) + { + if_can()->local_aliases()->add(destination_id, found_alias); + if (reserveUnusedAliases_) + { + NodeID next_id; + NodeAlias next_alias = 0; + if (!if_can()->local_aliases()->next_entry( + CanDefs::get_reserved_alias_node_id(0), &next_id, + &next_alias) || + !CanDefs::is_reserved_alias_node_id(next_id)) + { + allocate_new = true; + } + } + } + else + { + found_alias = 0; + allocate_new = true; + waitingClients_.insert(done); + } + if (allocate_new) + { + Buffer *b = alloc(); b->data()->do_not_reallocate(); - b->data()->state = AliasInfo::STATE_RESERVED; - reserved_aliases()->insert(b); + this->send(b); } + return found_alias; } AliasAllocator::~AliasAllocator() @@ -111,9 +210,7 @@ StateFlowBase::Action AliasAllocator::entry() HASSERT(pending_alias()->state == AliasInfo::STATE_EMPTY); while (!pending_alias()->alias) { - pending_alias()->alias = seed_; - next_seed(); - // TODO(balazs.racz): check if the alias is already known about. + pending_alias()->alias = get_new_seed(); } // Registers ourselves as a handler for incoming CAN frames to detect // conflicts. @@ -124,6 +221,27 @@ StateFlowBase::Action AliasAllocator::entry() return call_immediately(STATE(handle_allocate_for_cid_frame)); } +NodeAlias AliasAllocator::get_new_seed() +{ + while (true) + { + NodeAlias ret = seed_; + next_seed(); + LOG(VERBOSE, "(%p) alias test seed is %03X (next %03X)", this, ret, + seed_); + if (if_can()->local_aliases()->lookup(ret)) + { + continue; + } + if (if_can()->remote_aliases()->lookup(ret)) + { + continue; + } + LOG(VERBOSE, "alias get seed is %03X (next %03X)", ret, seed_); + return ret; + } +} + void AliasAllocator::next_seed() { uint16_t offset; @@ -216,9 +334,7 @@ StateFlowBase::Action AliasAllocator::send_rid_frame() pending_alias()->state = AliasInfo::STATE_RESERVED; if_can()->frame_dispatcher()->unregister_handler( &conflictHandler_, pending_alias()->alias, ~0x1FFFF000U); - if_can()->local_aliases()->add(AliasCache::RESERVED_ALIAS_NODE_ID, - pending_alias()->alias); - reserved_alias_pool_.insert(transfer_message()); + add_allocated_alias(pending_alias()->alias); return release_and_exit(); } @@ -241,26 +357,19 @@ void AliasAllocator::ConflictHandler::send(Buffer *message, message->unref(); } +#ifdef GTEST + void AliasAllocator::TEST_finish_pending_allocation() { if (is_state(STATE(wait_done))) { timer_.trigger(); } } -void AliasAllocator::TEST_add_allocated_alias(NodeAlias alias, bool repeat) +void AliasAllocator::TEST_add_allocated_alias(NodeAlias alias) { - Buffer *a; - mainBufferPool->alloc(&a); - a->data()->reset(); - a->data()->alias = alias; - a->data()->state = AliasInfo::STATE_RESERVED; - if (!repeat) - { - a->data()->do_not_reallocate(); - } - if_can()->local_aliases()->add( - AliasCache::RESERVED_ALIAS_NODE_ID, a->data()->alias); - reserved_aliases()->insert(a); + add_allocated_alias(alias); } +#endif + } // namespace openlcb diff --git a/src/openlcb/AliasAllocator.cxxtest b/src/openlcb/AliasAllocator.cxxtest index faef26b0d..4813a442c 100644 --- a/src/openlcb/AliasAllocator.cxxtest +++ b/src/openlcb/AliasAllocator.cxxtest @@ -1,8 +1,10 @@ #include -#include "utils/async_if_test_helper.hxx" #include "openlcb/AliasAllocator.hxx" #include "openlcb/AliasCache.hxx" +#include "openlcb/BulkAliasAllocator.hxx" +#include "openlcb/CanDefs.hxx" +#include "utils/async_if_test_helper.hxx" namespace openlcb { @@ -12,6 +14,7 @@ protected: AsyncAliasAllocatorTest() : b_(nullptr) , alias_allocator_(TEST_NODE_ID, ifCan_.get()) + , bulkAllocator_(create_bulk_alias_allocator(ifCan_.get())) { } @@ -30,7 +33,9 @@ protected: unsigned next_seed(AliasAllocator *alloc = nullptr) { if (!alloc) + { alloc = &alias_allocator_; + } alloc->next_seed(); return alloc->seed_; } @@ -39,16 +44,100 @@ protected: * until one is available. The alias will be saved into the buffer b_. */ void get_next_alias() { - while (b_ = static_cast *>( - alias_allocator_.reserved_aliases()->next().item), - !b_) + NodeAlias a; + nextAliasNodeId_++; + do + { + run_x([this, &a]() { + a = alias_allocator_.get_allocated_alias( + nextAliasNodeId_, &ex_); + }); + if (!a) + { + n_.wait_for_notification(); + } + } while (a == 0); + b_ = alias_allocator_.alloc(); + b_->data()->alias = a; + b_->data()->state = AliasInfo::STATE_RESERVED; + } + + /// Pre-generates some aliases into a vector. + void generate_aliases(AliasAllocator *alloc, unsigned count) + { + set_seed(0x555, alloc); + run_x([this, count, alloc]() { + for (unsigned i = 0; i < count; i++) + { + auto a = alloc->get_new_seed(); + LOG(INFO, "alias %03X", a); + aliases_.push_back(a); + } + }); + set_seed(0x555, alloc); + } + + /// Expects that CID frames are sent to the bus. + /// @param begin iterator into alias array + /// @param end iterator (end) into alias array + template void expect_cid(It begin, It end) + { + for (auto it = begin; it != end; ++it) + { + NodeAlias a = *it; + string msg = StringPrintf("cid %03X", a); + LOG(INFO, "cid %03X", a); + expect_packet(StringPrintf(":X17020%03XN;", a)) + .RetiresOnSaturation(); + expect_packet(StringPrintf(":X1610D%03XN;", a)) + .RetiresOnSaturation(); + expect_packet(StringPrintf(":X15000%03XN;", a)) + .RetiresOnSaturation(); + expect_packet(StringPrintf(":X14003%03XN;", a)) + .RetiresOnSaturation(); + } + } + + /// Expects that RID frames are sent to the bus. + /// @param begin iterator into alias array + /// @param end iterator (end) into alias array + template void expect_rid(It begin, It end) + { + for (auto it = begin; it != end; ++it) { - usleep(10); + NodeAlias a = *it; + LOG(INFO, "rid %03X", a); + expect_packet(StringPrintf(":X10700%03XN;", a)) + .RetiresOnSaturation(); } } + /// Helper class to pass into the asynchronous alias allocation wait. Will + /// notify n_ when the alias is ready to be taken. + class AllocExecutable : public Executable + { + public: + AllocExecutable(AsyncAliasAllocatorTest *parent) + : parent_(parent) + { + } + void run() override + { + } + void alloc_result(QMember *item) override + { + parent_->n_.notify(); + } + AsyncAliasAllocatorTest *parent_; + } ex_ {this}; + friend class AllocExecutable; + Buffer *b_; AliasAllocator alias_allocator_; + std::unique_ptr bulkAllocator_; + std::vector aliases_; + /// Will use this node ID to mark the next alias gotten. + openlcb::NodeID nextAliasNodeId_ = TEST_NODE_ID + 1; }; TEST_F(AsyncAliasAllocatorTest, SetupTeardown) @@ -63,21 +152,75 @@ TEST_F(AsyncAliasAllocatorTest, AllocateOne) expect_packet(":X1610D555N;"); expect_packet(":X15000555N;"); expect_packet(":X14003555N;"); - - mainBufferPool->alloc(&b_); - alias_allocator_.send(b_); - b_ = nullptr; - - /** @todo(balazs.racz) this should be after the wait because there should be - * a delay before sending it. */ expect_packet(":X10700555N;"); - wait(); get_next_alias(); ASSERT_TRUE(b_); EXPECT_EQ(0x555U, b_->data()->alias); EXPECT_EQ(AliasInfo::STATE_RESERVED, b_->data()->state); } + +TEST_F(AsyncAliasAllocatorTest, ReserveThenAllocate) +{ + set_seed(0x555); + clear_expect(true); + mainBufferPool->alloc(&b_); + expect_packet(":X17020555N;"); + expect_packet(":X1610D555N;"); + expect_packet(":X15000555N;"); + expect_packet(":X14003555N;"); + alias_allocator_.send(b_); + wait(); + expect_packet(":X10700555N;"); + usleep(250000); + wait(); + clear_expect(true); + run_x([this]() { + EXPECT_EQ(0x555, + alias_allocator_.get_allocated_alias(nextAliasNodeId_, nullptr)); + EXPECT_EQ(nextAliasNodeId_, + ifCan_->local_aliases()->lookup(NodeAlias(0x555))); + }); +} + +TEST_F(AsyncAliasAllocatorTest, ReserveUnused) +{ + alias_allocator_.TEST_set_reserve_unused_alias_count(1); + set_seed(0x555); + clear_expect(true); + expect_packet(":X17020555N;"); + expect_packet(":X1610D555N;"); + expect_packet(":X15000555N;"); + expect_packet(":X14003555N;"); + run_x([this]() { + EXPECT_EQ( + 0, alias_allocator_.get_allocated_alias(nextAliasNodeId_, &ex_)); + }); + wait(); + clear_expect(true); + expect_packet(":X10700555N;"); + usleep(250000); + clear_expect(true); + expect_packet(":X17020AAAN;"); + expect_packet(":X1610DAAAN;"); + expect_packet(":X15000AAAN;"); + expect_packet(":X14003AAAN;"); + set_seed(0xAAA); + run_x([this]() { + EXPECT_EQ(0x555, + alias_allocator_.get_allocated_alias(nextAliasNodeId_, &ex_)); + }); + wait(); + clear_expect(true); + expect_packet(":X10700AAAN;"); + usleep(250000); + run_x([this]() { + // This one should be marked as reserved. + EXPECT_EQ(CanDefs::get_reserved_alias_node_id(0xAAA), + ifCan_->local_aliases()->lookup(NodeAlias(0xAAA))); + }); +} + #if 0 TEST_F(AsyncAliasAllocatorTest, TestDelay) { @@ -98,13 +241,11 @@ TEST_F(AsyncAliasAllocatorTest, TestDelay) TEST_F(AsyncAliasAllocatorTest, AllocateMultiple) { set_seed(0x555); - mainBufferPool->alloc(&b_); expect_packet(":X17020555N;"); expect_packet(":X1610D555N;"); expect_packet(":X15000555N;"); expect_packet(":X14003555N;"); expect_packet(":X10700555N;"); - alias_allocator_.send(b_); b_ = nullptr; get_next_alias(); EXPECT_EQ(0x555U, b_->data()->alias); @@ -117,15 +258,10 @@ TEST_F(AsyncAliasAllocatorTest, AllocateMultiple) expect_packet(":X14003AAAN;"); expect_packet(":X10700AAAN;"); - mainBufferPool->alloc(&b_); - alias_allocator_.send(b_); - b_ = nullptr; - /* Conflicts with the previous alias to be tested. That's not a problem at * this point however, because that alias has already left the * allocator. */ - send_packet( - ":X10700555N;"); + send_packet(":X10700555N;"); get_next_alias(); EXPECT_EQ(0xAAAU, b_->data()->alias); @@ -134,6 +270,7 @@ TEST_F(AsyncAliasAllocatorTest, AllocateMultiple) TEST_F(AsyncAliasAllocatorTest, AllocationConflict) { + clear_expect(true); set_seed(0x555); mainBufferPool->alloc(&b_); expect_packet(":X17020555N;"); @@ -143,6 +280,7 @@ TEST_F(AsyncAliasAllocatorTest, AllocationConflict) alias_allocator_.send(b_); b_ = nullptr; wait(); + clear_expect(true); set_seed(0xAA5); expect_packet(":X17020AA5N;"); expect_packet(":X1610DAA5N;"); @@ -150,21 +288,29 @@ TEST_F(AsyncAliasAllocatorTest, AllocationConflict) expect_packet(":X14003AA5N;"); expect_packet(":X10700AA5N;"); send_packet(":X10700555N;"); + twait(); + clear_expect(true); + run_x([this]() { + // This one should be marked as reserved. + EXPECT_EQ(CanDefs::get_reserved_alias_node_id(0xAA5), + ifCan_->local_aliases()->lookup(NodeAlias(0xAA5))); + // This one should be unknown. + EXPECT_EQ(0U, ifCan_->local_aliases()->lookup(NodeAlias(0x555))); + }); get_next_alias(); EXPECT_EQ(0xAA5U, b_->data()->alias); EXPECT_EQ(AliasInfo::STATE_RESERVED, b_->data()->state); run_x([this]() { - // This one should be marked as reserved. - EXPECT_EQ(AliasCache::RESERVED_ALIAS_NODE_ID, + // This one should be marked for the new node ID. + EXPECT_EQ(nextAliasNodeId_, ifCan_->local_aliases()->lookup(NodeAlias(0xAA5))); - // This one should be unknown. - EXPECT_EQ(0U, ifCan_->local_aliases()->lookup(NodeAlias(0x555))); }); } TEST_F(AsyncAliasAllocatorTest, LateAllocationConflict) { + clear_expect(true); set_seed(0x555); mainBufferPool->alloc(&b_); expect_packet(":X17020555N;"); @@ -174,6 +320,7 @@ TEST_F(AsyncAliasAllocatorTest, LateAllocationConflict) alias_allocator_.send(b_); b_ = nullptr; wait(); + clear_expect(true); set_seed(0xAA5); usleep(100000); expect_packet(":X17020AA5N;"); @@ -182,10 +329,14 @@ TEST_F(AsyncAliasAllocatorTest, LateAllocationConflict) expect_packet(":X14003AA5N;"); send_packet(":X10700555N;"); wait(); + clear_expect(true); usleep(100000); expect_packet(":X10700AA5N;"); send_packet(":X10700555N;"); + twait(); + RX(EXPECT_EQ(1u, ifCan_->alias_allocator()->num_reserved_aliases())); get_next_alias(); + RX(EXPECT_EQ(0u, ifCan_->alias_allocator()->num_reserved_aliases())); EXPECT_EQ(0xAA5U, b_->data()->alias); EXPECT_EQ(AliasInfo::STATE_RESERVED, b_->data()->state); } @@ -217,4 +368,68 @@ TEST_F(AsyncAliasAllocatorTest, DifferentGenerated) // Makes sure 'other' disappears from the executor before destructing it. wait(); } + +TEST_F(AsyncAliasAllocatorTest, BulkFew) +{ + RX(EXPECT_EQ(0u, ifCan_->alias_allocator()->num_reserved_aliases())); + generate_aliases(ifCan_->alias_allocator(), 5); + expect_cid(aliases_.begin(), aliases_.end()); + LOG(INFO, "invoke"); + auto start_time = os_get_time_monotonic(); + auto invocation = invoke_flow_nowait(bulkAllocator_.get(), 5); + wait(); + LOG(INFO, "expect RIDs"); + clear_expect(true); + expect_rid(aliases_.begin(), aliases_.end()); + LOG(INFO, "wait for complete"); + invocation->wait(); + wait(); + clear_expect(true); + auto end_time = os_get_time_monotonic(); + EXPECT_LT(MSEC_TO_NSEC(200), end_time - start_time); + RX({ + EXPECT_EQ(5u, ifCan_->alias_allocator()->num_reserved_aliases()); + ifCan_->alias_allocator()->clear_reserved_aliases(); + EXPECT_EQ(0u, ifCan_->alias_allocator()->num_reserved_aliases()); + }); +} + +TEST_F(AsyncAliasAllocatorTest, BulkConflict) +{ + generate_aliases(ifCan_->alias_allocator(), 7); + clear_expect(true); + expect_cid(aliases_.begin(), aliases_.begin() + 5); + LOG(INFO, "invoke"); + auto invocation = invoke_flow_nowait(bulkAllocator_.get(), 5); + wait(); + LOG(INFO, "send conflicts"); + clear_expect(true); + expect_cid(aliases_.begin() + 5, aliases_.end()); + send_packet(StringPrintf(":X10700%03XN;", aliases_[0])); + send_packet(StringPrintf(":X10700%03XN;", aliases_[1])); + wait(); + usleep(10000); + wait(); + LOG(INFO, "expect RIDs"); + clear_expect(true); + expect_rid(aliases_.begin() + 2, aliases_.end()); + LOG(INFO, "wait for complete"); + invocation->wait(); + clear_expect(true); +} + +TEST_F(AsyncAliasAllocatorTest, BulkMany) +{ + clear_expect(true); + generate_aliases(ifCan_->alias_allocator(), 150); + expect_cid(aliases_.begin(), aliases_.end()); + expect_rid(aliases_.begin(), aliases_.end()); + LOG(INFO, "invoke"); + auto invocation = invoke_flow_nowait(bulkAllocator_.get(), 150); + wait(); + LOG(INFO, "wait for complete"); + invocation->wait(); + clear_expect(true); +} + } // namespace openlcb diff --git a/src/openlcb/AliasAllocator.hxx b/src/openlcb/AliasAllocator.hxx index c39606a6b..81f8b6592 100644 --- a/src/openlcb/AliasAllocator.hxx +++ b/src/openlcb/AliasAllocator.hxx @@ -69,9 +69,9 @@ struct AliasInfo } /** The current alias. This is 0 if the alias needs to be generated. */ - unsigned alias : 12; - unsigned state : 3; - unsigned return_to_reallocation : 1; + uint16_t alias : 12; + uint16_t state : 3; + uint16_t return_to_reallocation : 1; enum State { @@ -108,24 +108,57 @@ public: */ AliasAllocator(NodeID if_id, IfCan *if_can); + /** Destructor */ virtual ~AliasAllocator(); + /** @return the Node ID for the interface. */ + NodeID if_node_id() + { + return if_id_; + } + /** Resets the alias allocator to the state it was at construction. useful * after connection restart in order to ensure it will try to allocate the * same alias. */ void reinit_seed(); - /** "Allocate" a buffer from this pool (but without initialization) in - * order to get a reserved alias. */ - QAsync *reserved_aliases() - { - return &reserved_alias_pool_; - } + /** Returns a new alias to check from the random sequence. Checks that it + * is not in the alias cache yet.*/ + NodeAlias get_new_seed(); + + /** Allocates an alias from the reserved but unused aliases list. If there + * is a free alias there, that alias will be reassigned to destination_id + * in the local alias cache, and done will never be notified. If there is + * no free alias, then a new alias will be allocated, and done will be + * notified when the allocation is complete. Then the call has to be + * re-tried by the destination flow. + * @param destination_id if there is a free alias right now, it will be + * assigned to this Node ID in the local alias cache. + * @param done if an async allocation is necessary, this will be notified + * after a new alias has been received. + * @return the alias if the it was allocated inline, or 0 if there will be + * an asynchronous notification coming later. */ + NodeAlias get_allocated_alias(NodeID destination_id, Executable *done); + + /** @return the number of aliases that are reserved and available for new + * virtual nodes to use. */ + unsigned num_reserved_aliases(); + + /** Removes all aliases that are reserved but not yet used. */ + void clear_reserved_aliases(); /** Releases a given alias. Sends out an AMR frame and puts the alias into * the reserved aliases queue. */ void return_alias(NodeID id, NodeAlias alias); + /** Call from an alternate alias allocator. Marks that alias is reserved + * for the local interface (RID frame is just sent out). Adds the alias to + * the local alias cache and wakes up a flow that might be waiting for an + * alias. + * @param alias a reserved node alias. */ + void add_allocated_alias(NodeAlias alias); + +#ifdef GTEST /** If there is a pending alias allocation waiting for the timer to expire, * finishes it immediately. Needed in test destructors. */ void TEST_finish_pending_allocation(); @@ -133,8 +166,15 @@ public: /** Adds an allocated aliad to the reserved aliases queue. @param alias the next allocated alias to add. */ - void TEST_add_allocated_alias(NodeAlias alias, bool repeat=false); - + void TEST_add_allocated_alias(NodeAlias alias); + + /** Overrides the configured value for reserve_unused_alias_count. */ + void TEST_set_reserve_unused_alias_count(unsigned count) + { + reserveUnusedAliases_ = count; + } +#endif + private: /** Listens to incoming CAN frames and handles alias conflicts. */ class ConflictHandler : public IncomingFrameHandler @@ -172,10 +212,8 @@ private: StateFlowTimer timer_; - /** Freelist of reserved aliases that can be used by virtual nodes. The - AliasAllocatorFlow will post successfully reserved aliases to this - allocator. */ - QAsync reserved_alias_pool_; + /// Set of client flows that are waiting for allocating an alias. + Q waitingClients_; /// 48-bit nodeID that we will use for alias reservations. NodeID if_id_; @@ -195,6 +233,10 @@ private: /// Seed for generating random-looking alias numbers. unsigned seed_ : 12; + /// How many unused aliases we should reserve. Currently we only support 0 + /// or 1 as value. + unsigned reserveUnusedAliases_ : 8; + /// Notifiable used for tracking outgoing frames. BarrierNotifiable n_; diff --git a/src/openlcb/AliasCache.cxx b/src/openlcb/AliasCache.cxx index a637b3d6b..d4258fbcf 100644 --- a/src/openlcb/AliasCache.cxx +++ b/src/openlcb/AliasCache.cxx @@ -33,31 +33,221 @@ #include "openlcb/AliasCache.hxx" +#include + #include "os/OS.hxx" +#include "utils/logging.h" + +#ifdef GTEST +#define TEST_CONSISTENCY +#endif namespace openlcb { #define CONSTANT 0x1B0CA37ABA9 /**< constant for random number generation */ -const NodeID AliasCache::RESERVED_ALIAS_NODE_ID = 1; +#if defined(TEST_CONSISTENCY) +extern volatile int consistency_result; +volatile int consistency_result = 0; + +int AliasCache::check_consistency() +{ + if (idMap.size() != aliasMap.size()) + { + return 1; + } + if (aliasMap.size() == entries) + { + if (!freeList.empty()) + { + return 2; + } + } + else + { + if (freeList.empty()) + { + return 3; + } + } + if (aliasMap.size() == 0 && (!oldest.empty() || !newest.empty())) + { + return 4; + } + std::set free_entries; + for (PoolIdx p = freeList; !p.empty(); p = p.deref(this)->older_) + { + Metadata *m = p.deref(this); + if (free_entries.count(m)) + { + return 5; // duplicate entry on freelist + } + free_entries.insert(m); + } + if (free_entries.size() + aliasMap.size() != entries) + { + return 6; // lost some metadata entries + } + for (auto kv : aliasMap) + { + if (free_entries.count(kv.deref(this))) + { + return 19; + } + } + for (auto kv : idMap) + { + if (free_entries.count(kv.deref(this))) + { + return 20; + } + } + if (aliasMap.size() == 0) + { + if (!oldest.empty()) + { + return 7; + } + if (!newest.empty()) + { + return 8; + } + } + else + { + if (oldest.empty()) + { + return 9; + } + if (newest.empty()) + { + return 10; + } + if (free_entries.count(oldest.deref(this))) + { + return 11; // oldest is free + } + if (free_entries.count(newest.deref(this))) + { + return 12; // newest is free + } + } + if (aliasMap.size() == 0) + { + return 0; + } + // Check linking. + { + PoolIdx prev = oldest; + unsigned count = 1; + if (!prev.deref(this)->older_.empty()) + { + return 13; + } + while (!prev.deref(this)->newer_.empty()) + { + auto next = prev.deref(this)->newer_; + ++count; + if (free_entries.count(next.deref(this))) + { + return 21; + } + if (next.deref(this)->older_.idx_ != prev.idx_) + { + return 14; + } + prev = next; + } + if (prev.idx_ != newest.idx_) + { + return 18; + } + if (count != aliasMap.size()) + { + return 27; + } + } + { + PoolIdx next = newest; + if (!next.deref(this)->newer_.empty()) + { + return 15; + } + while (!next.deref(this)->older_.empty()) + { + auto prev = next.deref(this)->older_; + if (free_entries.count(prev.deref(this))) + { + return 22; + } + if (prev.deref(this)->newer_.idx_ != next.idx_) + { + return 16; + } + next = prev; + } + if (next.idx_ != oldest.idx_) + { + return 17; + } + } + for (unsigned i = 0; i < entries; ++i) + { + if (free_entries.count(pool + i)) + { + continue; + } + auto *e = pool + i; + if (idMap.find(e->get_node_id()) == idMap.end()) + { + return 23; + } + if (idMap.find(e->get_node_id())->idx_ != i) + { + return 24; + } + if (aliasMap.find(e->alias_) == aliasMap.end()) + { + return 25; + } + if (aliasMap.find(e->alias_)->idx_ != i) + { + return 26; + } + } + return 0; +} + +#endif void AliasCache::clear() { idMap.clear(); aliasMap.clear(); - oldest = nullptr; - newest = nullptr; - freeList = nullptr; + oldest.idx_ = NONE_ENTRY; + newest.idx_ = NONE_ENTRY; + freeList.idx_ = NONE_ENTRY; /* initialize the freeList */ for (size_t i = 0; i < entries; ++i) { - pool[i].prev = NULL; - pool[i].next = freeList; - freeList = pool + i; + pool[i].newer_.idx_ = NONE_ENTRY; + pool[i].older_ = freeList; + freeList.idx_ = i; } } +void debug_print_entry(void *, NodeID id, NodeAlias alias) +{ + LOG(INFO, "[%012" PRIx64 "]: %03X", id, alias); +} + +void debug_print_cache(AliasCache *c) +{ + LOG(INFO, "Alias cache:"); + c->for_each(&debug_print_entry, nullptr); +} + /** Add an alias to an alias cache. * @param id 48-bit NMRAnet Node ID to associate alias with * @param alias 12-bit alias associated with Node ID @@ -69,78 +259,96 @@ void AliasCache::add(NodeID id, NodeAlias alias) Metadata *insert; - AliasMap::Iterator it = aliasMap.find(alias); + auto it = aliasMap.find(alias); if (it != aliasMap.end()) { /* we already have a mapping for this alias, so lets remove it */ - insert = (*it).second; - remove(alias); - + insert = it->deref(this); + remove(insert->alias_); + + if (removeCallback) + { + /* tell the interface layer that we removed this mapping */ + (*removeCallback)(insert->get_node_id(), insert->alias_, context); + } + } + auto nit = idMap.find(id); + if (nit != idMap.end()) + { + /* we already have a mapping for this id, so lets remove it */ + insert = nit->deref(this); + remove(insert->alias_); + if (removeCallback) { /* tell the interface layer that we removed this mapping */ - (*removeCallback)(insert->id, insert->alias, context); + (*removeCallback)(insert->get_node_id(), insert->alias_, context); } } - if (freeList) + if (!freeList.empty()) { /* found an empty slot */ - insert = freeList; - freeList = insert->next; + insert = freeList.deref(this); + freeList = insert->older_; } else { - HASSERT(oldest != NULL && newest != NULL); + HASSERT(!oldest.empty() && !newest.empty()); /* kick out the oldest mapping and re-link the oldest endpoint */ - insert = oldest; - if (oldest->newer) + insert = oldest.deref(this); + auto second = insert->newer_; + if (!second.empty()) { - oldest->newer->older = NULL; + second.deref(this)->older_.idx_ = NONE_ENTRY; } - if (insert == newest) + if (insert == newest.deref(this)) { - newest = NULL; + newest.idx_ = NONE_ENTRY; } - oldest = oldest->newer; + oldest = second; - aliasMap.erase(insert->alias); - idMap.erase(insert->id); + aliasMap.erase(aliasMap.find(insert->alias_)); + idMap.erase(idMap.find(insert->get_node_id())); if (removeCallback) { /* tell the interface layer that we removed this mapping */ - (*removeCallback)(insert->id, insert->alias, context); + (*removeCallback)(insert->get_node_id(), insert->alias_, context); } } - - insert->timestamp = OSTime::get_monotonic(); - insert->id = id; - insert->alias = alias; - aliasMap[alias] = insert; - idMap[id] = insert; + insert->set_node_id(id); + insert->alias_ = alias; + + PoolIdx n; + n.idx_ = insert - pool; + aliasMap.insert(PoolIdx(n)); + idMap.insert(PoolIdx(n)); /* update the time based list */ - insert->newer = NULL; - if (newest == NULL) + insert->newer_.idx_ = NONE_ENTRY; + if (newest.empty()) { /* if newest == NULL, then oldest must also be NULL */ - HASSERT(oldest == NULL); + HASSERT(oldest.empty()); - insert->older = NULL; - oldest = insert; + insert->older_.idx_ = NONE_ENTRY; + oldest = n; } else { - insert->older = newest; - newest->newer = insert; + insert->older_ = newest; + newest.deref(this)->newer_ = n; } - newest = insert; - - return; + newest = n; + +#if defined(TEST_CONSISTENCY) + consistency_result = check_consistency(); + HASSERT(0 == consistency_result); +#endif } /** Remove an alias from an alias cache. This method does not call the @@ -150,44 +358,67 @@ void AliasCache::add(NodeID id, NodeAlias alias) */ void AliasCache::remove(NodeAlias alias) { - AliasMap::Iterator it = aliasMap.find(alias); + auto it = aliasMap.find(alias); if (it != aliasMap.end()) { - Metadata *metadata = (*it).second; + Metadata *metadata = it->deref(this); aliasMap.erase(it); - idMap.erase(metadata->id); - - if (metadata->newer) + idMap.erase(idMap.find(metadata->get_node_id())); + + if (!metadata->newer_.empty()) { - metadata->newer->older = metadata->older; + metadata->newer_.deref(this)->older_ = metadata->older_; } - if (metadata->older) + if (!metadata->older_.empty()) { - metadata->older->newer = metadata->newer; + metadata->older_.deref(this)->newer_ = metadata->newer_; } - if (metadata == newest) + if (metadata == newest.deref(this)) { - newest = metadata->older; + newest = metadata->older_; } - if (metadata == oldest) + if (metadata == oldest.deref(this)) { - oldest = metadata->newer; + oldest = metadata->newer_; } - - metadata->next = freeList; - freeList = metadata; + + metadata->older_ = freeList; + freeList.idx_ = metadata - pool; } - + +#if defined(TEST_CONSISTENCY) + consistency_result = check_consistency(); + HASSERT(0 == consistency_result); +#endif } bool AliasCache::retrieve(unsigned entry, NodeID* node, NodeAlias* alias) { HASSERT(entry < size()); Metadata* md = pool + entry; - if (!md->alias) return false; - if (node) *node = md->id; - if (alias) *alias = md->alias; + if (!md->alias_) return false; + if (node) *node = md->get_node_id(); + if (alias) *alias = md->alias_; + return true; +} + +bool AliasCache::next_entry(NodeID bound, NodeID *node, NodeAlias *alias) +{ + auto it = idMap.upper_bound(bound); + if (it == idMap.end()) + { + return false; + } + Metadata *metadata = it->deref(this); + if (alias) + { + *alias = metadata->alias_; + } + if (node) + { + *node = metadata->get_node_id(); + } return true; } @@ -199,15 +430,15 @@ NodeAlias AliasCache::lookup(NodeID id) { HASSERT(id != 0); - IdMap::Iterator it = idMap.find(id); + auto it = idMap.find(id); if (it != idMap.end()) { - Metadata *metadata = (*it).second; - + Metadata *metadata = it->deref(this); + /* update timestamp */ touch(metadata); - return metadata->alias; + return metadata->alias_; } /* no match found */ @@ -222,15 +453,15 @@ NodeID AliasCache::lookup(NodeAlias alias) { HASSERT(alias != 0); - AliasMap::Iterator it = aliasMap.find(alias); + auto it = aliasMap.find(alias); if (it != aliasMap.end()) { - Metadata *metadata = (*it).second; - + Metadata *metadata = it->deref(this); + /* update timestamp */ touch(metadata); - return metadata->id; + return metadata->get_node_id(); } /* no match found */ @@ -246,9 +477,10 @@ void AliasCache::for_each(void (*callback)(void*, NodeID, NodeAlias), void *cont { HASSERT(callback != NULL); - for (Metadata *metadata = newest; metadata != NULL; metadata = metadata->older) + for (PoolIdx idx = newest; !idx.empty(); idx = idx.deref(this)->older_) { - (*callback)(context, metadata->id, metadata->alias); + Metadata *metadata = idx.deref(this); + (*callback)(context, metadata->get_node_id(), metadata->alias_); } } @@ -277,26 +509,28 @@ NodeAlias AliasCache::generate() */ void AliasCache::touch(Metadata* metadata) { - metadata->timestamp = OSTime::get_monotonic(); - - if (metadata != newest) + if (metadata != newest.deref(this)) { - if (metadata == oldest) + if (metadata == oldest.deref(this)) { - oldest = metadata->newer; - oldest->older = NULL; + oldest = metadata->newer_; + oldest.deref(this)->older_.idx_ = NONE_ENTRY; } else { /* we have someone older */ - metadata->older->newer = metadata->newer; + metadata->older_.deref(this)->newer_ = metadata->newer_; } - metadata->newer->older = metadata->older; - metadata->newer = NULL; - metadata->older = newest; - newest->newer = metadata; - newest = metadata; + metadata->newer_.deref(this)->older_ = metadata->older_; + metadata->newer_.idx_ = NONE_ENTRY; + metadata->older_ = newest; + newest.deref(this)->newer_.idx_ = metadata - pool; + newest.idx_ = metadata - pool; } +#if defined(TEST_CONSISTENCY) + consistency_result = check_consistency(); + HASSERT(0 == consistency_result); +#endif } } diff --git a/src/openlcb/AliasCache.cxxtest b/src/openlcb/AliasCache.cxxtest index 9998d8cb4..c778a6e6a 100644 --- a/src/openlcb/AliasCache.cxxtest +++ b/src/openlcb/AliasCache.cxxtest @@ -52,6 +52,22 @@ static void alias_callback(void *context, NodeID node_id, NodeAlias alias) count++; } +/// Verifies that calling the next method gives back the correct next node ID. +void test_alias_next(AliasCache *cache, int count) +{ + NodeID last = 0; + NodeID next = 0; + NodeAlias next_alias; + for (int i = 0; i < count; i++) + { + ASSERT_TRUE(cache->next_entry(last, &next, &next_alias)); + EXPECT_EQ(node_ids[i], next); + EXPECT_EQ(aliases[i], next_alias); + last = next; + } + EXPECT_FALSE(cache->next_entry(last, &next, &next_alias)); +} + TEST(AliasCacheTest, constructor) { /* construct an object, map in a node, and run the for_each */ @@ -103,6 +119,8 @@ TEST(AliasCacheTest, ordering) EXPECT_TRUE(aliasCache->lookup((NodeID)103) == 6); EXPECT_TRUE(aliasCache->lookup((NodeID)102) == 11); EXPECT_TRUE(aliasCache->lookup((NodeID)101) == 10); + + test_alias_next(aliasCache, 6); } TEST(AliasCacheTest, reordering) @@ -126,8 +144,10 @@ TEST(AliasCacheTest, reordering) EXPECT_TRUE(aliasCache->lookup((NodeAlias)84) == 104); EXPECT_TRUE(aliasCache->lookup((NodeAlias)6) == 103); EXPECT_TRUE(aliasCache->lookup((NodeAlias)11) == 102); - EXPECT_TRUE(aliasCache->lookup((NodeAlias)10) == 101); - + EXPECT_TRUE(aliasCache->lookup((NodeAlias)10) == 101); + + test_alias_next(aliasCache, 6); + aliasCache->for_each(alias_callback, (void*)0xDEADBEEF); EXPECT_EQ(count, 6); @@ -152,7 +172,9 @@ TEST(AliasCacheTest, reordering) EXPECT_TRUE(aliasCache->lookup((NodeID)104) == 84); EXPECT_TRUE(aliasCache->lookup((NodeID)103) == 6); EXPECT_TRUE(aliasCache->lookup((NodeID)102) == 11); - EXPECT_TRUE(aliasCache->lookup((NodeID)101) == 10); + EXPECT_TRUE(aliasCache->lookup((NodeID)101) == 10); + + test_alias_next(aliasCache, 6); } TEST(AliasCacheTest, generate) @@ -463,94 +485,6 @@ protected: AliasCache c_{get_id(0x33), 10}; }; -namespace openlcb { -int AliasCache::check_consistency() { - if (idMap.size() != aliasMap.size()) return 1; - if (aliasMap.size() == entries) { - if (freeList != nullptr) return 2; - } else { - if (freeList == nullptr) return 3; - } - if (aliasMap.size() == 0 && - (oldest != nullptr || newest != nullptr)) { - return 4; - } - std::set free_entries; - for (Metadata* m = freeList; m; m=m->next) { - if (free_entries.count(m)) { - return 5; // duplicate entry on freelist - } - free_entries.insert(m); - } - if (free_entries.size() + aliasMap.size() != entries) { - return 6; // lost some metadata entries - } - for (auto kv : aliasMap) { - if (free_entries.count(kv.second)) { - return 19; - } - } - for (auto kv : idMap) { - if (free_entries.count(kv.second)) { - return 20; - } - } - if (aliasMap.size() == 0) { - if (oldest != nullptr) return 7; - if (newest != nullptr) return 8; - } else { - if (oldest == nullptr) return 9; - if (newest == nullptr) return 10; - } - if (free_entries.count(oldest)) { - return 11; // oldest is free - } - if (free_entries.count(newest)) { - return 12; // newest is free - } - if (aliasMap.size() == 0) return 0; - // Check linking. - { - Metadata* prev = oldest; - unsigned count = 1; - if (prev->older) return 13; - while (prev->newer) { - auto* next = prev->newer; - ++count; - if (free_entries.count(next)) { - return 21; - } - if (next->older != prev) return 14; - prev = next; - } - if (prev != newest) return 18; - if (count != aliasMap.size()) return 27; - } - { - Metadata* next = newest; - if (next->newer) return 15; - while (next->older) { - auto* prev = next->older; - if (free_entries.count(prev)) { - return 22; - } - if (prev->newer != next) return 16; - next = prev; - } - if (next != oldest) return 17; - } - for (unsigned i = 0; i < entries; ++i) { - if (free_entries.count(pool+i)) continue; - auto* e = pool+i; - if (idMap.find(e->id) == idMap.end()) return 23; - if (idMap[e->id] != e) return 24; - if (aliasMap.find(e->alias) == aliasMap.end()) return 25; - if (aliasMap[e->alias] != e) return 26; - } - return 0; -} - -} TEST_F(AliasStressTest, stress_test) { diff --git a/src/openlcb/AliasCache.hxx b/src/openlcb/AliasCache.hxx index b59ddc55d..1483d6092 100644 --- a/src/openlcb/AliasCache.hxx +++ b/src/openlcb/AliasCache.hxx @@ -35,9 +35,9 @@ #define _OPENLCB_ALIASCACHE_HXX_ #include "openlcb/Defs.hxx" -#include "utils/macros.h" #include "utils/Map.hxx" -#include "utils/RBTree.hxx" +#include "utils/SortedListMap.hxx" +#include "utils/macros.h" namespace openlcb { @@ -48,8 +48,40 @@ namespace openlcb * is no mutual exclusion locking mechanism built into this class. Mutual * exclusion must be handled by the user as needed. * - * @todo the class uses RBTree, consider a version that is a linear search for - * a small number of entries. + * This data structure is sometimes used with very large entry count + * (hundreds), therefore we must be careful about memory efficiency! + * + * Theory of operation: + * + * The data structure has three access patterns: + * - lookup of alias -> ID + * - lookup of ID -> alias + * - eviction of oldest entry from the cache + * + * We have three data structures to match these use-cases: + * + * The struct Metadata stores the actual NodeID and NodeAlias values. This is + * laid out in the pre-allocated C array `pool`. A freeList shows where unused + * entries are. + * + * To support the eviction of oldest entry, an LRU doubly-linked list is + * created in these Metadata entries. The links are represented by indexes into + * the `pool` array. + * + * Indexes into the `pool` are encapsulated into the PoolIdx struct to make + * them a unique C++ type. This is needed for template disambiguation. We also + * have a dereference function on PoolIdx that turns it into a Metadata + * pointer. + * + * To support lookup by alias, we have a SortedListSet which contains all used + * indexes as PoolIdx, sorted by the alias property in the respective entry in + * `pool`. This is achieved by a custom comparator that dereferences the + * PoolIdx object and fetches the alias from the Metadata struct. The sorted + * vector is maintained using the SortedListSet<> template, and takes a total + * of only 2 bytes per entry. + * + * A similar sorted vector is kept sorted by the NodeID values. This also takes + * only 2 bytes per entry. */ class AliasCache { @@ -62,24 +94,23 @@ public: * @param context context pointer to pass to remove_callback */ AliasCache(NodeID seed, size_t _entries, - void (*remove_callback)(NodeID id, NodeAlias alias, void *) = NULL, - void *context = NULL) - : pool(new Metadata[_entries]), - freeList(NULL), - aliasMap(_entries), - idMap(_entries), - oldest(NULL), - newest(NULL), - seed(seed), - entries(_entries), - removeCallback(remove_callback), - context(context) + void (*remove_callback)(NodeID id, NodeAlias alias, void *) = NULL, + void *context = NULL) + : pool(new Metadata[_entries]) + , aliasMap(this) + , idMap(this) + , seed(seed) + , entries(_entries) + , removeCallback(remove_callback) + , context(context) { + aliasMap.reserve(_entries); + idMap.reserve(_entries); clear(); } - /** This NodeID will be used for reserved but unused local aliases. */ - static const NodeID RESERVED_ALIAS_NODE_ID; + /// Sentinel entry for empty lists. + static constexpr uint16_t NONE_ENTRY = 0xFFFFu; /** Reinitializes the entire map. */ void clear(); @@ -126,12 +157,22 @@ public: * changes. * @param entry is between 0 and size() - 1. * @param node will be filled with the node ID. May be null. - * @param aliad will be filles with the alias. May be null. + * @param alias will be filled with the alias. May be null. * @return true if the entry is valid, and node and alias were filled, otherwise false if the entry is not allocated. */ bool retrieve(unsigned entry, NodeID* node, NodeAlias* alias); - /** Generate a 12-bit pseudo-random alias for a givin alias cache. + /** Retrieves the next entry by increasing node ID. + * @param bound is a Node ID. Will search for the next largest node ID + * (upper bound of this key). + * @param node will be filled with the node ID. May be null. + * @param alias will be filled with the alias. May be null. + * @return true if a larger element is found and node and alias were + * filled, otherwise false if bound is >= the largest node ID in the cache. + */ + bool next_entry(NodeID bound, NodeID *node, NodeAlias *alias); + + /** Generate a 12-bit pseudo-random alias for a given alias cache. * @return pseudo-random 12-bit alias, an alias of zero is invalid */ NodeAlias generate(); @@ -152,47 +193,172 @@ private: UNUSED_MASK = 0x10000000 }; + struct Metadata; + class PoolIdx; + friend class PoolIdx; + + /// Encapsulation of a pointer into the pool array. + class PoolIdx + { + public: + /// Constructor. Sets the pointer to invalid. + PoolIdx() + : idx_(NONE_ENTRY) + { + } + /// @return true if this entry does not point anywhere. + bool empty() + { + return idx_ == NONE_ENTRY; + } + /// Indexes the pool of the AliasCache. + uint16_t idx_; + /// Dereferences a pool index as if it was a pointer. + /// @param parent the AliasCache whose pool to index. + /// @return referenced Metadata pointer. + Metadata *deref(AliasCache *parent) + { + DASSERT(idx_ != NONE_ENTRY); + return parent->pool + idx_; + } + }; + /** Interesting information about a given cache entry. */ struct Metadata { - NodeID id = 0; /**< 48-bit NMRAnet Node ID */ - NodeAlias alias = 0; /**< NMRAnet alias */ - long long timestamp; /**< time stamp of last usage */ - union + /// Sets the node ID field. + /// @param id the node ID to set. + void set_node_id(NodeID id) { - Metadata *prev; /**< unused */ - Metadata *newer; /**< pointer to the next newest entry */ - }; - union + nodeIdLow_ = id & 0xFFFFFFFFu; + nodeIdHigh_ = (id >> 32) & 0xFFFFu; + } + + /// @return the node ID field. + NodeID get_node_id() { - Metadata *next; /**< pointer to next freeList entry */ - Metadata *older; /**< pointer to the next oldest entry */ - }; + uint64_t h = nodeIdHigh_; + h <<= 32; + h |= nodeIdLow_; + return h; + } + + /// OpenLCB Node ID low 32 bits. + uint32_t nodeIdLow_ = 0; + /// OpenLCB Node ID high 16 bits. + uint16_t nodeIdHigh_ = 0; + /// OpenLCB-CAN alias + NodeAlias alias_ = 0; + /// Index of next-newer entry according to the LRU linked list. + PoolIdx newer_; + /// Index of next-older entry according to the LRU linked list. + PoolIdx older_; }; /** pointer to allocated Metadata pool */ Metadata *pool; - - /** list of unused mapping entries */ - Metadata *freeList; - + + /// Comparator object comparing the aliases stored in the pool. + class AliasComparator + { + public: + /// Constructor + /// @param parent owning AliasCache. + AliasComparator(AliasCache *parent) + : parent_(parent) + { + } + + /// Less-than action. + /// @param e left hand side + /// @param alias right hand side + bool operator()(PoolIdx e, uint16_t alias) const + { + return e.deref(parent_)->alias_ < alias; + } + + /// Less-than action. + /// @param alias left hand side + /// @param e right hand side + bool operator()(uint16_t alias, PoolIdx e) const + { + return alias < e.deref(parent_)->alias_; + } + + /// Less-than action. + /// @param a left hand side + /// @param b right hand side + bool operator()(PoolIdx a, PoolIdx b) const + { + return a.deref(parent_)->alias_ < b.deref(parent_)->alias_; + } + + private: + /// AliasCache whose pool we are indexing into. + AliasCache *parent_; + }; + + /// Comparator object comparing the aliases stored in the pool. + class IdComparator + { + public: + /// Constructor + /// @param parent owning AliasCache. + IdComparator(AliasCache *parent) + : parent_(parent) + { + } + + /// Less-than action. + /// @param e left hand side + /// @param id right hand side + bool operator()(PoolIdx e, NodeID id) const + { + return e.deref(parent_)->get_node_id() < id; + } + + /// Less-than action. + /// @param id left hand side + /// @param e right hand side + bool operator()(NodeID id, PoolIdx e) const + { + return id < e.deref(parent_)->get_node_id(); + } + + /// Less-than action. + /// @param a left hand side + /// @param b right hand side + bool operator()(PoolIdx a, PoolIdx b) const + { + return a.deref(parent_)->get_node_id() < + b.deref(parent_)->get_node_id(); + } + + private: + /// AliasCache whose pool we are indexing into. + AliasCache *parent_; + }; + /** Short hand for the alias Map type */ - typedef Map AliasMap; - + typedef SortedListSet AliasMap; + /** Short hand for the ID Map type */ - typedef Map IdMap; + typedef SortedListSet IdMap; /** Map of alias to corresponding Metadata */ AliasMap aliasMap; /** Map of Node ID to corresponding Metadata */ IdMap idMap; - - /** oldest untouched entry */ - Metadata *oldest; - - /** newest, most recently touched entry */ - Metadata *newest; + + /** list of unused mapping entries (index into pool) */ + PoolIdx freeList; + + /** oldest untouched entry (index into pool) */ + PoolIdx oldest; + + /** newest, most recently touched entry (index into pool) */ + PoolIdx newest; /** Seed for the generation of the next alias */ NodeID seed; @@ -214,6 +380,6 @@ private: DISALLOW_COPY_AND_ASSIGN(AliasCache); }; -} /* namepace NMRAnet */ +} /* namespace openlcb */ -#endif /* _NMRAnetAliasCache_hxx */ +#endif // _OPENLCB_ALIASCACHE_HXX_ diff --git a/src/openlcb/BroadcastTime.cxx b/src/openlcb/BroadcastTime.cxx index fb5a91be0..0e2a8630a 100644 --- a/src/openlcb/BroadcastTime.cxx +++ b/src/openlcb/BroadcastTime.cxx @@ -46,8 +46,10 @@ namespace openlcb // void BroadcastTime::clear_timezone() { +#ifndef ESP32 setenv("TZ", "GMT0", 1); tzset(); +#endif } } // namespace openlcb diff --git a/src/openlcb/BulkAliasAllocator.cxx b/src/openlcb/BulkAliasAllocator.cxx new file mode 100644 index 000000000..dea531fa1 --- /dev/null +++ b/src/openlcb/BulkAliasAllocator.cxx @@ -0,0 +1,286 @@ +/** \copyright + * Copyright (c) 2020 Balazs Racz + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * \file BulkAliasAllocator.cxx + * + * State flow for allocating many aliases at the same time. + * + * @author Balazs Racz + * @date 14 Nov 2020 + */ + +#include "openlcb/BulkAliasAllocator.hxx" +#include "openlcb/CanDefs.hxx" +#include "utils/MakeUnique.hxx" + +namespace openlcb +{ + +/// Implementation of the BulkAliasAllocatorInterface to allocate many aliases +/// at the same time. +class BulkAliasAllocator : public CallableFlow +{ +public: + /// Constructor + /// @param iface the openlcb CAN interface + BulkAliasAllocator(IfCan *iface) + : CallableFlow(iface) + { + } + + /// Start of flow when a request arrives to allocate many aliases. Resets + /// the internal state and goes on to start the allocation process. + Action entry() override + { + startTime_ = os_get_time_monotonic(); + pendingAliasesByTime_.clear(); + pendingAliasesByKey_.clear(); + nextToStampTime_ = 0; + nextToClaim_ = 0; + if_can()->frame_dispatcher()->register_handler(&conflictHandler_, 0, 0); + return call_immediately(STATE(send_cid_frames)); + } + + /// Picks a bunch of random aliases, sends CID frames for them to the bus. + Action send_cid_frames() + { + unsigned needed = std::min(request()->numAliases_, + (unsigned)(config_bulk_alias_num_can_frames() + 3) / 4); + if (!needed) + { + return call_immediately(STATE(wait_for_results)); + } + bn_.reset(this); + for (unsigned i = 0; i < needed; ++i) + { + NodeAlias next_alias = if_can()->alias_allocator()->get_new_seed(); + auto if_id = if_can()->alias_allocator()->if_node_id(); + send_can_frame(next_alias, (if_id >> 36) & 0xfff, 7); + send_can_frame(next_alias, (if_id >> 24) & 0xfff, 6); + send_can_frame(next_alias, (if_id >> 12) & 0xfff, 5); + send_can_frame(next_alias, (if_id >> 0) & 0xfff, 4); + --request()->numAliases_; + pendingAliasesByTime_.push_back({next_alias}); + pendingAliasesByKey_.insert({next_alias}); + } + bn_.notify(); + return wait_and_call(STATE(stamp_time)); + } + + /// Adds the timestamps when the CID requests were sent out. + Action stamp_time() + { + auto ctime = relative_time(); + for (unsigned i = nextToStampTime_; i < pendingAliasesByTime_.size(); + ++i) + { + pendingAliasesByTime_[i].cidTime_ = ctime; + } + nextToStampTime_ = pendingAliasesByTime_.size(); + // Go back to sending more CID frames as needed. + return call_immediately(STATE(send_cid_frames)); + } + + /// Sends out the RID frames for any alias that the 200 msec has already + /// elapsed, then waits a bit and tries again. + Action wait_for_results() + { + if (nextToClaim_ == pendingAliasesByTime_.size()) + { + return complete(); + } + if (request()->numAliases_) + { + // Some conflicts were identified, go and allocate more. + return call_immediately(STATE(send_cid_frames)); + } + auto ctime = relative_time(); + unsigned num_sent = 0; + bn_.reset(this); + while ((nextToClaim_ < pendingAliasesByTime_.size()) && + (num_sent < (unsigned)(config_bulk_alias_num_can_frames())) && + (pendingAliasesByTime_[nextToClaim_].cidTime_ + ALLOCATE_DELAY < + ctime)) + { + NodeAlias a = + (NodeAlias)(pendingAliasesByTime_[nextToClaim_].alias_); + ++nextToClaim_; + auto it = pendingAliasesByKey_.find(a); + if (it->hasConflict_) + { + // we skip this alias because there was a conflict. + continue; + } + if_can()->alias_allocator()->add_allocated_alias(a); + ++num_sent; + send_can_frame(a, CanDefs::RID_FRAME, 0); + } + if (bn_.abort_if_almost_done()) + { + // no frame sent + return sleep_and_call( + &timer_, MSEC_TO_NSEC(10), STATE(wait_for_results)); + } + else + { + bn_.notify(); + // Wait for outgoing frames to be gone and call this again. + return wait(); + } + } + + /// Called when all RID frames are sent out. + Action complete() + { + if_can()->frame_dispatcher()->unregister_handler_all(&conflictHandler_); + pendingAliasesByTime_.clear(); + pendingAliasesByKey_.clear(); + return return_ok(); + } + +private: + /// Callback from the stack for all incoming frames while we are + /// operating. We sniff the alias uot of it and record any conflicts we + /// see. + /// @param message an incoming CAN frame. + void handle_conflict(Buffer *message) + { + auto rb = get_buffer_deleter(message); + auto alias = CanDefs::get_src(GET_CAN_FRAME_ID_EFF(*message->data())); + auto it = pendingAliasesByKey_.find(alias); + if (it != pendingAliasesByKey_.end() && !it->hasConflict_) + { + it->hasConflict_ = 1; + ++request()->numAliases_; + } + } + + /// Listens to incoming CAN frames and handles alias conflicts. + IncomingFrameHandler::GenericHandler conflictHandler_ { + this, &BulkAliasAllocator::handle_conflict}; + + /// How many count to wait before sending out the RID frames. One count is + /// 10 msec (see { \link relative_time } ). + static constexpr unsigned ALLOCATE_DELAY = 20; + + /// Sends a CAN control frame to the bus. Take a share of the barrier bn_ + /// to send with the frame. + /// @param src source alias to use on the frame. + /// @param control_field 16-bit control value (e.g. RID_FRAME, or 0 top + /// nibble and a chunk of the unique node ID in the middle). + /// @param sequence used for CID messages. + void send_can_frame(NodeAlias src, uint16_t control_field, int sequence) + { + auto *b = if_can()->frame_write_flow()->alloc(); + b->set_done(bn_.new_child()); + CanDefs::control_init(*b->data(), src, control_field, sequence); + if_can()->frame_write_flow()->send(b, 0); + } + + /// @return the openlcb CAN interface + IfCan *if_can() + { + return static_cast(service()); + } + + /// @return the time elapsed from start time in 10 msec units. + unsigned relative_time() + { + return (os_get_time_monotonic() - startTime_) / MSEC_TO_NSEC(10); + } + + /// We store this type in the time-ordered aliases structure. + struct PendingAliasInfo + { + /// Constructor + /// @param alias the openlcb alias that is being represented here. + PendingAliasInfo(NodeAlias alias) + : alias_(alias) + , cidTime_(0) + { + } + + /// The value of the alias + unsigned alias_ : 12; + /// The time when the CID requests were sent. Counter in + /// relative_time(), i.e. 10 msec per increment. + unsigned cidTime_ : 8; + }; + static_assert(sizeof(PendingAliasInfo) == 4, "memory bloat"); + + /// We store this type in the sorted map lookup structure. + struct AliasLookupInfo + { + /// Constructor + /// @param alias the openlcb alias that is being represented here. + AliasLookupInfo(NodeAlias alias) + : alias_(alias) + , hasConflict_(0) + { + } + + /// The value of the alias + uint16_t alias_ : 12; + /// 1 if we have seen a conflict + uint16_t hasConflict_ : 1; + }; + static_assert(sizeof(AliasLookupInfo) == 2, "memory bloat"); + /// Comparator function on AliasLookupInfo objects. + struct LookupCompare + { + bool operator()(AliasLookupInfo a, AliasLookupInfo b) + { + return a.alias_ < b.alias_; + } + }; + + /// Helper object for sleeping. + StateFlowTimer timer_ {this}; + /// Helper object to determine when the CAN frames have flushed from the + /// system. + BarrierNotifiable bn_; + /// We measure time elapsed relative to this point. + long long startTime_; + /// Stores the aliases we are trying to allocate in time order of picking + /// them. + std::vector pendingAliasesByTime_; + /// Stores the aliases we are trying to allocate in the alias order. + SortedListSet pendingAliasesByKey_; + /// Index into the pendingAliasesByTime_ vector where we need to stmap + /// time. + uint16_t nextToStampTime_; + /// Index into the pendingAliasesByTime_ vector where we need to send out + /// the reserve frame. + uint16_t nextToClaim_; +}; + +std::unique_ptr create_bulk_alias_allocator( + IfCan *can_if) +{ + return std::make_unique(can_if); +} + +} // namespace openlcb diff --git a/src/openlcb/BulkAliasAllocator.hxx b/src/openlcb/BulkAliasAllocator.hxx new file mode 100644 index 000000000..2fee889a0 --- /dev/null +++ b/src/openlcb/BulkAliasAllocator.hxx @@ -0,0 +1,64 @@ +/** \copyright + * Copyright (c) 2020 Balazs Racz + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * \file BulkAliasAllocator.hxx + * + * State flow for allocating many aliases at the same time. + * + * @author Balazs Racz + * @date 14 Nov 2020 + */ + +#include "executor/CallableFlow.hxx" +#include "openlcb/AliasAllocator.hxx" +#include "openlcb/AliasCache.hxx" +#include "openlcb/CanDefs.hxx" + +namespace openlcb +{ + +/// Message type to request allocating many aliases for an interface. +struct BulkAliasRequest : CallableFlowRequestBase +{ + /// @param count how many aliases to allocate. + void reset(unsigned count) + { + reset_base(); + numAliases_ = count; + } + + /// How many aliases to allocate. + unsigned numAliases_; +}; + +using BulkAliasAllocatorInterface = FlowInterface>; + +/// Creates a bulk alias allocator. +/// @param can_if the interface to bind it to. +std::unique_ptr create_bulk_alias_allocator( + IfCan *can_if); + +} // namespace openlcb diff --git a/src/openlcb/CanDefs.hxx b/src/openlcb/CanDefs.hxx index 08749f49a..9f4fdd189 100644 --- a/src/openlcb/CanDefs.hxx +++ b/src/openlcb/CanDefs.hxx @@ -121,12 +121,12 @@ struct CanDefs { HIGH_PRIORITY = 0, /**< high priority CAN message */ NORMAL_PRIORITY = 1 /**< normal priority CAN message */ }; - + enum ControlField { RID_FRAME = 0x0700, /**< Reserve ID Frame */ AMD_FRAME = 0x0701, /**< Alias Map Definition frame */ - AME_FRAME = 0x0702, /**< Alias Mapping Inquery */ + AME_FRAME = 0x0702, /**< Alias Mapping Enquiry */ AMR_FRAME = 0x0703 /**< Alias Map Reset */ }; @@ -136,6 +136,18 @@ struct CanDefs { NOT_LAST_FRAME = 0x10, }; + /// Constants used in the LocalAliasCache for reserved but not used + /// aliases. + enum ReservedAliasNodeId + { + /// To mark a reserved alias in the local alias cache, we use this as a + /// node ID and add the alias to the lowest 12 bits. Since this value + /// starts with a zero MSB byte, it is not a valid node ID. + RESERVED_ALIAS_NODE_BITS = 0xF000, + /// Mask for the reserved aliases. + RESERVED_ALIAS_NODE_MASK = 0xFFFFFFFFF000 + }; + /** Get the source field value of the CAN ID. * @param can_id identifier to act upon * @return source field @@ -363,6 +375,23 @@ struct CanDefs { frame.can_dlc = 0; } + /** Computes a reserved alias node ID for the local alias cache map. + * @param alias the alias to reserve + * @return Node ID to use in the alias map as a key. + */ + static NodeID get_reserved_alias_node_id(NodeAlias alias) + { + return RESERVED_ALIAS_NODE_BITS | alias; + } + + /** Tests if a node ID is a reserved alias Node ID. + * @param id node id to test + * @return true if this is a reserved alias node ID. */ + static bool is_reserved_alias_node_id(NodeID id) + { + return (id & RESERVED_ALIAS_NODE_MASK) == RESERVED_ALIAS_NODE_BITS; + } + private: /** This class should not be instantiated. */ CanDefs(); diff --git a/src/openlcb/DefaultNodeRegistry.hxx b/src/openlcb/DefaultNodeRegistry.hxx new file mode 100644 index 000000000..2b10b6b88 --- /dev/null +++ b/src/openlcb/DefaultNodeRegistry.hxx @@ -0,0 +1,78 @@ +/** \copyright + * Copyright (c) 2020, Balazs Racz + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * \file DefaultNodeRegistry.hxx + * + * Default implementations for data structures keyed by virtual nodes. + * + * @author Balazs Racz + * @date 12 Sep 2020 + */ + +#ifndef _OPENLCB_DEFAULTNODEREGISTRY_HXX_ +#define _OPENLCB_DEFAULTNODEREGISTRY_HXX_ + +#include + +#include "openlcb/NodeRegistry.hxx" + +namespace openlcb +{ + +class Node; + +class DefaultNodeRegistry : public NodeRegistry +{ +public: + /// Adds a node to the list of registered nodes. + /// @param node a virtual node. + void register_node(openlcb::Node *node) override + { + nodes_.insert(node); + } + + /// Removes a node from the list of registered nodes. + /// @param node a virtual node. + void unregister_node(openlcb::Node *node) override + { + nodes_.erase(node); + } + + /// Checks if a node is registered. + /// @param node a virtual node. + /// @return true if this node has been registered. + bool is_node_registered(openlcb::Node *node) override + { + return nodes_.find(node) != nodes_.end(); + } + +private: + std::set nodes_; +}; + +} // namespace openlcb + +#endif // _OPENLCB_DEFAULTNODEREGISTRY_HXX_ diff --git a/src/openlcb/Defs.hxx b/src/openlcb/Defs.hxx index e45ff994d..68f3e734e 100644 --- a/src/openlcb/Defs.hxx +++ b/src/openlcb/Defs.hxx @@ -246,6 +246,21 @@ struct Defs /// station may react to this by restoring locomotive speed settings. static constexpr uint64_t CLEAR_EMERGENCY_STOP_EVENT = 0x010000000000FFFCULL; + /// "Power supply brownout detected below minimum required by standard" + /// This event can be generated when a node detects that the CAN bus power + /// has dropped below the minimum declared in the standard. + static constexpr uint64_t POWER_STANDARD_BROWNOUT_EVENT = 0x010000000000FFF0ULL; + + /// "Power supply brownout detected below minimum required by node" + /// This event can be generated when a node detects that it has + /// insufficient power for normal operations. + static constexpr uint64_t NODE_POWER_BROWNOUT_EVENT = 0x010000000000FFF1ULL; + + /// "Ident button combination pressed" + /// This event can be generated by a node when it is instructed to generate + /// an identification event. + static constexpr uint64_t NODE_IDENT_BUTTON_EVENT = 0x010000000000FE00ULL; + /** Status of the pysical layer link */ enum LinkStatus { diff --git a/src/openlcb/If.cxx b/src/openlcb/If.cxx index 5e15582df..d53d06008 100644 --- a/src/openlcb/If.cxx +++ b/src/openlcb/If.cxx @@ -119,11 +119,11 @@ void buffer_to_error(const Payload &payload, uint16_t *error_code, error_message->clear(); if (payload.size() >= 2 && error_code) { - *error_code = (((uint16_t)payload[0]) << 8) | payload[1]; + *error_code = (((uint16_t)payload[0]) << 8) | (uint8_t)payload[1]; } if (payload.size() >= 4 && mti) { - *mti = (((uint16_t)payload[2]) << 8) | payload[3]; + *mti = (((uint16_t)payload[2]) << 8) | (uint8_t)payload[3]; } if (payload.size() > 4 && error_message) { diff --git a/src/openlcb/If.hxx b/src/openlcb/If.hxx index 4dd3ee4fa..c0b82c516 100644 --- a/src/openlcb/If.hxx +++ b/src/openlcb/If.hxx @@ -282,12 +282,20 @@ public: MessageHandler *global_message_write_flow() { HASSERT(globalWriteFlow_); + if (txHook_) + { + txHook_(); + } return globalWriteFlow_; } /** @return Flow to send addressed messages to the NMRAnet bus. */ MessageHandler *addressed_message_write_flow() { HASSERT(addressedWriteFlow_); + if (txHook_) + { + txHook_(); + } return addressedWriteFlow_; } @@ -399,7 +407,15 @@ public: * the interface holds internally. Noop for TCP interface. Must be called * on the interface executor. */ virtual void canonicalize_handle(NodeHandle *h) {} - + + /// Sets a transmit hook. This function will be called once for every + /// OpenLCB message transmitted. Used for implementing activity LEDs. + /// @param hook function to call for each transmit message. + void set_tx_hook(std::function hook) + { + txHook_ = std::move(hook); + } + protected: void remove_local_node_from_map(Node *node) { auto it = localNodes_.find(node->node_id()); @@ -416,6 +432,9 @@ private: /// Flow responsible for routing incoming messages to handlers. MessageDispatchFlow dispatcher_; + /// This function is pinged every time a message is transmitted. + std::function txHook_; + typedef Map VNodeMap; /// Local virtual nodes registered on this interface. diff --git a/src/openlcb/IfCan.cxx b/src/openlcb/IfCan.cxx index 121c8ddd3..441258229 100644 --- a/src/openlcb/IfCan.cxx +++ b/src/openlcb/IfCan.cxx @@ -362,6 +362,9 @@ class AMEGlobalQueryHandler : public StateFlowBase, return; } needRerun_ = true; + // Drops all remote aliases from the cache to re-populate this cache + // from the network responses. + if_can()->remote_aliases()->clear(); if (is_terminated()) { start_flow(STATE(rerun)); @@ -702,6 +705,33 @@ void IfCan::set_alias_allocator(AliasAllocator *a) aliasAllocator_.reset(a); } +void IfCan::send_global_alias_enquiry(Node *source) +{ + if (!source->is_initialized()) + { + LOG_ERROR("Tried to send global AME from not initialized node."); + return; + } + NodeAlias send_alias = local_aliases()->lookup(source->node_id()); + if (!send_alias) + { + LOG_ERROR("Tried to send global AME without a local alias."); + return; + } + { + auto *b = frame_write_flow()->alloc(); + CanDefs::control_init(*b->data(), send_alias, CanDefs::AME_FRAME, 0); + // Sends it out + frame_write_flow()->send(b); + } + { + // Sends another to the local node, but not using the local alias. + auto *b = frame_dispatcher()->alloc(); + CanDefs::control_init(*b->data(), 0, CanDefs::AME_FRAME, 0); + frame_dispatcher()->send(b); + } +} + void IfCan::add_addressed_message_support() { if (addressedWriteFlow_) @@ -718,7 +748,7 @@ void IfCan::delete_local_node(Node *node) { if (alias) { // The node had a local alias. localAliases_.remove(alias); - localAliases_.add(AliasCache::RESERVED_ALIAS_NODE_ID, alias); + localAliases_.add(CanDefs::get_reserved_alias_node_id(alias), alias); // Sends AMR & returns alias to pool. aliasAllocator_->return_alias(node->node_id(), alias); } diff --git a/src/openlcb/IfCan.cxxtest b/src/openlcb/IfCan.cxxtest index fc1d5af65..34c1a3b11 100644 --- a/src/openlcb/IfCan.cxxtest +++ b/src/openlcb/IfCan.cxxtest @@ -1,5 +1,6 @@ #include "utils/async_if_test_helper.hxx" +#include "openlcb/CanDefs.hxx" #include "openlcb/WriteHelper.hxx" namespace openlcb @@ -35,8 +36,6 @@ TEST_F(AsyncIfTest, InjectFrame) wait(); } -#define RX(args) run_x([this]() { args; }) - TEST_F(AsyncIfTest, InjectFrameAndExpectHandler) { StrictMock h; @@ -138,6 +137,30 @@ TEST_F(AsyncIfTest, AMESupport) wait(); } +TEST_F(AsyncNodeTest, GlobalAMESupport) +{ + EXPECT_TRUE(node_->is_initialized()); + RX({ + ifCan_->local_aliases()->add(UINT64_C(0x050101011877), 0x729U); + ifCan_->remote_aliases()->add(UINT64_C(0x050101011811), 0x111U); + EXPECT_EQ( + 0x111u, ifCan_->remote_aliases()->lookup(UINT64_C(0x050101011811))); + }); + // The enquiry is sent out... + expect_packet(":X1070222AN;"); + // ...along with all local aliases... + expect_packet(":X1070122AN02010D000003;"); + expect_packet(":X10701729N050101011877;"); + RX(ifCan_->send_global_alias_enquiry(node_)); + wait(); + // and the remote cache was cleared. + RX(EXPECT_EQ( + 0u, ifCan_->remote_aliases()->lookup(UINT64_C(0x050101011811)))); + // Checks regression: we did not lose the local node alias. + RX(EXPECT_EQ( + 0x22Au, ifCan_->local_aliases()->lookup(node_->node_id()))); +} + TEST_F(AsyncNodeTest, NodeIdLookupLocal) { NodeIdLookupFlow lflow(ifCan_.get()); @@ -348,7 +371,6 @@ TEST_F(AsyncMessageCanTests, WriteByMTIAllocatesLocalAlias) auto *b = ifCan_->global_message_write_flow()->alloc(); create_allocated_alias(); - expect_next_alias_allocation(); expect_packet(":X1070133AN02010D000004;"); expect_packet(":X195B433AN0102030405060708;"); b->data()->reset(Defs::MTI_EVENT_REPORT, TEST_NODE_ID + 1, @@ -397,7 +419,7 @@ TEST_F(AsyncMessageCanTests, AliasConflictCIDReply) TEST_F(AsyncMessageCanTests, ReservedAliasReclaimed) { - /** In this test we exercie the case when an alias that was previously + /** In this test we exercise the case when an alias that was previously * reserved by us but not used for any virtual node yet experiences various * conflicts. In the first case we see a regular CID conflict that gets * replied to. In the second case we see someone else actively using that @@ -405,33 +427,39 @@ TEST_F(AsyncMessageCanTests, ReservedAliasReclaimed) * detected at the time the next outgoing virtual node tries to allocate * that alias, and we'll test that it actually generates a new one * instead. */ - RX(ifCan_->local_aliases()->remove(NodeAlias(0x22A))); // resets the cache. + ifCan_->alias_allocator()->TEST_set_reserve_unused_alias_count(1); + RX(ifCan_->local_aliases()->clear()); // resets the cache. + // Sets up a regular alias for our standard node. + RX(ifCan_->alias_allocator()->add_allocated_alias(0x33A)); auto* b = ifCan_->global_message_write_flow()->alloc(); - create_allocated_alias(); - expect_next_alias_allocation(); expect_packet(":X1070133AN02010D000003;"); expect_packet(":X195B433AN0102030405060708;"); + LOG(INFO, "Next alias %03X", aliasSeed_); + // When we send out a packet from our node, a new alias will be grabbed. + expect_next_alias_allocation(); b->data()->reset(Defs::MTI_EVENT_REPORT, TEST_NODE_ID, eventid_to_buffer(UINT64_C(0x0102030405060708))); ifCan_->global_message_write_flow()->send(b); + wait(); + // Ticks down the time for the new alias to take hold. usleep(250000); wait(); - // Here we have the next reserved alias. - RX(EXPECT_EQ(AliasCache::RESERVED_ALIAS_NODE_ID, + // Checks that we have the next reserved alias. + RX(EXPECT_EQ(CanDefs::get_reserved_alias_node_id(0x44C), ifCan_->local_aliases()->lookup(NodeAlias(0x44C)))); - // A CID packet gets replied to. + // A CID packet on that alias gets replied to. send_packet_and_expect_response(":X1478944CN;", ":X1070044CN;"); // We still have it in the cache. - RX(EXPECT_EQ(AliasCache::RESERVED_ALIAS_NODE_ID, + RX(EXPECT_EQ(CanDefs::get_reserved_alias_node_id(0x44C), ifCan_->local_aliases()->lookup(NodeAlias(0x44C)))); // We kick it out with a regular frame. send_packet(":X1800044CN;"); wait(); RX(EXPECT_EQ(0U, ifCan_->local_aliases()->lookup(NodeAlias(0x44C)))); - // At this point we have an invalid alias in the reserved_aliases() - // queue. We check here that a new node gets a new alias. + // At this point we have no valid reserved alias in the cache. We check + // here that a new node gets a new alias. expect_next_alias_allocation(); // Unfortunately we have to guess the second next alias here because we // can't inject it. We can only inject one alias at a time, but now two @@ -444,12 +472,12 @@ TEST_F(AsyncMessageCanTests, ReservedAliasReclaimed) eventid_to_buffer(UINT64_C(0x0102030405060709))); b->set_done(get_notifiable()); ifCan_->global_message_write_flow()->send(b); - wait_for_notification(); + wait_for_notification(); // Will wait for one alias allocation to complete. RX(EXPECT_EQ( TEST_NODE_ID + 1, ifCan_->local_aliases()->lookup(NodeAlias(0x44D)))); - usleep(250000); - RX(EXPECT_EQ(AliasCache::RESERVED_ALIAS_NODE_ID, + usleep(250000); // Second alias allocation to complete. + RX(EXPECT_EQ(CanDefs::get_reserved_alias_node_id(0x6AA), ifCan_->local_aliases()->lookup(NodeAlias(0x6AA)))); } @@ -770,7 +798,6 @@ TEST_F(AsyncNodeTest, SendAddressedMessageFromNewNodeWithCachedAlias) // Simulate cache miss on local alias cache. RX(ifCan_->local_aliases()->remove(0x22A)); create_allocated_alias(); - expect_next_alias_allocation(); expect_packet(":X1070133AN02010D000003;"); // AMD for our new alias. // And the frame goes out. expect_packet(":X1948833AN0210050101FFFFDD;"); @@ -790,7 +817,6 @@ TEST_F(AsyncNodeTest, SendAddressedMessageFromNewNodeWithCacheMiss) // Simulate cache miss on local alias cache. RX(ifCan_->local_aliases()->remove(0x22A)); create_allocated_alias(); - expect_next_alias_allocation(); expect_packet(":X1070133AN02010D000003;"); // AMD for our new alias. // And the new alias will do the lookup. Not with an AME frame but straight // to the verify node id. @@ -817,7 +843,6 @@ TEST_F(AsyncNodeTest, SendAddressedMessageFromNewNodeWithCacheMissTimeout) // Simulate cache miss on local alias cache. RX(ifCan_->local_aliases()->remove(0x22A)); create_allocated_alias(); - expect_next_alias_allocation(); expect_packet(":X1070133AN02010D000003;"); // AMD for our new alias. // And the new alias will do the lookup. Not with an AME frame but straight // to the verify node id. diff --git a/src/openlcb/IfCan.hxx b/src/openlcb/IfCan.hxx index 484aa5be7..b5121483f 100644 --- a/src/openlcb/IfCan.hxx +++ b/src/openlcb/IfCan.hxx @@ -115,6 +115,15 @@ public: /// Sets the alias allocator for this If. Takes ownership of pointer. void set_alias_allocator(AliasAllocator *a); + /// Sends a global alias enquiry packet. This will also clear the remote + /// alias cache (in this node and in all other OpenMRN-based nodes on the + /// bus) and let it re-populate from the responses coming back. This call + /// should be used very sparingly because of the effect it has on the + /// entire bus. + /// @param source a local node to use for sending this message out. Must be + /// already initialized. + void send_global_alias_enquiry(Node *source); + void add_owned_flow(Executable *e) override; bool matching_node(NodeHandle expected, NodeHandle actual) override; diff --git a/src/openlcb/IfCanImpl.hxx b/src/openlcb/IfCanImpl.hxx index bd3129ca0..5d3455d4e 100644 --- a/src/openlcb/IfCanImpl.hxx +++ b/src/openlcb/IfCanImpl.hxx @@ -104,51 +104,17 @@ private: * @TODO(balazs.racz): implement proper local alias reclaim/reuse * mechanism. */ HASSERT(if_can()->alias_allocator()); - return allocate_and_call( - STATE(take_new_alias), - if_can()->alias_allocator()->reserved_aliases()); - } - - Action take_new_alias() - { - /* In this function we do a number of queries to the local alias - * cache. It is important that these queries show a consistent state; - * we do not need to hold any mutex though because only the current - * executor is allowed to access that object. */ - NodeAlias alias = 0; + NodeAlias alias = if_can()->alias_allocator()->get_allocated_alias( + nmsg()->src.id, this); + if (!alias) { - Buffer *new_alias = - full_allocation_result(if_can()->alias_allocator()); - HASSERT(new_alias->data()->alias); - alias = new_alias->data()->alias; - /* Sends the alias back for reallocating. This will trigger the - * alias allocator flow. */ - if (new_alias->data()->return_to_reallocation) - { - new_alias->data()->reset(); - if_can()->alias_allocator()->send(new_alias); - } - else - { - new_alias->unref(); - } + // wait for notification and re-try this step. + return wait(); } LOG(INFO, "Allocating new alias %03X for node %012" PRIx64, alias, nmsg()->src.id); - // Checks that there was no conflict on this alias. - if (if_can()->local_aliases()->lookup(alias) != - AliasCache::RESERVED_ALIAS_NODE_ID) - { - LOG(INFO, "Alias has seen conflict: %03X", alias); - // Problem. Let's take another alias. - return call_immediately(STATE(allocate_new_alias)); - } - srcAlias_ = alias; - /** @TODO(balazs.racz): We leak aliases here in case of eviction by the - * AliasCache object. */ - if_can()->local_aliases()->add(nmsg()->src.id, alias); // Take a CAN frame to send off the AMD frame. return allocate_and_call(if_can()->frame_write_flow(), STATE(send_amd_frame)); diff --git a/src/openlcb/IfImpl.cxxtest b/src/openlcb/IfImpl.cxxtest index 37ba84c1b..2f019c05f 100644 --- a/src/openlcb/IfImpl.cxxtest +++ b/src/openlcb/IfImpl.cxxtest @@ -14,7 +14,6 @@ public: TwoNodeTest() { create_allocated_alias(); - expect_next_alias_allocation(); expect_packet(":X1070133AN02010D000004;"); expect_packet(":X1910033AN02010D000004;"); secondNode_.reset( diff --git a/src/openlcb/MemoryConfigClient.hxx b/src/openlcb/MemoryConfigClient.hxx index f306fe40e..9df55f32b 100644 --- a/src/openlcb/MemoryConfigClient.hxx +++ b/src/openlcb/MemoryConfigClient.hxx @@ -83,7 +83,10 @@ struct MemoryConfigClientRequest : public CallableFlowRequestBase /// @param ReadCmd polymorphic matching arg; always set to READ. /// @param d is the destination node to query /// @param space is the memory space to read out - void reset(ReadCmd, NodeHandle d, uint8_t space) + /// @param cb if specified, will be called inline multiple times during the + /// processing as more data arrives. + void reset(ReadCmd, NodeHandle d, uint8_t space, + std::function cb = nullptr) { reset_base(); cmd = CMD_READ; @@ -92,6 +95,7 @@ struct MemoryConfigClientRequest : public CallableFlowRequestBase address = 0; size = 0xffffffffu; payload.clear(); + progressCb = std::move(cb); } /// Sets up a command to read a part of a memory space. @@ -201,6 +205,17 @@ struct MemoryConfigClientRequest : public CallableFlowRequestBase CMD_WRITE, CMD_META_REQUEST }; + + /// Helper function invoked at every other reset call. + void reset_base() + { + CallableFlowRequestBase::reset_base(); + progressCb = nullptr; + payload.clear(); + size = 0; + address = 0; + } + Command cmd; uint8_t memory_space; unsigned address; @@ -208,6 +223,8 @@ struct MemoryConfigClientRequest : public CallableFlowRequestBase /// Node to send the request to. NodeHandle dst; string payload; + /// Callback to execute as progress is being made. + std::function progressCb; }; class MemoryConfigClient : public CallableFlow @@ -364,6 +381,10 @@ private: unsigned dlen = len - ofs; request()->payload.append((char *)(bytes + ofs), dlen); offset_ += dlen; + if (request()->progressCb) + { + request()->progressCb(request()); + } if ((dlen < 64) || (request()->size == 0)) { return call_immediately(STATE(finish_read)); diff --git a/src/openlcb/NodeInitializeFlow.cxxtest b/src/openlcb/NodeInitializeFlow.cxxtest index a353beba5..4735f8261 100644 --- a/src/openlcb/NodeInitializeFlow.cxxtest +++ b/src/openlcb/NodeInitializeFlow.cxxtest @@ -36,7 +36,6 @@ TEST_F(AsyncIfTest, TwoNodesInitialize) wait(); expect_packet(":X1070133AN02010d000004;"); // AMD frame expect_packet(":X1910033AN02010d000004;"); // initialization complete - expect_next_alias_allocation(); DefaultNode node2(ifCan_.get(), TEST_NODE_ID + 1); wait(); } diff --git a/src/openlcb/NodeInitializeFlow.hxx b/src/openlcb/NodeInitializeFlow.hxx index dfda0ee6d..b62f39a84 100644 --- a/src/openlcb/NodeInitializeFlow.hxx +++ b/src/openlcb/NodeInitializeFlow.hxx @@ -80,6 +80,10 @@ private: Action entry() OVERRIDE { + if (!node()) + { + return release_and_exit(); + } HASSERT(message()->data()->node); return allocate_and_call( node()->iface()->global_message_write_flow(), diff --git a/src/openlcb/NodeRegistry.hxx b/src/openlcb/NodeRegistry.hxx new file mode 100644 index 000000000..868bbf9ef --- /dev/null +++ b/src/openlcb/NodeRegistry.hxx @@ -0,0 +1,64 @@ +/** \copyright + * Copyright (c) 2020, Balazs Racz + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * \file NodeRegistry.hxx + * + * Abstract class for data structures keyed by virtual nodes. + * + * @author Balazs Racz + * @date 12 Sep 2020 + */ + +#ifndef _OPENLCB_NODEREGISTRY_HXX_ +#define _OPENLCB_NODEREGISTRY_HXX_ + +#include "utils/Destructable.hxx" + +namespace openlcb +{ + +class Node; + +class NodeRegistry : public Destructable +{ +public: + /// Adds a node to the list of registered nodes. + /// @param node a virtual node. + virtual void register_node(openlcb::Node *node) = 0; + + /// Removes a node from the list of registered nodes. + /// @param node a virtual node. + virtual void unregister_node(openlcb::Node *node) = 0; + + /// Checks if a node is registered. + /// @param node a virtual node. + /// @return true if this node has been registered. + virtual bool is_node_registered(openlcb::Node *node) = 0; +}; + +} // namespace openlcb + +#endif // _OPENLCB_NODEREGISTRY_HXX_ diff --git a/src/openlcb/ProtocolIdentification.cxxtest b/src/openlcb/ProtocolIdentification.cxxtest index 83545d81f..6dbd1e995 100644 --- a/src/openlcb/ProtocolIdentification.cxxtest +++ b/src/openlcb/ProtocolIdentification.cxxtest @@ -67,7 +67,6 @@ TEST_F(ProtocolIdentificationTest, DoNotHandleTest) expect_packet(":X1070133AN02010d000004;"); // AMD frame expect_packet(":X1910033AN02010d000004;"); // initialization complete create_allocated_alias(); - expect_next_alias_allocation(); DefaultNode node2(ifCan_.get(), TEST_NODE_ID + 1); wait(); send_packet(":X198289FFN033A;"); diff --git a/src/openlcb/SNIPClient.cxxtest b/src/openlcb/SNIPClient.cxxtest new file mode 100644 index 000000000..c921df941 --- /dev/null +++ b/src/openlcb/SNIPClient.cxxtest @@ -0,0 +1,160 @@ +/** \copyright + * Copyright (c) 2020, Balazs Racz + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * \file SNIPClient.cxxtest + * + * Unit test for SNIP client library. + * + * @author Balazs Racz + * @date 18 Oct 2020 + */ + +static long long snipTimeout = 50 * 1000000; +#define SNIP_CLIENT_TIMEOUT_NSEC snipTimeout + +#include "openlcb/SNIPClient.hxx" + +#include "openlcb/SimpleNodeInfo.hxx" +#include "openlcb/SimpleNodeInfoMockUserFile.hxx" +#include "utils/async_if_test_helper.hxx" + +namespace openlcb +{ + +const char *const SNIP_DYNAMIC_FILENAME = MockSNIPUserFile::snip_user_file_path; + +const SimpleNodeStaticValues SNIP_STATIC_DATA = { + 4, "TestingTesting", "Undefined model", "Undefined HW version", "0.9"}; + +class SNIPClientTest : public AsyncNodeTest +{ +protected: + SNIPClientTest() + { + eb_.release_block(); + run_x([this]() { + ifTwo_.alias_allocator()->TEST_add_allocated_alias(0xFF2); + }); + wait(); + } + + ~SNIPClientTest() + { + wait(); + } + + MockSNIPUserFile userFile_ {"Undefined node name", "Undefined node descr"}; + + /// General flow for simple info requests. + SimpleInfoFlow infoFlow_ {ifCan_.get()}; + /// Handles SNIP requests. + SNIPHandler snipHandler_ {ifCan_.get(), node_, &infoFlow_}; + /// The actual client to test. + SNIPClient client_ {ifCan_.get()}; + + // These objects create a second node on the CAN bus (with its own + // interface). + BlockExecutor eb_ {&g_executor}; + static constexpr NodeID TWO_NODE_ID = 0x02010d0000ddULL; + + IfCan ifTwo_ {&g_executor, &can_hub0, local_alias_cache_size, + remote_alias_cache_size, local_node_count}; + AddAliasAllocator alloc_ {TWO_NODE_ID, &ifTwo_}; + DefaultNode nodeTwo_ {&ifTwo_, TWO_NODE_ID}; +}; + +TEST_F(SNIPClientTest, create) +{ +} + +static const char kExpectedData[] = + "\x04TestingTesting\0Undefined model\0Undefined HW version\0" + "0.9\0" + "\x02Undefined node name\0Undefined node descr"; // C adds another \0. + +TEST_F(SNIPClientTest, localhost) +{ + auto b = invoke_flow(&client_, node_, NodeHandle(node_->node_id())); + EXPECT_EQ(0, b->data()->resultCode); + EXPECT_EQ( + string(kExpectedData, sizeof(kExpectedData)), b->data()->response); + + // do another request. + auto bb = invoke_flow(&client_, node_, NodeHandle(node_->node_id())); + EXPECT_EQ(0, bb->data()->resultCode); + EXPECT_EQ( + string(kExpectedData, sizeof(kExpectedData)), bb->data()->response); +} + +TEST_F(SNIPClientTest, remote) +{ + auto b = invoke_flow(&client_, &nodeTwo_, NodeHandle(node_->node_id())); + EXPECT_EQ(0, b->data()->resultCode); + EXPECT_EQ( + string(kExpectedData, sizeof(kExpectedData)), b->data()->response); + + // do another request. + auto bb = invoke_flow(&client_, &nodeTwo_, NodeHandle(node_->node_id())); + EXPECT_EQ(0, bb->data()->resultCode); + EXPECT_EQ( + string(kExpectedData, sizeof(kExpectedData)), bb->data()->response); +} + +TEST_F(SNIPClientTest, timeout) +{ + long long start = os_get_time_monotonic(); + auto b = invoke_flow(&client_, &nodeTwo_, NodeHandle(nodeTwo_.node_id())); + EXPECT_EQ(SNIPClientRequest::OPENMRN_TIMEOUT, b->data()->resultCode); + EXPECT_EQ(0u, b->data()->response.size()); + long long time = os_get_time_monotonic() - start; + EXPECT_LT(MSEC_TO_NSEC(49), time); +} + +TEST_F(SNIPClientTest, reject) +{ + SyncNotifiable n; + auto b = get_buffer_deleter(client_.alloc()); + b->data()->reset(node_, NodeHandle(NodeAlias(0x555))); + b->data()->done.reset(&n); + + expect_packet(":X19DE822AN0555;"); + client_.send(b.get()); + wait(); + clear_expect(true); + EXPECT_EQ(SNIPClientRequest::OPERATION_PENDING, b->data()->resultCode); + + send_packet(":X19068555N022A209905EB;"); + wait(); + EXPECT_EQ(SNIPClientRequest::OPERATION_PENDING, b->data()->resultCode); + + send_packet(":X19068555N022A20990DE8;"); + wait(); + EXPECT_EQ(SNIPClientRequest::ERROR_REJECTED | 0x2099, b->data()->resultCode); + n.wait_for_notification(); + +} + +} // namespace openlcb diff --git a/src/openlcb/SNIPClient.hxx b/src/openlcb/SNIPClient.hxx new file mode 100644 index 000000000..4d87ab99c --- /dev/null +++ b/src/openlcb/SNIPClient.hxx @@ -0,0 +1,198 @@ +/** \copyright + * Copyright (c) 2020, Balazs Racz + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * \file SNIPClient.hxx + * + * A client library for talking to an arbitrary openlcb Node and ask it for the + * Simple Node Ident Info data. + * + * @author Balazs Racz + * @date 18 Oct 2020 + */ + +#ifndef _OPENLCB_SNIPCLIENT_HXX_ +#define _OPENLCB_SNIPCLIENT_HXX_ + +#include "executor/CallableFlow.hxx" +#include "openlcb/Defs.hxx" +#include "openlcb/If.hxx" + +namespace openlcb +{ + +/// Buffer contents for invoking the SNIP client. +struct SNIPClientRequest : public CallableFlowRequestBase +{ + /// Helper function for invoke_subflow. + /// @param src the openlcb node to call from + /// @param dst the openlcb node to target + void reset(Node *src, NodeHandle dst) + { + reset_base(); + resultCode = OPERATION_PENDING; + src_ = src; + dst_ = dst; + } + + enum + { + OPERATION_PENDING = 0x20000, //< cleared when done is called. + ERROR_REJECTED = 0x200000, //< Target node has rejected the request. + OPENMRN_TIMEOUT = 0x80000, //< Timeout waiting for ack/nack. + }; + + /// Source node where to send the request from. + Node *src_; + /// Destination node to query. + NodeHandle dst_; + /// Response payload if successful. + Payload response; +}; + +#if !defined(GTEST) || !defined(SNIP_CLIENT_TIMEOUT_NSEC) +/// Specifies how long to wait for a SNIP request to get a response. Writable +/// for unittesting purposes. +static constexpr long long SNIP_CLIENT_TIMEOUT_NSEC = MSEC_TO_NSEC(1500); +#endif + +class SNIPClient : public CallableFlow +{ +public: + /// Constructor. + /// @param s service of the openlcb executor. + SNIPClient(Service *s) + : CallableFlow(s) + { + } + + Action entry() override + { + request()->resultCode = SNIPClientRequest::OPERATION_PENDING; + return allocate_and_call( + iface()->addressed_message_write_flow(), STATE(write_request)); + } + +private: + enum + { + MTI_1a = Defs::MTI_TERMINATE_DUE_TO_ERROR, + MTI_1b = Defs::MTI_OPTIONAL_INTERACTION_REJECTED, + MASK_1 = ~(MTI_1a ^ MTI_1b), + MTI_1 = MTI_1a, + + MTI_2 = Defs::MTI_IDENT_INFO_REPLY, + MASK_2 = Defs::MTI_EXACT, + }; + + /// Called once the allocation is complete. Sends out the SNIP request to + /// the bus. + Action write_request() + { + auto *b = + get_allocation_result(iface()->addressed_message_write_flow()); + b->data()->reset(Defs::MTI_IDENT_INFO_REQUEST, + request()->src_->node_id(), request()->dst_, EMPTY_PAYLOAD); + + iface()->dispatcher()->register_handler( + &responseHandler_, MTI_1, MASK_1); + iface()->dispatcher()->register_handler( + &responseHandler_, MTI_2, MASK_2); + + iface()->addressed_message_write_flow()->send(b); + + return sleep_and_call( + &timer_, SNIP_CLIENT_TIMEOUT_NSEC, STATE(response_came)); + } + + /// Callback from the response handler. + /// @param message the incoming response message from the bus + void handle_response(Buffer *message) + { + auto rb = get_buffer_deleter(message); + if (request()->src_ != message->data()->dstNode || + !iface()->matching_node(request()->dst_, message->data()->src)) + { + // Not from the right place. + return; + } + if (message->data()->mti == Defs::MTI_OPTIONAL_INTERACTION_REJECTED || + message->data()->mti == Defs::MTI_TERMINATE_DUE_TO_ERROR) + { + uint16_t mti, error_code; + buffer_to_error( + message->data()->payload, &error_code, &mti, nullptr); + LOG(INFO, "rejection err %04x mti %04x", error_code, mti); + if (mti && mti != Defs::MTI_IDENT_INFO_REQUEST) + { + // Got error response for a different interaction. Ignore. + return; + } + request()->resultCode = + error_code | SNIPClientRequest::ERROR_REJECTED; + } + else if (message->data()->mti == Defs::MTI_IDENT_INFO_REPLY) + { + request()->response = std::move(message->data()->payload); + request()->resultCode = 0; + } + else + { + // Dunno what this MTI is. Ignore. + LOG(INFO, "Unexpected MTI for SNIP response handler: %04x", + message->data()->mti); + return; + } + // Wakes up parent flow. + request()->resultCode &= ~SNIPClientRequest::OPERATION_PENDING; + timer_.trigger(); + } + + Action response_came() + { + iface()->dispatcher()->unregister_handler_all(&responseHandler_); + if (request()->resultCode & SNIPClientRequest::OPERATION_PENDING) + { + return return_with_error(SNIPClientRequest::OPENMRN_TIMEOUT); + } + return return_with_error(request()->resultCode); + } + + /// @return openlcb source interface. + If *iface() + { + return request()->src_->iface(); + } + + /// Handles the timeout feature. + StateFlowTimer timer_ {this}; + /// Registered handler for response messages. + IncomingMessageStateFlow::GenericHandler responseHandler_ { + this, &SNIPClient::handle_response}; +}; + +} // namespace openlcb + +#endif // _OPENLCB_SNIPCLIENT_HXX_ diff --git a/src/openlcb/SimpleInfoProtocol.hxx b/src/openlcb/SimpleInfoProtocol.hxx index 272447061..b8def4bb2 100644 --- a/src/openlcb/SimpleInfoProtocol.hxx +++ b/src/openlcb/SimpleInfoProtocol.hxx @@ -38,6 +38,7 @@ #include #include +#include "openmrn_features.h" #include "openlcb/If.hxx" #include "executor/StateFlow.hxx" @@ -217,7 +218,7 @@ private: } break; } -#if (!defined(ARDUINO)) || defined(ESP32) +#if OPENMRN_HAVE_POSIX_FD case SimpleInfoDescriptor::FILE_CHAR_ARRAY: open_and_seek_next_file(); // fall through @@ -227,7 +228,7 @@ private: currentLength_ = d.arg; HASSERT(currentLength_); break; -#if (!defined(ARDUINO)) || defined(ESP32) +#if OPENMRN_HAVE_POSIX_FD case SimpleInfoDescriptor::FILE_LITERAL_BYTE: { open_and_seek_next_file(); @@ -240,7 +241,7 @@ private: byteOffset_ = 0; break; } -#endif // NOT ARDUINO, YES ESP32 +#endif // if have fd default: currentLength_ = 0; } diff --git a/src/openlcb/SimpleNodeInfo.cxx b/src/openlcb/SimpleNodeInfo.cxx index 35766f2ca..eb627f748 100644 --- a/src/openlcb/SimpleNodeInfo.cxx +++ b/src/openlcb/SimpleNodeInfo.cxx @@ -34,6 +34,9 @@ #include "openlcb/SimpleNodeInfo.hxx" +#include "openmrn_features.h" +#include "utils/format_utils.hxx" + namespace openlcb { @@ -46,15 +49,15 @@ const SimpleInfoDescriptor SNIPHandler::SNIP_RESPONSE[] = { {SimpleInfoDescriptor::C_STRING, 0, 0, SNIP_STATIC_DATA.model_name}, {SimpleInfoDescriptor::C_STRING, 0, 0, SNIP_STATIC_DATA.hardware_version}, {SimpleInfoDescriptor::C_STRING, 0, 0, SNIP_STATIC_DATA.software_version}, -#if defined(ARDUINO) && (!defined(ESP32)) +#if OPENMRN_HAVE_POSIX_FD + {SimpleInfoDescriptor::FILE_LITERAL_BYTE, 2, 0, SNIP_DYNAMIC_FILENAME}, + {SimpleInfoDescriptor::FILE_C_STRING, 63, 1, SNIP_DYNAMIC_FILENAME}, + {SimpleInfoDescriptor::FILE_C_STRING, 64, 64, SNIP_DYNAMIC_FILENAME}, +#else /// @todo(balazs.racz) Add eeprom support to arduino. {SimpleInfoDescriptor::LITERAL_BYTE, 2, 0, nullptr}, {SimpleInfoDescriptor::LITERAL_BYTE, 0, 0, nullptr}, {SimpleInfoDescriptor::LITERAL_BYTE, 0, 0, nullptr}, -#else - {SimpleInfoDescriptor::FILE_LITERAL_BYTE, 2, 0, SNIP_DYNAMIC_FILENAME}, - {SimpleInfoDescriptor::FILE_C_STRING, 63, 1, SNIP_DYNAMIC_FILENAME}, - {SimpleInfoDescriptor::FILE_C_STRING, 64, 64, SNIP_DYNAMIC_FILENAME}, #endif {SimpleInfoDescriptor::END_OF_DATA, 0, 0, 0}}; @@ -65,9 +68,8 @@ void init_snip_user_file(int fd, const char *user_name, SimpleNodeDynamicValues data; memset(&data, 0, sizeof(data)); data.version = 2; - strncpy(data.user_name, user_name, sizeof(data.user_name)); - strncpy(data.user_description, user_description, - sizeof(data.user_description)); + str_populate(data.user_name, user_name); + str_populate(data.user_description, user_description); int ofs = 0; auto *p = (const uint8_t *)&data; const int len = sizeof(data); diff --git a/src/openlcb/SimpleNodeInfoMockUserFile.cxx b/src/openlcb/SimpleNodeInfoMockUserFile.cxx index f8a48b951..32c11e537 100644 --- a/src/openlcb/SimpleNodeInfoMockUserFile.cxx +++ b/src/openlcb/SimpleNodeInfoMockUserFile.cxx @@ -37,7 +37,9 @@ #define _POSIX_C_SOURCE 200112L #endif -#include "SimpleNodeInfoMockUserFile.hxx" +#include "openlcb/SimpleNodeInfoMockUserFile.hxx" + +#include "utils/format_utils.hxx" #ifdef __FreeRTOS__ openlcb::MockSNIPUserFile::MockSNIPUserFile(const char *user_name, @@ -45,9 +47,8 @@ openlcb::MockSNIPUserFile::MockSNIPUserFile(const char *user_name, : snipData_{2} , userFile_(MockSNIPUserFile::snip_user_file_path, &snipData_, false) { - strncpy(snipData_.user_name, user_name, sizeof(snipData_.user_name)); - strncpy(snipData_.user_description, user_description, - sizeof(snipData_.user_description)); + str_populate(snipData_.user_name, user_name); + str_populate(snipData_.user_description, user_description); } openlcb::MockSNIPUserFile::~MockSNIPUserFile() @@ -63,8 +64,7 @@ openlcb::MockSNIPUserFile::MockSNIPUserFile(const char *user_name, { init_snip_user_file(userFile_.fd(), user_name, user_description); HASSERT(userFile_.name().size() < sizeof(snip_user_file_path)); - strncpy(snip_user_file_path, userFile_.name().c_str(), - sizeof(snip_user_file_path)); + str_populate(snip_user_file_path, userFile_.name().c_str()); } char openlcb::MockSNIPUserFile::snip_user_file_path[128] = "/dev/zero"; diff --git a/src/openlcb/SimpleStack.cxx b/src/openlcb/SimpleStack.cxx index 49890ac99..dd0214252 100644 --- a/src/openlcb/SimpleStack.cxx +++ b/src/openlcb/SimpleStack.cxx @@ -41,11 +41,6 @@ #include #include /* tc* functions */ #endif -#if defined(__linux__) -#include "utils/HubDeviceSelect.hxx" -#include -#include -#endif #include #include @@ -56,6 +51,9 @@ #include "openlcb/EventHandler.hxx" #include "openlcb/NodeInitializeFlow.hxx" #include "openlcb/SimpleNodeInfo.hxx" +#include "openmrn_features.h" +#include "utils/HubDeviceSelect.hxx" +#include "utils/SocketCan.hxx" namespace openlcb { @@ -103,12 +101,12 @@ SimpleTcpStack::SimpleTcpStack(const openlcb::NodeID node_id) void SimpleStackBase::start_stack(bool delay_start) { -#if (!defined(ARDUINO)) || defined(ESP32) +#if OPENMRN_HAVE_POSIX_FD // Opens the eeprom file and sends configuration update commands to all // listeners. configUpdateFlow_.open_file(CONFIG_FILENAME); configUpdateFlow_.init_flow(); -#endif // NOT ARDUINO, YES ESP32 +#endif // have posix fd if (!delay_start) { @@ -138,7 +136,7 @@ void SimpleStackBase::default_start_node() node(), MemoryConfigDefs::SPACE_ACDI_SYS, space); additionalComponents_.emplace_back(space); } -#if (!defined(ARDUINO)) || defined(ESP32) +#if OPENMRN_HAVE_POSIX_FD { auto *space = new FileMemorySpace( SNIP_DYNAMIC_FILENAME, sizeof(SimpleNodeDynamicValues)); @@ -156,7 +154,7 @@ void SimpleStackBase::default_start_node() node(), MemoryConfigDefs::SPACE_CDI, space); additionalComponents_.emplace_back(space); } -#if (!defined(ARDUINO)) || defined(ESP32) +#if OPENMRN_HAVE_POSIX_FD if (CONFIG_FILENAME != nullptr) { auto *space = new FileMemorySpace(CONFIG_FILENAME, CONFIG_FILE_SIZE); @@ -201,16 +199,6 @@ void SimpleCanStackBase::start_iface(bool restart) if_can()->alias_allocator()->reinit_seed(); if_can()->local_aliases()->clear(); if_can()->remote_aliases()->clear(); - // Deletes all reserved aliases from the queue. - while (!if_can()->alias_allocator()->reserved_aliases()->empty()) - { - Buffer *a = static_cast *>( - if_can()->alias_allocator()->reserved_aliases()->next().item); - if (a) - { - a->unref(); - } - } } // Bootstraps the fresh alias allocation process. @@ -418,35 +406,12 @@ void SimpleCanStackBase::add_gridconnect_tty( void SimpleCanStackBase::add_socketcan_port_select( const char *device, int loopback) { - int s; - struct sockaddr_can addr; - struct ifreq ifr; - - s = socket(PF_CAN, SOCK_RAW, CAN_RAW); - - // Set the blocking limit to the minimum allowed, typically 1024 in Linux - int sndbuf = 0; - setsockopt(s, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf)); - - // turn on/off loopback - setsockopt(s, SOL_CAN_RAW, CAN_RAW_LOOPBACK, &loopback, sizeof(loopback)); - - // setup error notifications - can_err_mask_t err_mask = CAN_ERR_TX_TIMEOUT | CAN_ERR_LOSTARB | - CAN_ERR_CRTL | CAN_ERR_PROT | CAN_ERR_TRX | CAN_ERR_ACK | - CAN_ERR_BUSOFF | CAN_ERR_BUSERROR | CAN_ERR_RESTARTED; - setsockopt(s, SOL_CAN_RAW, CAN_RAW_ERR_FILTER, &err_mask, sizeof(err_mask)); - strcpy(ifr.ifr_name, device); - - ::ioctl(s, SIOCGIFINDEX, &ifr); - - addr.can_family = AF_CAN; - addr.can_ifindex = ifr.ifr_ifindex; - - bind(s, (struct sockaddr *)&addr, sizeof(addr)); - - auto *port = new HubDeviceSelect(can_hub(), s); - additionalComponents_.emplace_back(port); + int s = socketcan_open(device, loopback); + if (s >= 0) + { + auto *port = new HubDeviceSelect(can_hub(), s); + additionalComponents_.emplace_back(port); + } } #endif extern Pool *const __attribute__((__weak__)) g_incoming_datagram_allocator = diff --git a/src/openlcb/SimpleStack.hxx b/src/openlcb/SimpleStack.hxx index 5adb6a2b5..fe37aa76e 100644 --- a/src/openlcb/SimpleStack.hxx +++ b/src/openlcb/SimpleStack.hxx @@ -54,6 +54,7 @@ #include "openlcb/SimpleNodeInfo.hxx" #include "openlcb/TractionTrain.hxx" #include "openlcb/TrainInterface.hxx" +#include "utils/ActivityLed.hxx" #include "utils/GcTcpHub.hxx" #include "utils/GridConnectHub.hxx" #include "utils/HubDevice.hxx" @@ -153,6 +154,17 @@ public: return &configUpdateFlow_; } + /// Adds an activiy LED which will be flashed every time a message is sent + /// from this node to the network. + /// @param gpio LED that will be flashed on for each packet. + /// @param period defines in nanosecond the time to spend between updates. + void set_tx_activity_led( + const Gpio *led, long long period = MSEC_TO_NSEC(33)) + { + auto *al = new ActivityLed(iface(), led, period); + iface()->set_tx_hook(std::bind(&ActivityLed::activity, al)); + } + /// Reinitializes the node. Useful to call after the connection has flapped /// (gone down and up). void restart_stack(); @@ -371,8 +383,15 @@ public: { /// @TODO (balazs.racz) make this more efficient by rendering to string /// only once for all connections. - /// @TODO (balazs.racz) do not leak this. - new GcTcpHub(can_hub(), port); + gcHubServer_.reset(new GcTcpHub(can_hub(), port)); + } + + /// Retrieve the instance of the GridConnect Hub server, which was started + /// with start_tcp_hub_server(). + /// @return the TCP hub server, or nullptr if no server was ever started. + GcTcpHub *get_tcp_hub_server() + { + return gcHubServer_.get(); } /// Connects to a CAN hub using TCP with the gridconnect protocol. @@ -461,6 +480,9 @@ private: /// the CAN interface to function. Will be called exactly once by the /// constructor of the base class. std::unique_ptr create_if(const openlcb::NodeID node_id); + + /// Holds the ownership of the TCP hub server (if one was created). + std::unique_ptr gcHubServer_; }; class SimpleTcpStackBase : public SimpleStackBase diff --git a/src/openlcb/TractionConsist.cxxtest b/src/openlcb/TractionConsist.cxxtest index 92c764435..06ad096a3 100644 --- a/src/openlcb/TractionConsist.cxxtest +++ b/src/openlcb/TractionConsist.cxxtest @@ -1,60 +1,118 @@ #include "utils/async_traction_test_helper.hxx" -#include "openlcb/TractionTrain.hxx" #include "openlcb/TractionTestTrain.hxx" #include "openlcb/TractionThrottle.hxx" +#include "openlcb/TractionTrain.hxx" namespace openlcb { -static constexpr NodeID nodeIdLead = 0x060100000000 | 1371; -static constexpr NodeID nodeIdC1 = 0x060100000000 | 1372; -static constexpr NodeID nodeIdC2 = 0x060100000000 | 1373; +static constexpr NodeID nodeIdLead = 0x060100000000 | 1370; +static constexpr NodeID nodeIdC1 = 0x060100000000 | 1371; +static constexpr NodeID nodeIdC2 = 0x060100000000 | 1372; +static constexpr NodeID nodeIdC3 = 0x060100000000 | 1373; +static constexpr NodeID nodeIdC4 = 0x060100000000 | 1374; +static constexpr NodeID nodeIdC5 = 0x060100000000 | 1375; + +class TrainNodeWithMockPolicy : public TrainNodeForProxy +{ +public: + INHERIT_CONSTRUCTOR(TrainNodeWithMockPolicy, TrainNodeForProxy); + + MOCK_METHOD5(function_policy, + bool(NodeHandle src, uint8_t command_byte, uint32_t fnum, + uint16_t value, Notifiable *done)); +}; -class ConsistTest : public TractionTest { +class ConsistTest : public TractionTest +{ protected: - ConsistTest() { - create_allocated_alias(); + ConsistTest() + { run_x([this]() { - otherIf_.local_aliases()->add(nodeIdLead, 0x771); - otherIf_.local_aliases()->add(nodeIdC1, 0x772); - otherIf_.local_aliases()->add(nodeIdC2, 0x773); + otherIf_.local_aliases()->add(nodeIdLead, 0x770); + otherIf_.local_aliases()->add(nodeIdC1, 0x771); + otherIf_.local_aliases()->add(nodeIdC2, 0x772); + otherIf_.local_aliases()->add(nodeIdC3, 0x773); + otherIf_.remote_aliases()->add(nodeIdC4, 0x774); + otherIf_.remote_aliases()->add(nodeIdC5, 0x775); }); - nodeLead_.reset(new TrainNodeForProxy(&trainService_, &trainLead_)); - nodeC1_.reset(new TrainNodeForProxy(&trainService_, &trainC1_)); - nodeC2_.reset(new TrainNodeForProxy(&trainService_, &trainC2_)); + nodeLead_.reset(new StrictMock( + &trainService_, &trainLead_)); + nodeC1_.reset( + new StrictMock(&trainService_, &trainC1_)); + nodeC2_.reset( + new StrictMock(&trainService_, &trainC2_)); + nodeC3_.reset( + new StrictMock(&trainService_, &trainC3_)); + wait(); + auto b = invoke_flow(&throttle_, TractionThrottleCommands::ASSIGN_TRAIN, + nodeIdLead, false); + EXPECT_EQ(0, b->data()->resultCode); wait(); } - TractionThrottle throttle_{node_}; + /// Sets up a star shaped consist. + void create_consist() + { + auto b = invoke_flow( + &throttle_, TractionThrottleCommands::CONSIST_ADD, nodeIdC1, 0); + ASSERT_EQ(0, b->data()->resultCode); + b = invoke_flow(&throttle_, TractionThrottleCommands::CONSIST_ADD, + nodeIdC2, + TractionDefs::CNSTFLAGS_REVERSE | TractionDefs::CNSTFLAGS_LINKF0); + ASSERT_EQ(0, b->data()->resultCode); + b = invoke_flow(&throttle_, TractionThrottleCommands::CONSIST_ADD, + nodeIdC3, + TractionDefs::CNSTFLAGS_REVERSE | TractionDefs::CNSTFLAGS_LINKF0 | + TractionDefs::CNSTFLAGS_LINKFN); + ASSERT_EQ(0, b->data()->resultCode); + // reverse link + nodeC3_->add_consist(nodeIdLead, + TractionDefs::CNSTFLAGS_REVERSE | TractionDefs::CNSTFLAGS_LINKF0 | + TractionDefs::CNSTFLAGS_LINKFN); + wait(); + } - IfCan otherIf_{&g_executor, &can_hub0, 5, 5, 5}; - TrainService trainService_{&otherIf_}; + void inject_default_true_policy(TrainNodeWithMockPolicy *tn) + { + EXPECT_CALL(*tn, function_policy(_, _, _, _, _)) + .WillRepeatedly( + DoAll(WithArg<4>(Invoke(&InvokeNotification)), Return(true))); + } - LoggingTrain trainLead_{1371}; - LoggingTrain trainC1_{1372}; - LoggingTrain trainC2_{1373}; - std::unique_ptr nodeLead_; - std::unique_ptr nodeC1_; - std::unique_ptr nodeC2_; -}; + /// Sets up a default "true" function policy for all train nodes. + void inject_default_policy() + { + inject_default_true_policy(nodeLead_.get()); + inject_default_true_policy(nodeC1_.get()); + inject_default_true_policy(nodeC2_.get()); + inject_default_true_policy(nodeC3_.get()); + } -TEST_F(ConsistTest, CreateDestroy) { -} + TractionThrottle throttle_ {node_}; -TEST_F(ConsistTest, CreateAndRunConsist) { - auto b = invoke_flow( - &throttle_, TractionThrottleCommands::ASSIGN_TRAIN, nodeIdLead, false); - ASSERT_EQ(0, b->data()->resultCode); - wait(); + IfCan otherIf_ {&g_executor, &can_hub0, 5, 5, 5}; + TrainService trainService_ {&otherIf_}; - b = invoke_flow( - &throttle_, TractionThrottleCommands::CONSIST_ADD, nodeIdC1, 0); - ASSERT_EQ(0, b->data()->resultCode); - b = invoke_flow(&throttle_, TractionThrottleCommands::CONSIST_ADD, nodeIdC2, - TractionDefs::CNSTFLAGS_REVERSE | TractionDefs::CNSTFLAGS_LINKF0); - ASSERT_EQ(0, b->data()->resultCode); + LoggingTrain trainLead_ {1370}; + LoggingTrain trainC1_ {1371}; + LoggingTrain trainC2_ {1372}; + LoggingTrain trainC3_ {1373}; + std::unique_ptr nodeLead_; + std::unique_ptr nodeC1_; + std::unique_ptr nodeC2_; + std::unique_ptr nodeC3_; +}; +TEST_F(ConsistTest, CreateDestroy) +{ +} + +TEST_F(ConsistTest, CreateAndRunConsist) +{ + create_consist(); + inject_default_policy(); Velocity v; v.set_mph(37.5); throttle_.set_speed(v); @@ -78,9 +136,140 @@ TEST_F(ConsistTest, CreateAndRunConsist) { EXPECT_EQ(Velocity::REVERSE, trainLead_.get_speed().direction()); EXPECT_EQ(Velocity::REVERSE, trainC1_.get_speed().direction()); EXPECT_EQ(Velocity::FORWARD, trainC2_.get_speed().direction()); + + EXPECT_FALSE(trainLead_.get_fn(0)); + EXPECT_FALSE(trainLead_.get_fn(2)); + EXPECT_FALSE(trainC1_.get_fn(0)); + EXPECT_FALSE(trainC1_.get_fn(2)); + EXPECT_FALSE(trainC2_.get_fn(0)); + EXPECT_FALSE(trainC2_.get_fn(2)); + EXPECT_FALSE(trainC3_.get_fn(0)); + EXPECT_FALSE(trainC3_.get_fn(2)); + + throttle_.set_fn(0, 1); + wait(); + + // F0 forwarded to C2 and C3, not to C1. + EXPECT_TRUE(trainLead_.get_fn(0)); + EXPECT_FALSE(trainC1_.get_fn(0)); + EXPECT_TRUE(trainC2_.get_fn(0)); + EXPECT_TRUE(trainC3_.get_fn(0)); + + throttle_.set_fn(2, 1); + wait(); + + // F2 forwarded to C3, not to C1 and C2. + EXPECT_TRUE(trainLead_.get_fn(2)); + EXPECT_FALSE(trainC1_.get_fn(2)); + EXPECT_FALSE(trainC2_.get_fn(2)); + EXPECT_TRUE(trainC3_.get_fn(2)); } +TEST_F(ConsistTest, ListenerExpectations) +{ + inject_default_policy(); + auto b = + invoke_flow(&throttle_, TractionThrottleCommands::CONSIST_ADD, nodeIdC4, + TractionDefs::CNSTFLAGS_REVERSE | TractionDefs::CNSTFLAGS_LINKF0 | + TractionDefs::CNSTFLAGS_LINKFN); + ASSERT_EQ(0, b->data()->resultCode); + b = invoke_flow(&throttle_, TractionThrottleCommands::CONSIST_ADD, nodeIdC5, + TractionDefs::CNSTFLAGS_LINKF0 | TractionDefs::CNSTFLAGS_LINKFN); + ASSERT_EQ(0, b->data()->resultCode); + + clear_expect(true); + Velocity v; + v.set_mph(37.5); + + // Throttle to lead + expect_packet(":X195EB22AN0770004C31;"); + // Lead to follower with listening flag and reversed value + expect_packet(":X195EB770N077480CC31;"); + // Lead to follower with listening flag and regular value + expect_packet(":X195EB770N0775804C31;"); + throttle_.set_speed(v); + wait(); + + // Throttle to lead + expect_packet(":X195EB22AN0770010000020001;"); + // Lead to follower with listening flag + expect_packet(":X195EB770N0774810000020001;"); + // Lead to follower with listening flag + expect_packet(":X195EB770N0775810000020001;"); + throttle_.set_fn(2, 1); + wait(); +} + +TEST_F(ConsistTest, FunctionPolicy) +{ + create_consist(); + inject_default_true_policy(nodeLead_.get()); + inject_default_true_policy(nodeC1_.get()); + inject_default_true_policy(nodeC2_.get()); + + Notifiable *done = nullptr; + EXPECT_CALL(*nodeC3_, + function_policy(Field(&NodeHandle::id, nodeIdLead), 0x81, 3, 1, _)) + .WillOnce(DoAll(SaveArg<4>(&done), Return(true))); + throttle_.set_fn(3, 1); + wait(); + EXPECT_TRUE(trainLead_.get_fn(3)); + EXPECT_FALSE(trainC1_.get_fn(3)); + EXPECT_FALSE(trainC2_.get_fn(3)); + EXPECT_FALSE(trainC3_.get_fn(3)); + ASSERT_TRUE(done); + + EXPECT_CALL(*nodeC3_, + function_policy(Field(&NodeHandle::id, nodeIdLead), 0x81, 3, 1, _)) + .WillOnce(DoAll(WithArg<4>(Invoke(&InvokeNotification)), Return(true))); + + done->notify(); + wait(); + EXPECT_TRUE(trainC3_.get_fn(3)); + // rejected by policy + EXPECT_CALL(*nodeC3_, + function_policy(Field(&NodeHandle::id, nodeIdLead), 0x81, 4, 1, _)) + .WillOnce( + DoAll(WithArg<4>(Invoke(&InvokeNotification)), Return(false))); + + throttle_.set_fn(4, 1); + wait(); + EXPECT_TRUE(trainLead_.get_fn(4)); + EXPECT_FALSE(trainC1_.get_fn(4)); + EXPECT_FALSE(trainC2_.get_fn(4)); + EXPECT_FALSE(trainC3_.get_fn(4)); + + // Switch cab to C3. + auto b = invoke_flow( + &throttle_, TractionThrottleCommands::ASSIGN_TRAIN, nodeIdC3, false); + EXPECT_EQ(0, b->data()->resultCode); + + // Different policy now + EXPECT_CALL(*nodeC3_, + function_policy(Field(&NodeHandle::alias, 0x22A), 0x01, 5, 1, _)) + .WillOnce(DoAll(WithArg<4>(Invoke(&InvokeNotification)), Return(true))); + + throttle_.set_fn(5, 1); + wait(); + EXPECT_TRUE(trainLead_.get_fn(5)); + EXPECT_FALSE(trainC1_.get_fn(5)); + EXPECT_FALSE(trainC2_.get_fn(5)); + EXPECT_TRUE(trainC3_.get_fn(5)); + + // Local policy false should not prevent remote propagation. + EXPECT_CALL(*nodeC3_, + function_policy(Field(&NodeHandle::alias, 0x22A), 0x01, 0, 1, _)) + .WillOnce( + DoAll(WithArg<4>(Invoke(&InvokeNotification)), Return(false))); + + throttle_.set_fn(0, 1); + wait(); + EXPECT_TRUE(trainLead_.get_fn(0)); + EXPECT_FALSE(trainC1_.get_fn(0)); // no link F0 + EXPECT_TRUE(trainC2_.get_fn(0)); + EXPECT_FALSE(trainC3_.get_fn(0)); // no policy +} } // namespace openlcb diff --git a/src/openlcb/TractionDefs.hxx b/src/openlcb/TractionDefs.hxx index 0d05b9f5a..389a984f4 100644 --- a/src/openlcb/TractionDefs.hxx +++ b/src/openlcb/TractionDefs.hxx @@ -107,6 +107,12 @@ struct TractionDefs { REQ_CONSIST_CONFIG = 0x30, REQ_TRACTION_MGMT = 0x40, + /// Mask to apply to the command byte of the requests. + REQ_MASK = 0x7F, + /// Flag bit in the command byte set when a listener command is + /// forwarded. + REQ_LISTENER = 0x80, + // Byte 1 of REQ_CONTROLLER_CONFIG command CTRLREQ_ASSIGN_CONTROLLER = 0x01, CTRLREQ_RELEASE_CONTROLLER = 0x02, diff --git a/src/openlcb/TractionTestTrain.cxxtest b/src/openlcb/TractionTestTrain.cxxtest index e44566efc..d8a1db91d 100644 --- a/src/openlcb/TractionTestTrain.cxxtest +++ b/src/openlcb/TractionTestTrain.cxxtest @@ -44,7 +44,6 @@ protected: LoggingTrainTest() : trainImpl_(1732) { create_allocated_alias(); - expect_next_alias_allocation(); // alias reservation expect_packet(":X1070133AN0601000006C4;"); // initialized diff --git a/src/openlcb/TractionThrottle.cxxtest b/src/openlcb/TractionThrottle.cxxtest index 01695e960..8f4f66de4 100644 --- a/src/openlcb/TractionThrottle.cxxtest +++ b/src/openlcb/TractionThrottle.cxxtest @@ -15,7 +15,6 @@ protected: ThrottleTest() { print_all_packets(); - create_allocated_alias(); run_x( [this]() { otherIf_.local_aliases()->add(TRAIN_NODE_ID, 0x771); }); trainNode_.reset(new TrainNodeForProxy(&trainService_, &trainImpl_)); diff --git a/src/openlcb/TractionThrottle.hxx b/src/openlcb/TractionThrottle.hxx index cb7a400be..c06c44b98 100644 --- a/src/openlcb/TractionThrottle.hxx +++ b/src/openlcb/TractionThrottle.hxx @@ -174,8 +174,16 @@ class TractionThrottleInterface : public openlcb::TrainImpl { public: + /// Flips a function on<>off. virtual void toggle_fn(uint32_t fn) = 0; + /// Sends a query for a function to the server. The response will be + /// asynchronously reported by the throttle listener update callback. + /// @param fn function number. + virtual void query_fn(uint32_t fn) + { + } + /// Determine if a train is currently assigned to this trottle. /// @return true if a train is assigned, else false virtual bool is_train_assigned() = 0; @@ -295,7 +303,15 @@ public: } set_fn(fn, fnstate); } - + + /// Sends out a function query command. The throttle listener will be + /// called when the response is available. + /// @param address function to query. + void query_fn(uint32_t address) override + { + send_traction_message(TractionDefs::fn_get_payload(address)); + } + uint32_t legacy_address() override { return 0; @@ -567,7 +583,9 @@ private: } } - void pending_reply_arrived() + /// Notifies that a pending query during load has gotten a reply. + /// @return true if we were in the load state. + bool pending_reply_arrived() { if (pendingQueries_ > 0) { @@ -575,12 +593,19 @@ private: { timer_.trigger(); } + return true; } + return false; } void speed_reply(Buffer *msg) { AutoReleaseBuffer rb(msg); + if (msg->data()->dstNode != node_) + { + // For a different throttle. + return; + } if (!iface()->matching_node(msg->data()->src, NodeHandle(dst_))) { return; @@ -592,13 +617,15 @@ private: { case TractionDefs::RESP_QUERY_SPEED: { - pending_reply_arrived(); + bool expected = pending_reply_arrived(); Velocity v; if (TractionDefs::speed_get_parse_last(p, &v)) { lastSetSpeed_ = v; - /// @todo (balazs.racz): call a callback for the client. - + if (updateCallback_ && !expected) + { + updateCallback_(-1); + } /// @todo (Stuart.Baker): Do we need to do anything with /// estopActive_? } @@ -606,12 +633,16 @@ private: } case TractionDefs::RESP_QUERY_FN: { - pending_reply_arrived(); + bool expected = pending_reply_arrived(); uint16_t v; unsigned num; if (TractionDefs::fn_get_parse(p, &v, &num)) { lastKnownFn_[num] = v; + if (updateCallback_ && !expected) + { + updateCallback_(num); + } } } } @@ -739,7 +770,7 @@ private: const Payload &p = msg->data()->payload; if (p.size() < 1) return; - switch (p[0]) + switch (p[0] & TractionDefs::REQ_MASK) { case TractionDefs::REQ_SET_SPEED: { diff --git a/src/openlcb/TractionTrain.cxx b/src/openlcb/TractionTrain.cxx index 0e2773b59..c2214dadd 100644 --- a/src/openlcb/TractionTrain.cxx +++ b/src/openlcb/TractionTrain.cxx @@ -42,7 +42,7 @@ namespace openlcb { -TrainNode::TrainNode(TrainService *service, TrainImpl *train) +DefaultTrainNode::DefaultTrainNode(TrainService *service, TrainImpl *train) : service_(service) , train_(train) , isInitialized_(0) @@ -52,22 +52,46 @@ TrainNode::TrainNode(TrainService *service, TrainImpl *train) TrainNode::~TrainNode() { - while (!consistSlaves_.empty()) { +} + +TrainNodeWithConsist::~TrainNodeWithConsist() +{ + while (!consistSlaves_.empty()) + { delete consistSlaves_.pop_front(); } } +DefaultTrainNode::~DefaultTrainNode() +{ +} + TrainNodeForProxy::TrainNodeForProxy(TrainService *service, TrainImpl *train) - : TrainNode(service, train) { - service_->register_train(this); + : DefaultTrainNode(service, train) +{ + service->register_train(this); +} + +TrainNodeForProxy::~TrainNodeForProxy() +{ + /// @todo enable this line of code. It currently breaks unit tests due to + /// bugs + // service_->unregister_train(this); } TrainNodeWithId::TrainNodeWithId( TrainService *service, TrainImpl *train, NodeID node_id) - : TrainNode(service, train) + : DefaultTrainNode(service, train) , nodeId_(node_id) { - service_->register_train(this); + service->register_train(this); +} + +TrainNodeWithId::~TrainNodeWithId() +{ + /// @todo enable this line of code. It currently breaks unit tests due to + /// bugs + // service_->unregister_train(this); } NodeID TrainNodeForProxy::node_id() @@ -76,7 +100,7 @@ NodeID TrainNodeForProxy::node_id() train_->legacy_address_type(), train_->legacy_address()); } -If *TrainNode::iface() +If *DefaultTrainNode::iface() { return service_->iface(); } @@ -150,8 +174,7 @@ struct TrainService::Impl return release_and_exit(); } // Checks if destination is a local traction-enabled node. - if (trainService_->nodes_.find(train_node()) == - trainService_->nodes_.end()) + if (!trainService_->nodes_->is_node_registered(train_node())) { LOG(VERBOSE, "Traction message for node %p that is not " "traction enabled.", @@ -167,7 +190,7 @@ struct TrainService::Impl LOG(VERBOSE, "Traction message with no command byte."); return reject_permanent(); } - uint8_t cmd = payload()[0]; + uint8_t cmd = payload()[0] & TractionDefs::REQ_MASK; switch (cmd) { /** @TODO(balazs.racz) need to validate caller of mutating @@ -190,7 +213,24 @@ struct TrainService::Impl uint16_t value = payload()[4]; value <<= 8; value |= payload()[5]; - train_node()->train()->set_fn(address, value); + bn_.reset(this); + bool should_apply = + train_node()->function_policy(nmsg()->src, payload()[0], + address, value, bn_.new_child()); + // The function_policy call may have completed inline. We + // can inquire from the barrier. If it was not completed + // inline, we have to wait for the notification and re-try + // the call. + if (!bn_.abort_if_almost_done()) + { + // Not notified inline. + bn_.notify(); // consumes our share + return wait(); + } + if (should_apply) + { + train_node()->train()->set_fn(address, value); + } nextConsistIndex_ = 0; return call_immediately(STATE(maybe_forward_consist)); } @@ -427,7 +467,7 @@ struct TrainService::Impl ++nextConsistIndex_; return again(); } - uint8_t cmd = payload()[0]; + uint8_t cmd = payload()[0] & TractionDefs::REQ_MASK; bool flip_speed = false; if (cmd == TractionDefs::REQ_SET_SPEED) { if (flags & TractionDefs::CNSTFLAGS_REVERSE) { @@ -463,6 +503,7 @@ struct TrainService::Impl if (flip_speed) { b->data()->payload[1] ^= 0x80; } + b->data()->payload[0] |= TractionDefs::REQ_LISTENER; iface()->addressed_message_write_flow()->send(b); return exit(); } @@ -489,8 +530,11 @@ struct TrainService::Impl } b->data()->reset(message()->data()->mti, train_node()->node_id(), NodeHandle(dst), message()->data()->payload); - if ((payload()[0] == TractionDefs::REQ_SET_SPEED) && - (flags & TractionDefs::CNSTFLAGS_REVERSE)) { + b->data()->payload[0] |= TractionDefs::REQ_LISTENER; + if (((payload()[0] & TractionDefs::REQ_MASK) == + TractionDefs::REQ_SET_SPEED) && + (flags & TractionDefs::CNSTFLAGS_REVERSE)) + { b->data()->payload[1] ^= 0x80; } iface()->addressed_message_write_flow()->send(b); @@ -600,14 +644,16 @@ struct TrainService::Impl unsigned reserved_ : 1; TrainService *trainService_; Buffer *response_; + BarrierNotifiable bn_; }; TractionRequestFlow traction_; }; -TrainService::TrainService(If *iface) +TrainService::TrainService(If *iface, NodeRegistry *train_node_registry) : Service(iface->executor()) , iface_(iface) + , nodes_(train_node_registry) { impl_ = new Impl(this); } @@ -623,9 +669,16 @@ void TrainService::register_train(TrainNode *node) extern void StartInitializationFlow(Node * node); StartInitializationFlow(node); AtomicHolder h(this); - nodes_.insert(node); + nodes_->register_node(node); LOG(VERBOSE, "Registered node %p for traction.", node); - HASSERT(nodes_.find(node) != nodes_.end()); +} + +void TrainService::unregister_train(TrainNode *node) +{ + HASSERT(nodes_->is_node_registered(node)); + iface_->delete_local_node(node); + AtomicHolder h(this); + nodes_->unregister_node(node); } } // namespace openlcb diff --git a/src/openlcb/TractionTrain.cxxtest b/src/openlcb/TractionTrain.cxxtest index 91a19e31d..9c8c386c7 100644 --- a/src/openlcb/TractionTrain.cxxtest +++ b/src/openlcb/TractionTrain.cxxtest @@ -95,7 +95,6 @@ protected: TractionSingleMockTest() { create_allocated_alias(); - expect_next_alias_allocation(); EXPECT_CALL(m1_, legacy_address()).Times(AtLeast(0)).WillRepeatedly( Return(0x00003456U)); EXPECT_CALL(m1_, legacy_address_type()) diff --git a/src/openlcb/TractionTrain.hxx b/src/openlcb/TractionTrain.hxx index fdb16b619..61e29bc31 100644 --- a/src/openlcb/TractionTrain.hxx +++ b/src/openlcb/TractionTrain.hxx @@ -38,6 +38,7 @@ #include #include "executor/Service.hxx" +#include "openlcb/DefaultNodeRegistry.hxx" #include "openlcb/Node.hxx" #include "openlcb/TractionDefs.hxx" #include "openlcb/TrainInterface.hxx" @@ -48,24 +49,6 @@ namespace openlcb class TrainService; -/// Linked list entry for all registered consist clients for a given train -/// node. -struct ConsistEntry : public QMember { - ConsistEntry(NodeID s, uint8_t flags) : payload((s << 8) | flags) {} - NodeID get_slave() const { - return payload >> 8; - } - uint8_t get_flags() const { - return payload & 0xff; - } - void set_flags(uint8_t new_flags) { - payload ^= (payload & 0xff); - payload |= new_flags; - } -private: - uint64_t payload; -}; - /// Virtual node class for an OpenLCB train protocol node. /// /// Usage: @@ -77,39 +60,33 @@ private: class TrainNode : public Node { public: - TrainNode(TrainService *service, TrainImpl *train); ~TrainNode(); - If *iface() OVERRIDE; - bool is_initialized() OVERRIDE - { - return isInitialized_; - } - void set_initialized() OVERRIDE - { - isInitialized_ = 1; - } - - // Used for restarting the stack. - void clear_initialized() OVERRIDE - { - isInitialized_ = 0; - } - - TrainImpl *train() - { - return train_; - } - - NodeHandle get_controller() - { - return controllerNodeId_; - } - - void set_controller(NodeHandle id) - { - controllerNodeId_ = id; - } + /// @return the train implementation object for issuing control commands to + /// this train. + virtual TrainImpl *train() = 0; + + /// Applies a policy to function change requests coming in from the OpenLCB + /// bus. If the policy returns false, the change will not be applied to the + /// TrainImpl. This is used to implement consist function behavior. + /// @param src source node where the request came from. + /// @param command_byte is the first byte of the payload (usually 0x01 or + /// 0x81 depending on the REQ_LISTENER bit) + /// @param fnum which function to set + /// @param value what value to set this function to + /// @param done must be notified inline if the policy application is + /// successful. If not notified inline, then the returned value is ignored + /// and the call is repeated after done has been invoked by the callee. + /// @return true if the function should be applied to the TrainImpl, false + /// if it should not be applied. + virtual bool function_policy(NodeHandle src, uint8_t command_byte, + uint32_t fnum, uint16_t value, Notifiable *done) = 0; + + /// @return the last stored controller node. + virtual NodeHandle get_controller() = 0; + + /// @param id the controller node of this train. + virtual void set_controller(NodeHandle id) = 0; // Thread-safety information // @@ -124,9 +101,80 @@ public: // before any consist change requests would reach the front of the queue // for the traction flow. + /// Adds a node ID to the consist targets. @return false if the node was + /// already in the target list, true if it was newly added. + /// @param tgt the destination of the consist link + /// @param flags consisting flags from the Traction protocol. + virtual bool add_consist(NodeID tgt, uint8_t flags) = 0; + + /// Removes a node ID from the consist targets. @return true if the target + /// was removed, false if the target was not on the list. + /// @param tgt destination of consist link to remove. + virtual bool remove_consist(NodeID tgt) = 0; + + /// Fetch a given consist link. + /// @return The target of a given consist link, or NodeID(0) if there are + /// fewer than id consist targets. + /// @param id zero-based index of consist links. + /// @param flags retrieved consist link's flags go here. + virtual NodeID query_consist(int id, uint8_t* flags) = 0; + + /// @return the number of slaves in this consist. + virtual int query_consist_length() = 0; +}; + +/// Linked list entry for all registered consist clients for a given train +/// node. +struct ConsistEntry : public QMember +{ + /// Creates a new consist entry storage. + /// @param s the stored node ID + /// @param flags the stored flag byte + ConsistEntry(NodeID s, uint8_t flags) + : payload((s << 8) | flags) + { + } + /// @return the stored Node ID. + NodeID get_slave() const + { + return payload >> 8; + } + /// @return the stored flags byte. + uint8_t get_flags() const + { + return payload & 0xff; + } + /// Overrides the stored flags. + /// @param new_flags the new value of the flags byte. + void set_flags(uint8_t new_flags) + { + payload ^= (payload & 0xff); + payload |= new_flags; + } + +private: + /// Data contents. + uint64_t payload; +}; + +/// Intermediate class which is still abstract, but adds implementation for the +/// consist management functions. +class TrainNodeWithConsist : public TrainNode { +public: + ~TrainNodeWithConsist(); + + /// @copydoc TrainNode::function_policy() + /// The default function policy applies everything. + bool function_policy(NodeHandle src, uint8_t command_byte, uint32_t fnum, + uint16_t value, Notifiable *done) override + { + AutoNotify an(done); + return true; + } + /** Adds a node ID to the consist targets. @return false if the node was * already in the target list, true if it was newly added. */ - bool add_consist(NodeID tgt, uint8_t flags) + bool add_consist(NodeID tgt, uint8_t flags) override { if (!tgt) { @@ -150,8 +198,8 @@ public: } /** Removes a node ID from the consist targets. @return true if the target - * was removesd, false if the target was not on the list. */ - bool remove_consist(NodeID tgt) + * was removed, false if the target was not on the list. */ + bool remove_consist(NodeID tgt) override { for (auto it = consistSlaves_.begin(); it != consistSlaves_.end(); ++it) { @@ -168,7 +216,7 @@ public: /** Returns the consist target with offset id, or NodeID(0) if there are * fewer than id consist targets. id is zero-based. */ - NodeID query_consist(int id, uint8_t* flags) + NodeID query_consist(int id, uint8_t* flags) override { int k = 0; for (auto it = consistSlaves_.begin(); @@ -184,7 +232,7 @@ public: } /** Returns the number of slaves in this consist. */ - int query_consist_length() + int query_consist_length() override { int ret = 0; for (auto it = consistSlaves_.begin(); it != consistSlaves_.end(); @@ -194,38 +242,101 @@ public: return ret; } + TypedQueue consistSlaves_; +}; + +/// Default implementation of a train node. +class DefaultTrainNode : public TrainNodeWithConsist +{ +public: + DefaultTrainNode(TrainService *service, TrainImpl *impl); + ~DefaultTrainNode(); + + NodeHandle get_controller() override + { + return controllerNodeId_; + } + + void set_controller(NodeHandle id) override + { + controllerNodeId_ = id; + } + + If *iface() override; + bool is_initialized() override + { + return isInitialized_; + } + void set_initialized() override + { + isInitialized_ = 1; + } + // Used for restarting the stack. + void clear_initialized() override + { + isInitialized_ = 0; + } + + TrainImpl *train() override + { + return train_; + } + protected: + /// Pointer to the traction service. TrainService *service_; + /// Pointer to the train implementation object. TrainImpl *train_; private: + /// Node is initialized bit for startup transient. unsigned isInitialized_ : 1; /// Controller node that is assigned to run this train. 0 if none. NodeHandle controllerNodeId_; - TypedQueue consistSlaves_; }; - /// Train node class with a an OpenLCB Node ID from the DCC pool. Used for command stations. -class TrainNodeForProxy : public TrainNode { +class TrainNodeForProxy : public DefaultTrainNode +{ public: + /// Constructor. + /// @param service the traction service object that will own this node. + /// @param train the implementation object that the traction messages + /// should be forwarded to. TrainNodeForProxy(TrainService *service, TrainImpl *train); + /// Destructor. + ~TrainNodeForProxy(); + + /// @return the OpenLCB node ID, generated from the legacy protocol types + /// that we get from TrainImpl. NodeID node_id() OVERRIDE; }; /// Train node class with a fixed OpenLCB Node ID. This is useful for native /// train nodes that are not dynamically generated by a command station. -class TrainNodeWithId : public TrainNode { +class TrainNodeWithId : public DefaultTrainNode +{ public: + /// Constructor. + /// @param service the traction service object that will own this node. + /// @param train the implementation object that the traction messages + /// should be forwarded to. + /// @param node_id the OpenLCB node ID for this train. TrainNodeWithId(TrainService *service, TrainImpl *train, NodeID node_id); - NodeID node_id() OVERRIDE { + /// Destructor. + ~TrainNodeWithId(); + + /// @return the openlcb node ID. + NodeID node_id() OVERRIDE + { return nodeId_; } private: + /// The OpenLCB node ID. NodeID nodeId_; }; @@ -237,7 +348,12 @@ private: class TrainService : public Service, private Atomic { public: - TrainService(If *iface); + /// Constructor. + /// @param iface the OpenLCB interface to which the train nodes are bound. + /// @param train_node_registry implementation of the + /// NodeRegistry. Ownership is transferred. + TrainService( + If *iface, NodeRegistry *train_node_registry = new DefaultNodeRegistry); ~TrainService(); If *iface() @@ -249,14 +365,27 @@ public: initialization flow for the train. */ void register_train(TrainNode *node); + /// Removes a train node from the local interface. + /// @param node train to remove from registry. + void unregister_train(TrainNode *node); + + /// Checks if the a given node is a train node operated by this Traction + /// Service. + /// @param node a virtual node + /// @return true if this is a known train node. + bool is_known_train_node(Node *node) + { + return nodes_->is_node_registered(node); + } + private: struct Impl; /** Implementation flows. */ Impl *impl_; - + /** OpenLCB interface */ If *iface_; /** List of train nodes managed by this Service. */ - std::set nodes_; + std::unique_ptr nodes_; }; } // namespace openlcb diff --git a/src/openlcb/VirtualMemorySpace.cxxtest b/src/openlcb/VirtualMemorySpace.cxxtest new file mode 100644 index 000000000..749c171e4 --- /dev/null +++ b/src/openlcb/VirtualMemorySpace.cxxtest @@ -0,0 +1,959 @@ +/** \copyright + * Copyright (c) 2020, Balazs Racz + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * \file VirtualMemorySpace.cxxtest + * + * Unit tests for the virtual memory space. + * + * @author Balazs Racz + * @date 10 Aug 2020 + */ + +#include "openlcb/VirtualMemorySpace.hxx" + +#include "openlcb/ConfigRepresentation.hxx" +#include "openlcb/MemoryConfigClient.hxx" +#include "utils/async_datagram_test_helper.hxx" + +namespace openlcb +{ + +string arg1; +string arg2; + +CDI_GROUP(ExampleMemorySpace); +CDI_GROUP_ENTRY(skipped, EmptyGroup<5>); +CDI_GROUP_ENTRY(first, StringConfigEntry<13>); +CDI_GROUP_ENTRY(skipped2, EmptyGroup<8>); +CDI_GROUP_ENTRY(second, StringConfigEntry<20>); +CDI_GROUP_ENTRY(skipped3, EmptyGroup<8>); +CDI_GROUP_END(); + +ExampleMemorySpace cfg(44); + +const unsigned arg1_ofs = 44 + 5; +const unsigned arg2_ofs = 44 + 5 + 13 + 8; + +class VirtualMemorySpaceTest : public AsyncDatagramTest +{ +protected: + ~VirtualMemorySpaceTest() + { + wait(); + } + + MemoryConfigHandler memCfg_ {&datagram_support_, node_, 3}; + std::unique_ptr space_; + MemoryConfigClient client_ {node_, &memCfg_}; +}; + +class TestSpace : public VirtualMemorySpace +{ +public: + TestSpace() + { + arg1.clear(); + arg2.clear(); + register_string( + cfg.first(), string_reader(&arg1), string_writer(&arg1)); + register_string( + cfg.second(), string_reader(&arg2), string_writer(&arg2)); + } + + /// Creates a ReaderFunction that just returns a string from a given + /// variable. + /// @param ptr the string whose contents to return as read value. Must stay + /// alive as long as the function is in use. + /// @return the ReaderFunction. + std::function + string_reader(string *ptr) + { + return + [ptr](unsigned repeat, string *contents, BarrierNotifiable *done) { + *contents = *ptr; + done->notify(); + return true; + }; + } + + /// Creates a WriterFunction that just stores the data in a given string + /// variable. + /// @param ptr the string whose contents to return as read value. Must stay + /// alive as long as the function is in use. + /// @return the ReaderFunction. + std::function + string_writer(string *ptr) + { + return + [ptr](unsigned repeat, string contents, BarrierNotifiable *done) { + *ptr = std::move(contents); + done->notify(); + }; + } +}; + +class TestSpaceTest : public VirtualMemorySpaceTest +{ +protected: + TestSpaceTest() + { + space_.reset(new TestSpace); + memCfg_.registry()->insert(node_, SPACE, space_.get()); + } + + /// Memory space number where the test space is registered. + const uint8_t SPACE = 0x52; +}; + +TEST_F(TestSpaceTest, create) +{ +} + +/// Basic tests reading variables from the exact offset including partial +/// reads. +TEST_F(TestSpaceTest, read_payload) +{ + arg1 = "hello"; + auto b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, arg1_ofs, 13); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_STREQ("hello", b->data()->payload.c_str()); + EXPECT_EQ(13u, b->data()->payload.size()); + + b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, arg2_ofs, 20); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_STREQ("", b->data()->payload.c_str()); + EXPECT_EQ(20u, b->data()->payload.size()); + + // prefix read + b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, arg1_ofs, 3); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ("hel", b->data()->payload); +} + +/// Test reading a variable from an imprecise offset (too early). +TEST_F(TestSpaceTest, read_early) +{ + arg1 = "hello"; + auto b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, arg1_ofs - 2, 10); + ASSERT_EQ(0, b->data()->resultCode); + string exp("\0\0hello\0\0\0", 10); + EXPECT_EQ(exp, b->data()->payload); + EXPECT_EQ(10u, b->data()->payload.size()); + + b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, arg2_ofs - 4, 3); + ASSERT_EQ(0, b->data()->resultCode); + string exp2("\0\0\0", 3); + EXPECT_EQ(exp2, b->data()->payload); + EXPECT_EQ(3u, b->data()->payload.size()); +} + +/// Test reading a variable from an imprecise offset (too late -- middle of +/// variable). +TEST_F(TestSpaceTest, read_middle) +{ + arg1 = "hello"; + auto b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, arg1_ofs + 2, 10); + ASSERT_EQ(0, b->data()->resultCode); + string exp("llo\0\0\0\0\0\0\0", 10); + EXPECT_EQ(exp, b->data()->payload); + EXPECT_EQ(10u, b->data()->payload.size()); + + arg2 = "abcdefghij"; + b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, arg2_ofs + 2, 3); + ASSERT_EQ(0, b->data()->resultCode); + string exp2("cde", 3); + EXPECT_EQ(exp2, b->data()->payload); + EXPECT_EQ(3u, b->data()->payload.size()); +} + +/// Test writing a variable to a offset that is not covered at all. +TEST_F(TestSpaceTest, read_hole) +{ + auto b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, arg2_ofs - 5, 3); + ASSERT_EQ(0, b->data()->resultCode); + string exp("\0\0\0", 3); + EXPECT_EQ(exp, b->data()->payload); + + /// @todo this return seems to be wrong, although this address is out of + /// the memory space limits. + b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, arg1_ofs - 5, 2); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ(string(), b->data()->payload); + + /// @todo this return seems to be wrong, although this address is out of + /// the memory space limits. + b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, arg2_ofs + 100, 4); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ(string(), b->data()->payload); +} + +/// Test reading beyond eof. +TEST_F(TestSpaceTest, read_eof) +{ + unsigned reg_last = cfg.second().offset(); + auto b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, reg_last, 50); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ(20u, b->data()->payload.size()); + b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, reg_last + 20, 50); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ(0u, b->data()->payload.size()); + + b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, reg_last + 23, 50); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ(0u, b->data()->payload.size()); +} + +/// Basic tests writing variables from the exact offset but not including +/// partial writes. +TEST_F(TestSpaceTest, write_payload) +{ + auto b = invoke_flow(&client_, MemoryConfigClientRequest::WRITE, + NodeHandle(node_->node_id()), SPACE, arg1_ofs, "xyzw"); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ("xyzw", arg1); + EXPECT_EQ(4u, arg1.size()); + + b = invoke_flow(&client_, MemoryConfigClientRequest::WRITE, + NodeHandle(node_->node_id()), SPACE, arg2_ofs, "abcde"); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ("abcde", arg2); + EXPECT_EQ(5u, arg2.size()); +} + +/// Test writing a variable to a offset that is too early. +TEST_F(TestSpaceTest, write_early) +{ + auto b = invoke_flow(&client_, MemoryConfigClientRequest::WRITE, + NodeHandle(node_->node_id()), SPACE, arg1_ofs - 2, "xyzw"); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ("zw", arg1); + EXPECT_EQ(2u, arg1.size()); + + b = invoke_flow(&client_, MemoryConfigClientRequest::WRITE, + NodeHandle(node_->node_id()), SPACE, arg2_ofs - 1, "qwert"); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ("wert", arg2); + EXPECT_EQ(4u, arg2.size()); +} + +/// Test writing into to a offset that is in the middle of a variable. +TEST_F(TestSpaceTest, write_middle) +{ + arg1 = "hellllo"; + auto b = invoke_flow(&client_, MemoryConfigClientRequest::WRITE, + NodeHandle(node_->node_id()), SPACE, arg1_ofs + 3, "xy"); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ(string("helxylo\0\0\0\0\0\0", 13), arg1); + + // Writes to middle then beyond the end of the variable. + string payload(19, 'a'); + payload += "bbbbbbbbb"; + arg2 = "0123456789i123456789"; + b = invoke_flow(&client_, MemoryConfigClientRequest::WRITE, + NodeHandle(node_->node_id()), SPACE, arg1_ofs + 3, payload); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ("helaaaaaaaaaa", arg1); + EXPECT_EQ(13u, arg1.size()); + EXPECT_EQ("abbbbbbbbb", arg2); + EXPECT_EQ(10u, arg2.size()); + + // Writes a string in multiple datagrams. + payload = "0123456789abcdef"; + payload.push_back(0); + + b = invoke_flow(&client_, MemoryConfigClientRequest::WRITE, + NodeHandle(node_->node_id()), SPACE, arg2_ofs, payload.substr(0, 10)); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ("0123456789", arg2); + + b = invoke_flow(&client_, MemoryConfigClientRequest::WRITE, + NodeHandle(node_->node_id()), SPACE, arg2_ofs + 10, + payload.substr(10, 3)); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_STREQ("0123456789abc", arg2.c_str()); + b = invoke_flow(&client_, MemoryConfigClientRequest::WRITE, + NodeHandle(node_->node_id()), SPACE, arg2_ofs + 13, + payload.substr(13, 2)); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_STREQ("0123456789abcde", arg2.c_str()); + b = invoke_flow(&client_, MemoryConfigClientRequest::WRITE, + NodeHandle(node_->node_id()), SPACE, arg2_ofs + 15, payload.substr(15)); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_STREQ("0123456789abcdef", arg2.c_str()); +} + +/// Test writing a variable to a offset that is not covered at all. +TEST_F(TestSpaceTest, write_hole) +{ + auto b = invoke_flow(&client_, MemoryConfigClientRequest::WRITE, + NodeHandle(node_->node_id()), SPACE, arg2_ofs - 5, "xyz"); + ASSERT_EQ(0, b->data()->resultCode); + + b = invoke_flow(&client_, MemoryConfigClientRequest::WRITE, + NodeHandle(node_->node_id()), SPACE, arg1_ofs - 5, "qw"); + ASSERT_EQ(0, b->data()->resultCode); + + b = invoke_flow(&client_, MemoryConfigClientRequest::WRITE, + NodeHandle(node_->node_id()), SPACE, arg2_ofs + 100, "qw"); + ASSERT_EQ(0, b->data()->resultCode); +} + +CDI_GROUP(NumericGroup); +CDI_GROUP_ENTRY(skipped, EmptyGroup<5>); +CDI_GROUP_ENTRY(first, Uint32ConfigEntry); +CDI_GROUP_ENTRY(second, Int16ConfigEntry); +CDI_GROUP_ENTRY(third, Uint8ConfigEntry); +CDI_GROUP_ENTRY(skipped2, EmptyGroup<8>); +CDI_GROUP_END(); + +CDI_GROUP(NumericMemorySpace); +CDI_GROUP_ENTRY(skipped, EmptyGroup<5>); +CDI_GROUP_ENTRY(first, StringConfigEntry<13>); +CDI_GROUP_ENTRY(skipped2, EmptyGroup<8>); +CDI_GROUP_ENTRY(second, StringConfigEntry<20>); +CDI_GROUP_ENTRY(skipped3, EmptyGroup<8>); +CDI_GROUP_ENTRY(outer_before, Uint32ConfigEntry); +using RG = RepeatedGroup; +CDI_GROUP_ENTRY(grp, RG); +CDI_GROUP_ENTRY(outer_after, Uint32ConfigEntry); +CDI_GROUP_END(); + +NumericMemorySpace ncfg(44); + +class TestSpaceAsync : public VirtualMemorySpace +{ +public: + TestSpaceAsync() + { + arg1.clear(); + arg2.clear(); + register_string( + ncfg.first(), string_reader(&arg1), string_writer(&arg1)); + register_string( + ncfg.second(), string_reader(&arg2), string_writer(&arg2)); + register_numeric(ncfg.outer_before(), typed_reader(&rnBefore_), + typed_writer(&rnBefore_)); + register_numeric(ncfg.outer_after(), typed_reader(&rnAfter_), + typed_writer(&rnAfter_)); + register_numeric(ncfg.grp().entry(0).first(), typed_reader(&rnFirst_), + typed_writer(&rnFirst_)); + register_numeric(ncfg.grp().entry(0).second(), typed_reader(&rnSecond_), + typed_writer(&rnSecond_)); + register_numeric(ncfg.grp().entry(0).third(), typed_reader(&rnThird_), + typed_writer(&rnThird_)); + register_repeat(ncfg.grp()); + } + + /// Creates a ReaderFunction that just returns a string from a given + /// variable. + /// @param ptr the string whose contents to return as read value. Must stay + /// alive as long as the function is in use. + /// @return the ReaderFunction. + std::function + string_reader(string *ptr) + { + return [this, ptr]( + unsigned repeat, string *contents, BarrierNotifiable *done) { + attempt++; + lastRepeat_ = repeat; + if ((attempt & 1) == 0) + { + *contents = *ptr; + done->notify(); + return true; + } + else + { + g_executor.add( + new CallbackExecutable([done]() { done->notify(); })); + return false; + } + }; + } + + /// Creates a TypedReaderFunction that just returns a value from a given + /// variable. + /// @param ptr the variable whose contents to return as read value. Must + /// stay alive as long as the function is in use. + /// @return the TypedReaderFunction. + template + typename std::function + typed_reader(T *ptr) + { + return [this, ptr](unsigned repeat, BarrierNotifiable *done) { + attempt++; + lastRepeat_ = repeat; + if ((attempt & 1) == 0) + { + done->notify(); + return *ptr; + } + else + { + g_executor.add( + new CallbackExecutable([done]() { done->notify(); })); + return T(); + } + }; + } + + /// Creates a WriterFunction that just stores the data in a given string + /// variable. + /// @param ptr the variable where to store the contents. Must stay alive as + /// long as the function is in use. + /// @return the WriterFunction. + std::function + string_writer(string *ptr) + { + return [this, ptr]( + unsigned repeat, string contents, BarrierNotifiable *done) { + attempt++; + lastRepeat_ = repeat; + if ((attempt & 1) == 0) + { + *ptr = std::move(contents); + done->notify(); + return true; + } + else + { + g_executor.add( + new CallbackExecutable([done]() { done->notify(); })); + return false; + } + }; + } + + /// Creates a TypedWriterFunction that just stores the data in a given + /// variable. + /// @param ptr the variable where to store the contents. Must stay alive as + /// long as the function is in use. + /// @return the TypedWriterFunction. + template + std::function + typed_writer(T *ptr) + { + return + [this, ptr](unsigned repeat, T contents, BarrierNotifiable *done) { + attempt++; + lastRepeat_ = repeat; + if ((attempt & 1) == 0) + { + *ptr = std::move(contents); + done->notify(); + } + else + { + g_executor.add( + new CallbackExecutable([done]() { done->notify(); })); + } + }; + } + + /// Stores the last invoked repetition number. + unsigned lastRepeat_ = 0; + /// Shadow for NumericGroup.first. + uint32_t rnFirst_ = 0; + /// Shadow for NumericGroup.second. + int16_t rnSecond_ = 0; + /// Shadow for NumericGroup.third. + uint8_t rnThird_ = 0; + + /// Shadow for NUmericMemorySpace.before. + uint32_t rnBefore_ = 0; + /// Shadow for NUmericMemorySpace.after. + uint32_t rnAfter_ = 0; + +private: + size_t attempt = 0; +}; + +class TestSpaceAsyncTest : public VirtualMemorySpaceTest +{ +protected: + TestSpaceAsyncTest() + { + space_.reset(tspace_); + memCfg_.registry()->insert(node_, SPACE, space_.get()); + } + + TestSpaceAsync *tspace_ = new TestSpaceAsync; + /// Memory space number where the test space is registered. + const uint8_t SPACE = 0x52; +}; + +/// Basic tests reading variables from async space. +TEST_F(TestSpaceAsyncTest, read_payload_async) +{ + arg1 = "hello"; + auto b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, arg1_ofs, 13); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_STREQ("hello", b->data()->payload.c_str()); + EXPECT_EQ(13u, b->data()->payload.size()); + + b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, arg2_ofs, 20); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_STREQ("", b->data()->payload.c_str()); + EXPECT_EQ(20u, b->data()->payload.size()); +} + +/// Test reading a variable from an imprecise offset (too late -- middle of +/// variable). +TEST_F(TestSpaceAsyncTest, read_middle) +{ + arg1 = "hello"; + auto b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, arg1_ofs + 2, 10); + ASSERT_EQ(0, b->data()->resultCode); + string exp("llo\0\0\0\0\0\0\0", 10); + EXPECT_EQ(exp, b->data()->payload); + EXPECT_EQ(10u, b->data()->payload.size()); + + arg2 = "abcdefghij"; + b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, arg2_ofs + 2, 3); + ASSERT_EQ(0, b->data()->resultCode); + string exp2("cde", 3); + EXPECT_EQ(exp2, b->data()->payload); + EXPECT_EQ(3u, b->data()->payload.size()); +} + +/// Basic tests writing variables from the exact offset but not including +/// partial writes. +TEST_F(TestSpaceAsyncTest, write_payload_async) +{ + auto b = invoke_flow(&client_, MemoryConfigClientRequest::WRITE, + NodeHandle(node_->node_id()), SPACE, arg1_ofs, "xyzw"); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ("xyzw", arg1); + EXPECT_EQ(4u, arg1.size()); + + b = invoke_flow(&client_, MemoryConfigClientRequest::WRITE, + NodeHandle(node_->node_id()), SPACE, arg2_ofs, "abcde"); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ("abcde", arg2); + EXPECT_EQ(5u, arg2.size()); +} + +/// Test writing into to a offset that is in the middle of a variable. +TEST_F(TestSpaceAsyncTest, write_middle) +{ + arg1 = "hellllo"; + auto b = invoke_flow(&client_, MemoryConfigClientRequest::WRITE, + NodeHandle(node_->node_id()), SPACE, arg1_ofs + 3, "xy"); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ(string("helxylo\0\0\0\0\0\0", 13), arg1); + + // Writes to middle then beyond the end of the variable. + string payload(19, 'a'); + payload += "bbbbbbbbb"; + arg2 = "0123456789i123456789"; + b = invoke_flow(&client_, MemoryConfigClientRequest::WRITE, + NodeHandle(node_->node_id()), SPACE, arg1_ofs + 3, payload); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ("helaaaaaaaaaa", arg1); + EXPECT_EQ(13u, arg1.size()); + EXPECT_EQ("abbbbbbbbb", arg2); + EXPECT_EQ(10u, arg2.size()); + + // Writes a string in multiple datagrams. + payload = "0123456789abcdef"; + payload.push_back(0); + + b = invoke_flow(&client_, MemoryConfigClientRequest::WRITE, + NodeHandle(node_->node_id()), SPACE, arg2_ofs, payload.substr(0, 10)); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ("0123456789", arg2); + + b = invoke_flow(&client_, MemoryConfigClientRequest::WRITE, + NodeHandle(node_->node_id()), SPACE, arg2_ofs + 10, + payload.substr(10, 3)); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_STREQ("0123456789abc", arg2.c_str()); + b = invoke_flow(&client_, MemoryConfigClientRequest::WRITE, + NodeHandle(node_->node_id()), SPACE, arg2_ofs + 13, + payload.substr(13, 2)); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_STREQ("0123456789abcde", arg2.c_str()); + b = invoke_flow(&client_, MemoryConfigClientRequest::WRITE, + NodeHandle(node_->node_id()), SPACE, arg2_ofs + 15, payload.substr(15)); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_STREQ("0123456789abcdef", arg2.c_str()); +} + +/// Tests reading and writing numeric variables with endianness. +TEST_F(TestSpaceAsyncTest, rw_numeric_async) +{ + string u32payload; + u32payload.push_back(0xAA); + u32payload.push_back(2); + u32payload.push_back(3); + u32payload.push_back(4); + + auto b = invoke_flow(&client_, MemoryConfigClientRequest::WRITE, + NodeHandle(node_->node_id()), SPACE, ncfg.outer_before().offset(), + u32payload); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ(0xAA020304u, tspace_->rnBefore_); + + tspace_->rnBefore_ = 0xbb554433; + b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, ncfg.outer_before().offset(), 4); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ((char)0xbb, b->data()->payload[0]); + EXPECT_EQ(0x55, b->data()->payload[1]); + EXPECT_EQ(0x44, b->data()->payload[2]); + EXPECT_EQ(0x33, b->data()->payload[3]); +} + +/// Tests variable after repeted group. +TEST_F(TestSpaceAsyncTest, rw_numeric_after_repeat) +{ + string u32payload; + u32payload.push_back(0xAA); + u32payload.push_back(2); + u32payload.push_back(3); + u32payload.push_back(4); + + auto b = invoke_flow(&client_, MemoryConfigClientRequest::WRITE, + NodeHandle(node_->node_id()), SPACE, ncfg.outer_after().offset(), + u32payload); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ(0xAA020304u, tspace_->rnAfter_); + + tspace_->rnAfter_ = 0xbb554433; + b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, ncfg.outer_after().offset(), 4); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ((char)0xbb, b->data()->payload[0]); + EXPECT_EQ(0x55, b->data()->payload[1]); + EXPECT_EQ(0x44, b->data()->payload[2]); + EXPECT_EQ(0x33, b->data()->payload[3]); +} + +/// Tests reading and writing numeric variables with endianness from repetitions. +TEST_F(TestSpaceAsyncTest, rw_numeric_repeat) +{ + string u32payload; + u32payload.push_back(0xAA); + u32payload.push_back(2); + u32payload.push_back(3); + u32payload.push_back(4); + + auto b = invoke_flow(&client_, MemoryConfigClientRequest::WRITE, + NodeHandle(node_->node_id()), SPACE, + ncfg.grp().entry(3).first().offset(), u32payload); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ(3u, tspace_->lastRepeat_); + EXPECT_EQ(0xAA020304u, tspace_->rnFirst_); + + tspace_->rnSecond_ = -2; + b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, + ncfg.grp().entry(4).second().offset(), 2); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ(4u, tspace_->lastRepeat_); + ASSERT_EQ(2u, b->data()->payload.size()); + EXPECT_EQ((char)0xFF, b->data()->payload[0]); + EXPECT_EQ((char)0xFE, b->data()->payload[1]); + + tspace_->rnSecond_ = 55; + b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, + ncfg.grp().entry(0).second().offset(), 2); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ(0u, tspace_->lastRepeat_); + ASSERT_EQ(2u, b->data()->payload.size()); + EXPECT_EQ((char)0, b->data()->payload[0]); + EXPECT_EQ((char)55, b->data()->payload[1]); +} + +CDI_GROUP(RepeatMemoryDef); +CDI_GROUP_ENTRY(skipped, EmptyGroup<5>); +CDI_GROUP_ENTRY(before, StringConfigEntry<13>); +CDI_GROUP_ENTRY(skipped2, EmptyGroup<8>); +using GroupRept = RepeatedGroup; +CDI_GROUP_ENTRY(grp, GroupRept); +CDI_GROUP_ENTRY(skipped3, EmptyGroup<8>); +CDI_GROUP_ENTRY(after, StringConfigEntry<20>); +CDI_GROUP_ENTRY(grp2, GroupRept); +CDI_GROUP_END(); + +RepeatMemoryDef spacerept(22); + +class SpaceWithRepeat : public VirtualMemorySpace +{ +public: + SpaceWithRepeat() + { + arg1.clear(); + arg2.clear(); + register_string(spacerept.grp().entry<0>().first(), + string_reader(&arg1), string_writer(&arg1)); + register_string(spacerept.grp().entry<0>().second(), + string_reader(&arg2), string_writer(&arg2)); + register_string(spacerept.before(), string_reader(&before_), + string_writer(&before_)); + register_string( + spacerept.after(), string_reader(&after_), string_writer(&after_)); + register_repeat(spacerept.grp()); + register_string(spacerept.grp2().entry<0>().second(), + string_reader(&arg2), string_writer(&arg2)); + register_repeat(spacerept.grp2()); + } + + /// Creates a ReaderFunction that just returns a string from a given + /// variable. + /// @param ptr the string whose contents to return as read value. Must stay + /// alive as long as the function is in use. + /// @return the ReaderFunction. + std::function + string_reader(string *ptr) + { + return [this, ptr]( + unsigned repeat, string *contents, BarrierNotifiable *done) { + lastRepeat_ = repeat; + *contents = *ptr; + done->notify(); + return true; + }; + } + + /// Creates a WriterFunction that just stores the data in a given string + /// variable. + /// @param ptr the string whose contents to return as read value. Must stay + /// alive as long as the function is in use. + /// @return the ReaderFunction. + std::function + string_writer(string *ptr) + { + return [this, ptr]( + unsigned repeat, string contents, BarrierNotifiable *done) { + lastRepeat_ = repeat; + *ptr = std::move(contents); + done->notify(); + }; + } + + /// Saves the last repeat variable into this value. + unsigned lastRepeat_; + /// Storage variable for a field. + string before_; + /// Storage variable for a field. + string after_; +}; + +class ReptSpaceTest : public VirtualMemorySpaceTest +{ +protected: + ReptSpaceTest() + { + memCfg_.registry()->insert(node_, SPACE, &s_); + } + + /// Memory space number where the test space is registered. + const uint8_t SPACE = 0x52; + SpaceWithRepeat s_; +}; + +TEST_F(ReptSpaceTest, create) +{ +} + +// Looks for a field that is before the repeated group. +TEST_F(ReptSpaceTest, before) +{ + s_.before_ = "hello"; + auto b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, spacerept.before().offset(), 13); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_STREQ("hello", b->data()->payload.c_str()); + EXPECT_EQ(13u, b->data()->payload.size()); + EXPECT_EQ(0u, s_.lastRepeat_); + + b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, spacerept.before().offset() - 2, + 5); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ(string("\0\0hel", 5), b->data()->payload); + EXPECT_EQ(0u, s_.lastRepeat_); +} + +// Looks for a field in the first repetition of the group. +TEST_F(ReptSpaceTest, first_repeat) +{ + arg1 = "world"; + auto b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, + spacerept.grp().entry<0>().first().offset(), 13); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_STREQ("world", b->data()->payload.c_str()); + EXPECT_EQ(13u, b->data()->payload.size()); + EXPECT_EQ(0u, s_.lastRepeat_); + + // Start offset within the group. + b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, + spacerept.grp().entry<0>().first().offset() - 2, 5); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ(string("\0\0wor", 5), b->data()->payload); + EXPECT_EQ(0u, s_.lastRepeat_); + + // Start offset _before_ the group. + b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, + spacerept.grp().entry<0>().first().offset() - 7, 10); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ(string("\0\0\0\0\0\0\0wor", 10), b->data()->payload); + EXPECT_EQ(0u, s_.lastRepeat_); + + arg2 = "ahoi"; + // Second field, exact match + b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, + spacerept.grp().entry<0>().second().offset(), 13); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_STREQ("ahoi", b->data()->payload.c_str()); + EXPECT_EQ(13u, b->data()->payload.size()); + EXPECT_EQ(0u, s_.lastRepeat_); + + // Second field, before match + b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, + spacerept.grp().entry<0>().second().offset() - 2, 5); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ(string("\0\0aho", 5), b->data()->payload); + EXPECT_EQ(0u, s_.lastRepeat_); +} + +// Looks for a field in the first repetition of the group. +TEST_F(ReptSpaceTest, mid_repeat) +{ + arg1 = "world"; + auto b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, + spacerept.grp().entry<2>().first().offset(), 13); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_STREQ("world", b->data()->payload.c_str()); + EXPECT_EQ(13u, b->data()->payload.size()); + EXPECT_EQ(2u, s_.lastRepeat_); + + // Start offset within the group. + b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, + spacerept.grp().entry<2>().first().offset() - 2, 5); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ(string("\0\0wor", 5), b->data()->payload); + EXPECT_EQ(2u, s_.lastRepeat_); + + // Start offset in the previous group repeat. + b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, + spacerept.grp().entry<2>().first().offset() - 7, 10); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ(string("\0\0\0\0\0\0\0wor", 10), b->data()->payload); + EXPECT_EQ(2u, s_.lastRepeat_); + + arg2 = "ahoi"; + // Second field, exact match + b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, + spacerept.grp().entry<2>().second().offset(), 13); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_STREQ("ahoi", b->data()->payload.c_str()); + EXPECT_EQ(13u, b->data()->payload.size()); + EXPECT_EQ(2u, s_.lastRepeat_); + + // Second field, before match + b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, + spacerept.grp().entry<2>().second().offset() - 2, 5); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ(string("\0\0aho", 5), b->data()->payload); + EXPECT_EQ(2u, s_.lastRepeat_); +} + +/// Test reading beyond eof. The end of the space there is a repeated group +/// where the registry entries do not cover all bytes. This is a difficult +/// cornercase and we test that all bytes until the end of the repetition can +/// be read but not beyond. +TEST_F(ReptSpaceTest, read_eof) +{ + unsigned reg_last = spacerept.grp2().entry<2>().second().offset(); + // EOF is 28 bytes away from here. + EXPECT_EQ(reg_last + 27, s_.max_address()); + auto b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, reg_last, 50); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ(28u, b->data()->payload.size()); + b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, reg_last + 20, 50); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ(8u, b->data()->payload.size()); + + b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, reg_last + 23, 50); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ(5u, b->data()->payload.size()); + + b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, reg_last + 27, 50); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ(1u, b->data()->payload.size()); + + b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, reg_last + 28, 50); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ(0u, b->data()->payload.size()); + + b = invoke_flow(&client_, MemoryConfigClientRequest::READ_PART, + NodeHandle(node_->node_id()), SPACE, reg_last + 29, 50); + ASSERT_EQ(0, b->data()->resultCode); + EXPECT_EQ(0u, b->data()->payload.size()); +} + +} // namespace openlcb diff --git a/src/openlcb/VirtualMemorySpace.hxx b/src/openlcb/VirtualMemorySpace.hxx new file mode 100644 index 000000000..9007abe8b --- /dev/null +++ b/src/openlcb/VirtualMemorySpace.hxx @@ -0,0 +1,526 @@ +/** \copyright + * Copyright (c) 2020, Balazs Racz + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * \file VirtualMemorySpace.hxx + * + * Implementation of a memory space where the values are not stored in a + * contiguous storage area but are read and written via callbacks. + * + * @author Balazs Racz + * @date 10 Aug 2020 + */ + +#ifndef _OPENLCB_VIRTUALMEMORYSPACE_HXX +#define _OPENLCB_VIRTUALMEMORYSPACE_HXX + +#include "openlcb/ConfigEntry.hxx" +#include "openlcb/ConfigRepresentation.hxx" +#include "openlcb/MemoryConfig.hxx" +#include "utils/SortedListMap.hxx" + +namespace openlcb +{ + +/// Implementation of a memory space where the values are not stored in a +/// contiguous storage area but are read and written via callbacks. +class VirtualMemorySpace : public MemorySpace +{ +public: + VirtualMemorySpace() + : isReadOnly_(false) + { + } + + /// @returns whether the memory space does not accept writes. + bool read_only() override + { + return isReadOnly_; + } + /// @returns the lowest address that's valid for this block. + address_t min_address() override + { + return minAddress_; + } + /// @returns the largest valid address for this block. A read of 1 from + /// this address should succeed in returning the last byte. + address_t max_address() override + { + return maxAddress_; + } + + /// @return the number of bytes successfully written (before hitting end + /// of space). + /// @param destination address to write to + /// @param data to write + /// @param len how many bytes to write + /// @param error if set to non-null, then the operation has failed. If the + /// operation needs to be continued, then sets error to + /// MemorySpace::ERROR_AGAIN, and calls the Notifiable + /// @param again when a re-try makes sense. The caller should call write + /// once more, with the offset adjusted with the previously returned + /// bytes. + size_t write(address_t destination, const uint8_t *data, size_t len, + errorcode_t *error, Notifiable *again) override + { + if ((destination > maxAddress_) || ((destination + len) <= minAddress_)) + { + *error = MemoryConfigDefs::ERROR_OUT_OF_BOUNDS; + return 0; + } + if (destination + len > maxAddress_ + 1) + { + len = maxAddress_ + 1 - destination; + } + *error = 0; + unsigned repeat; + const DataElement *element = nullptr; + ssize_t skip = find_data_element(destination, len, &element, &repeat); + string payload; + size_t written_len; + if (skip > 0) + { + // Will cause a new call be delivered with adjusted data and len. + return skip; + } + else if (skip < 0) + { + HASSERT(element); + // We have some missing bytes that we need to read out first, then + // can perform the write. + address_t field_start = destination + skip; + if (!(cacheOffset_ == field_start && + (cachedData_.size() >= (size_t)-skip))) + { + cacheOffset_ = field_start; + cachedData_.clear(); + bn_.reset(again); + element->readImpl_(repeat, &cachedData_, bn_.new_child()); + if (!bn_.abort_if_almost_done()) + { + // did not succeed synchronously. + bn_.notify(); // our slice + *error = MemorySpace::ERROR_AGAIN; + return 0; + } + cachedData_.resize(element->size_); // pads with zeroes + } + // Now: cachedData_ contains the payload in the current storage. + payload = cachedData_; + written_len = + std::min((size_t)len, (size_t)(element->size_ + skip)); + memcpy(&payload[-skip], (const char *)data, written_len); + } + else // exact address write. + { + HASSERT(element); + payload.assign((const char *)data, + std::min((size_t)len, (size_t)element->size_)); + written_len = payload.size(); + } + bn_.reset(again); + element->writeImpl_(repeat, std::move(payload), bn_.new_child()); + if (bn_.abort_if_almost_done()) + { + cachedData_.clear(); + return written_len; + } + else + { + // did not succeed synchronously. + bn_.notify(); // our slice + *error = MemorySpace::ERROR_AGAIN; + return 0; + } + } + + /** @returns the number of bytes successfully read (before hitting end of + * space). If *error is set to non-null, then the operation has failed. If + * the operation needs to be continued, then sets error to ERROR_AGAIN, and + * calls the Notifiable @param again when a re-try makes sense. The caller + * should call read once more, with the offset adjusted with the previously + * returned bytes. */ + size_t read(address_t source, uint8_t *dst, size_t len, errorcode_t *error, + Notifiable *again) override + { + if ((source > maxAddress_) || ((source + len) <= minAddress_)) + { + *error = MemoryConfigDefs::ERROR_OUT_OF_BOUNDS; + return 0; + } + if (source + len > maxAddress_ + 1) + { + len = maxAddress_ + 1 - source; + } + *error = 0; + unsigned repeat; + const DataElement *element = nullptr; + ssize_t skip = find_data_element(source, len, &element, &repeat); + if (skip > 0) + { + memset(dst, 0, skip); + return skip; + } + // Now: skip <= 0 + HASSERT(element); + string payload; + bn_.reset(again); + element->readImpl_(repeat, &payload, bn_.new_child()); + if (!bn_.abort_if_almost_done()) + { + // did not succeed synchronously. + bn_.notify(); // our slice + *error = MemorySpace::ERROR_AGAIN; + return 0; + } + payload.resize(element->size_); // pads with zeroes + size_t data_len = std::min(payload.size() + skip, len); + memcpy(dst, payload.data() - skip, data_len); + return data_len; + } + +protected: + /// Function that will be called for writes. + /// @param repeat 0 to number of repeats if this is in a repeated + /// group. Always 0 if not repeated group. + /// @param contents data payload that needs to be written. The data + /// bytes of this container start at the address_. + /// @param done must be notified when the write is complete (possibly + /// inline). + using WriteFunction = std::function; + /// Function that will be called for reads. + /// @param repeat 0 to number of repeats if this is in a repeated + /// group. Always 0 if not repeated group. + /// @param contents the payload to be returned from this variable shall + /// be written here. Will be zero-padded to size_ bytes if shorter. + /// @param done must be notified when the read values are ready. The + /// call will be re-tried if this does not happen inline. + using ReadFunction = std::function; + + /// Typed WriteFunction for primitive types. + template + using TypedWriteFunction = typename std::function; + + /// Typed ReadFunction for primitive types. @return the read value if the + /// read was successful. If the read did not complete, return 0. + template + using TypedReadFunction = typename std::function; + + /// Setup the address bounds from a single CDI group declaration. + /// @param group is an instance of a group, for example a segment. + template void set_bounds_from_group(const G &group) + { + minAddress_ = group.offset(); + maxAddress_ = group.offset() + group.size() - 1; + } + + /// Expand the address bounds from a single CDI group declaration. + /// @param group is an instance of a group, for example a segment. + template void expand_bounds_from_group(const G &group) + { + minAddress_ = std::min(minAddress_, (address_t)group.offset()); + maxAddress_ = std::max( + maxAddress_, (address_t)(group.offset() + group.size() - 1)); + } + + /// Register an untyped element. + /// @param address the address in the memory space + /// @param size how many bytes this elements occupes + /// @param read_f will be called to read this data + /// @param write_f will be called to write this data + void register_element(address_t address, address_t size, + ReadFunction read_f, WriteFunction write_f) + { + elements_.insert(DataElement(address, size, read_f, write_f)); + } + + /// Registers a string typed element. + /// @param entry is the CDI ConfigRepresentation. + /// @param read_f will be called to read this data + /// @param write_f will be called to write this data + template + void register_string(const StringConfigEntry &entry, + ReadFunction read_f, WriteFunction write_f) + { + expand_bounds_from_group(entry); + register_element(entry.offset(), SIZE, read_f, write_f); + } + + /// Registers a numeric typed element. + /// @param T is the type argument, e.g. uint8_t. + /// @param entry is the CDI ConfigRepresentation. + /// @param read_f will be called to read this data + /// @param write_f will be called to write this data + template + void register_numeric(const NumericConfigEntry &entry, + TypedReadFunction read_f, TypedWriteFunction write_f) + { + expand_bounds_from_group(entry); + auto trf = [read_f](unsigned repeat, string *contents, + BarrierNotifiable *done) { + T result = read_f(repeat, done); + contents->clear(); + contents->resize(sizeof(T)); + *((T *)&((*contents)[0])) = + NumericConfigEntry::endian_convert(result); + }; + auto twf = [write_f](unsigned repeat, string contents, + BarrierNotifiable *done) { + contents.resize(sizeof(T)); + T result = NumericConfigEntry::endian_convert( + *(const T *)contents.data()); + write_f(repeat, result, done); + }; + register_element( + entry.offset(), entry.size(), std::move(trf), std::move(twf)); + } + + /// Registers a repeated group. Calling this function means that the + /// virtual memory space of the group will be looped onto the first + /// repetition. The correct usage is to register the elements of the first + /// repetition, then register the repetition itself using this call. Nested + /// repetitions are not supported (either the outer or the inner repetition + /// needs to be unrolled and registered for each repeat there). + /// @param group is the repeated group instance. Will take the start + /// offset, repeat count and repeat size from it. + template + void register_repeat(const RepeatedGroup &group) + { + RepeatElement re; + re.start_ = group.offset(); + re.end_ = group.end_offset(); + re.repeatSize_ = Group::size(); + HASSERT(re.repeatSize_ * N == re.end_ - re.start_); + repeats_.insert(std::move(re)); + expand_bounds_from_group(group); + } + + /// Bounds for valid addresses. + address_t minAddress_ = 0xFFFFFFFFu; + /// Bounds for valid addresses. A read of length 1 from this address + /// should succeed in returning the last byte. + address_t maxAddress_ = 0; + /// Whether the space should report as RO. + unsigned isReadOnly_ : 1; + +private: + /// We keep one of these for each variable that was declared. + struct DataElement + { + DataElement(address_t address, address_t size, ReadFunction read_f, + WriteFunction write_f) + : address_(address) + , size_(size) + , writeImpl_(write_f) + , readImpl_(read_f) + { + } + /// Base offset of this variable (first repeat only). + address_t address_; + /// Size of this variable. This is how many bytes of address space this + /// variable occupies. + address_t size_; + /// Function that will be called for writes. + WriteFunction writeImpl_; + /// Function that will be called for reads. + ReadFunction readImpl_; + }; + + /// STL-compatible comparator function for sorting DataElements. + struct DataComparator + { + /// Sorting operator by address. + bool operator()(const DataElement &a, const DataElement &b) const + { + return a.address_ < b.address_; + } + /// Sorting operator by address. + bool operator()(unsigned a, const DataElement &b) const + { + return a < b.address_; + } + /// Sorting operator by address. + bool operator()(const DataElement &a, unsigned b) const + { + return a.address_ < b; + } + }; + + /// Represents a repeated group. + struct RepeatElement + { + /// Offset of the repeated group (first repeat). + uint32_t start_; + /// Address bytes per repeat. + uint32_t repeatSize_; + /// Address byte after the last repeat. + uint32_t end_; + }; + + /// STL-compatible comparator function for sorting RepeatElements. Sorts + /// repeats by the end_ as the key. + struct RepeatComparator + { + /// Sorting operator by end address. + bool operator()(const RepeatElement &a, const RepeatElement &b) const + { + return a.end_ < b.end_; + } + /// Sorting operator by end address against a lookup key. + bool operator()(uint32_t a, const RepeatElement &b) const + { + return a < b.end_; + } + }; + + /// Look up the first matching data element given an address in the virtual + /// memory space. + /// @param address byte offset to look up. + /// @param len how many bytes long range to search from address. + /// @param ptr will be filled with a pointer to the data element when + /// found, or filled with nullptr if no data element overlaps with the + /// given range. + /// @param repeat output argument, filled with zero or the repetition + /// number. + /// @return 0 if an exact match is found. -N if a data element is found, + /// but N first bytes of this element are not covered. A number N in + /// [1..len-1] if a data element is found, but this many bytes need to be + /// skipped from address to arrive at the given data element's offset. len + /// if there was no data element found (in which case also set ptr to + /// null). + ssize_t find_data_element(address_t address, address_t len, + const DataElement **ptr, unsigned *repeat) + { + *repeat = 0; + *ptr = nullptr; + bool in_repeat = false; + address_t original_address = address; + ElementsType::iterator b = elements_.begin(); + ElementsType::iterator e = elements_.end(); + // Align in the known repetitions first. + auto rit = repeats_.upper_bound(address); + int max_repeat = 0; + if (rit == repeats_.end()) + { + // not a repeat. + } + else + { + if (rit->start_ <= address && address < rit->end_) + { + // we are in the repeat. + unsigned cnt = (address - rit->start_) / rit->repeatSize_; + *repeat = cnt; + if (address + rit->repeatSize_ < rit->end_) + { + // Try one repetition later too. + max_repeat = 1; + } + // re-aligns address to the first repetition. + address -= cnt * rit->repeatSize_; + in_repeat = true; + b = elements_.lower_bound(rit->start_); + e = elements_.lower_bound(rit->start_ + rit->repeatSize_); + } + } + LOG(VERBOSE, + "searching for element at address %u in_repeat=%d address=%u " + "len=%u", + (unsigned)original_address, in_repeat, (unsigned)address, + (unsigned)len); + + for (int is_repeat = 0; is_repeat <= max_repeat; ++is_repeat) + { + auto it = std::upper_bound(b, e, address, DataComparator()); + if (it != elements_.begin()) + { + auto pit = it - 1; + // now: pit->address_ <= address + if (pit->address_ + pit->size_ > address) + { + // found overlap + *ptr = &*pit; + return (ssize_t)pit->address_ - + (ssize_t)address; // may be negative! + } + // else: no overlap, look at the next item + } + // now: it->address_ > address + if ((it != elements_.end()) && (address + len > it->address_)) + { + // found overlap, but some data needs to be discarded. + *ptr = &*it; + return it->address_ - address; + } + + if (in_repeat) + { + // We might be too close to the end of a repetition, we will + // try with the next repeat instead. + address -= rit->repeatSize_; + *repeat += 1; + if (original_address + rit->repeatSize_ >= rit->end_) + { + // We ran out of repeats. Look at the range beyond the + // group instead. + b = elements_.lower_bound(rit->end_); + e = elements_.end(); + *repeat = 0; + } + } + else + { + break; + } + } + + // now: no overlap either before or after. + LOG(VERBOSE, "element not found for address %u", + (unsigned)original_address); + return len; + } + + static constexpr unsigned NO_CACHE = static_cast(-1); + /// Offset in the memory space at which cachedData_ starts. + address_t cacheOffset_ = NO_CACHE; + /// Stored information for read-modify-write calls. + string cachedData_; + /// Container type for storing the data elements. + typedef SortedListSet ElementsType; + /// Stores all the registered variables. + ElementsType elements_; + /// Stores all the registered variables. + SortedListSet repeats_; + /// Helper object in the function calls. + BarrierNotifiable bn_; +}; // class VirtualMemorySpace + +} // namespace openlcb + +#endif // _OPENLCB_VIRTUALMEMORYSPACE_HXX diff --git a/src/openlcb/nmranet_constants.cxx b/src/openlcb/nmranet_constants.cxx index 87c9f3465..8476bf031 100644 --- a/src/openlcb/nmranet_constants.cxx +++ b/src/openlcb/nmranet_constants.cxx @@ -39,6 +39,9 @@ DEFAULT_CONST(remote_alias_cache_size, 10); /** Number of entries in the local alias cache */ DEFAULT_CONST(local_alias_cache_size, 3); +/** Keep this many allocated but unused aliases around. */ +DEFAULT_CONST(reserve_unused_alias_count, 0); + /** Maximum number of local nodes */ DEFAULT_CONST(local_nodes_count, 2); @@ -63,3 +66,7 @@ DEFAULT_CONST_FALSE(enable_all_memory_space); * identified messages at boot time. This is required by the OpenLCB * standard. */ DEFAULT_CONST_TRUE(node_init_identify); + +/** How many CAN frames should the bulk alias allocator be sending at the same + * time. */ +DEFAULT_CONST(bulk_alias_num_can_frames, 20); diff --git a/src/openlcb/sources b/src/openlcb/sources index 9f849e954..5e8e4a01a 100644 --- a/src/openlcb/sources +++ b/src/openlcb/sources @@ -8,6 +8,7 @@ CXXSRCS += \ BroadcastTime.cxx \ BroadcastTimeClient.cxx \ BroadcastTimeServer.cxx \ + BulkAliasAllocator.cxx \ CanDefs.cxx \ ConfigEntry.cxx \ ConfigUpdateFlow.cxx \ diff --git a/src/os/MDNS.cxx b/src/os/MDNS.cxx index f84cde383..ffa5e1fa9 100644 --- a/src/os/MDNS.cxx +++ b/src/os/MDNS.cxx @@ -296,7 +296,7 @@ void MDNS::resolve_callback(AvahiServiceResolver *r, sa_in->sin6_flowinfo = 0; sa_in->sin6_family = AF_INET6; sa_in->sin6_port = htons(port); - memcpy(&sa_in->sin6_addr.s6_addr, + memcpy(&(sa_in->sin6_addr.s6_addr), address->data.ipv6.address, sizeof(address->data.ipv6.address)); break; diff --git a/src/os/OSSelectWakeup.cxx b/src/os/OSSelectWakeup.cxx index b040906e9..2fa97bf57 100644 --- a/src/os/OSSelectWakeup.cxx +++ b/src/os/OSSelectWakeup.cxx @@ -1,10 +1,110 @@ +/** \copyright +* Copyright (c) 2015, Balazs Racz +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* - Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* - Redistributions in binary form must reproduce the above copyright notice, +* this list of conditions and the following disclaimer in the documentation +* and/or other materials provided with the distribution. +* +* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +* POSSIBILITY OF SUCH DAMAGE. +* +* \file OSSelectWakeup.cxx +* Helper class for portable wakeup of a thread blocked in a select call. +* +* @author Balazs Racz +* @date 10 Apr 2015 +*/ + #include "os/OSSelectWakeup.hxx" #include "utils/logging.h" +#if defined(__MACH__) +#define _DARWIN_C_SOURCE // pselect +#endif void empty_signal_handler(int) { } +int OSSelectWakeup::select(int nfds, fd_set *readfds, + fd_set *writefds, fd_set *exceptfds, + long long deadline_nsec) +{ + { + AtomicHolder l(this); + inSelect_ = true; + if (pendingWakeup_) + { + deadline_nsec = 0; + } + else + { +#if OPENMRN_FEATURE_DEVICE_SELECT + Device::select_clear(); +#endif + } + } +#if OPENMRN_FEATURE_DEVICE_SELECT + int ret = + Device::select(nfds, readfds, writefds, exceptfds, deadline_nsec); + if (!ret && pendingWakeup_) + { + ret = -1; + errno = EINTR; + } +#elif OPENMRN_HAVE_PSELECT + struct timespec timeout; + timeout.tv_sec = deadline_nsec / 1000000000; + timeout.tv_nsec = deadline_nsec % 1000000000; + int ret = + ::pselect(nfds, readfds, writefds, exceptfds, &timeout, &origMask_); +#elif OPENMRN_HAVE_SELECT +#ifdef ESP32 + fd_set newexcept; + if (!exceptfds) + { + FD_ZERO(&newexcept); + exceptfds = &newexcept; + } + FD_SET(vfsFd_, exceptfds); + if (vfsFd_ >= nfds) { + nfds = vfsFd_ + 1; + } +#endif //ESP32 + struct timeval timeout; + timeout.tv_sec = deadline_nsec / 1000000000; + timeout.tv_usec = (deadline_nsec / 1000) % 1000000; + int ret = + ::select(nfds, readfds, writefds, exceptfds, &timeout); +#elif !defined(OPENMRN_FEATURE_SINGLE_THREADED) + #error no select implementation in multi threaded OS. +#else + // Single threaded OS: nothing to wake up. + int ret = 0; +#endif + { + AtomicHolder l(this); + pendingWakeup_ = false; + inSelect_ = false; + } + return ret; +} + #ifdef ESP32 #include #include diff --git a/src/os/OSSelectWakeup.hxx b/src/os/OSSelectWakeup.hxx index 2471ba8fa..5d284256e 100644 --- a/src/os/OSSelectWakeup.hxx +++ b/src/os/OSSelectWakeup.hxx @@ -181,67 +181,7 @@ public: * asynchronously */ int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, - long long deadline_nsec) - { - { - AtomicHolder l(this); - inSelect_ = true; - if (pendingWakeup_) - { - deadline_nsec = 0; - } - else - { -#if OPENMRN_FEATURE_DEVICE_SELECT - Device::select_clear(); -#endif - } - } -#if OPENMRN_FEATURE_DEVICE_SELECT - int ret = - Device::select(nfds, readfds, writefds, exceptfds, deadline_nsec); - if (!ret && pendingWakeup_) - { - ret = -1; - errno = EINTR; - } -#elif OPENMRN_HAVE_PSELECT - struct timespec timeout; - timeout.tv_sec = deadline_nsec / 1000000000; - timeout.tv_nsec = deadline_nsec % 1000000000; - int ret = - ::pselect(nfds, readfds, writefds, exceptfds, &timeout, &origMask_); -#elif OPENMRN_HAVE_SELECT -#ifdef ESP32 - fd_set newexcept; - if (!exceptfds) - { - FD_ZERO(&newexcept); - exceptfds = &newexcept; - } - FD_SET(vfsFd_, exceptfds); - if (vfsFd_ >= nfds) { - nfds = vfsFd_ + 1; - } -#endif //ESP32 - struct timeval timeout; - timeout.tv_sec = deadline_nsec / 1000000000; - timeout.tv_usec = (deadline_nsec / 1000) % 1000000; - int ret = - ::select(nfds, readfds, writefds, exceptfds, &timeout); -#elif !defined(OPENMRN_FEATURE_SINGLE_THREADED) - #error no select implementation in multi threaded OS. -#else - // Single threaded OS: nothing to wake up. - int ret = 0; -#endif - { - AtomicHolder l(this); - pendingWakeup_ = false; - inSelect_ = false; - } - return ret; - } + long long deadline_nsec); private: #ifdef ESP32 diff --git a/src/utils/ActivityLed.hxx b/src/utils/ActivityLed.hxx new file mode 100644 index 000000000..0e78cd145 --- /dev/null +++ b/src/utils/ActivityLed.hxx @@ -0,0 +1,88 @@ +/** \copyright + * Copyright (c) 2020, Balazs Racz + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * \file ActivityLed.hxx + * + * State flow that controls an activity LED based on triggers of events. + * + * @author Balazs Racz + * @date 26 Sep 2020 + */ + +#ifndef _UTILS_ACTIVITYLED_HXX_ +#define _UTILS_ACTIVITYLED_HXX_ + +#include "executor/StateFlow.hxx" +#include "os/Gpio.hxx" + +/// Operates an LED to visually display some activity. When the activity() +/// function is called at least once within a period, we turn the LED on for +/// the next period, if no call was made, the LED turns off for the next +/// period. +class ActivityLed : private ::Timer +{ +public: + /// Constructor. + /// @param service defines which executor this timer should be running on. + /// @param pin the LED of the output. Will be high for activity, low for + /// inactivity. Use InvertedGPIO if needed. + /// @param period defines in nanosecond the time to spend between updates. + ActivityLed( + Service *service, const Gpio *pin, long long period = MSEC_TO_NSEC(33)) + : ::Timer(service->executor()->active_timers()) + , gpio_(pin) + { + start(period); + } + + /// Call this function when activity happens. + void activity() + { + triggerCount_++; + } + +private: + long long timeout() override + { + if (triggerCount_) + { + gpio_->write(true); + triggerCount_ = 0; + } + else + { + gpio_->write(false); + } + return RESTART; + } + + /// Output pin to blink for activity. + const Gpio *gpio_; + /// How many triggers happened since the last run of the timer. + unsigned triggerCount_ {0}; +}; + +#endif // _UTILS_ACTIVITYLED_HXX_ diff --git a/src/utils/BandwidthMerger.cxxtest b/src/utils/BandwidthMerger.cxxtest new file mode 100644 index 000000000..a7fa83578 --- /dev/null +++ b/src/utils/BandwidthMerger.cxxtest @@ -0,0 +1,92 @@ +/** \copyright + * Copyright (c) 2020, Balazs Racz + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * \file BandwidthMerger.cxxtest + * + * Unit tests for the stride scheduler. + * + * @author Balazs Racz + * @date 10 Oct 2020 + */ + +#include "utils/BandwidthMerger.hxx" + +#include "utils/test_main.hxx" + +class BandwidthMergerTest : public ::testing::Test +{ +protected: + /// Runs a simulation with a certain number of steps with a bandwidth + /// merger. + void collect_stats(uint8_t percent, unsigned num_steps) + { + BandwidthMerger m {percent}; + results_[0] = results_[1] = 0; + for (unsigned i = 0; i < num_steps; ++i) + { + if (m.step()) + { + ++results_[1]; + } + else + { + ++results_[0]; + } + } + } + + /// Results of the simulation. [0] is the number of steps with false + /// output, [1] is the number of steps with true output. + unsigned results_[2]; +}; + +TEST_F(BandwidthMergerTest, fifty) +{ + collect_stats(50, 34); + EXPECT_EQ(17u, results_[0]); + EXPECT_EQ(17u, results_[1]); +} + +TEST_F(BandwidthMergerTest, hundred) +{ + collect_stats(100, 34); + EXPECT_EQ(0u, results_[0]); + EXPECT_EQ(34u, results_[1]); +} + +TEST_F(BandwidthMergerTest, zero) +{ + collect_stats(0, 34); + EXPECT_EQ(34u, results_[0]); + EXPECT_EQ(0u, results_[1]); +} + +TEST_F(BandwidthMergerTest, ten) +{ + collect_stats(10, 34); + EXPECT_EQ(31u, results_[0]); + EXPECT_EQ(3u, results_[1]); +} diff --git a/src/utils/BandwidthMerger.hxx b/src/utils/BandwidthMerger.hxx new file mode 100644 index 000000000..caf32a2d4 --- /dev/null +++ b/src/utils/BandwidthMerger.hxx @@ -0,0 +1,89 @@ +/** \copyright + * Copyright (c) 2020, Balazs Racz + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * \file BandwidthMerger.hxx + * + * Simple stride scheduler for splitting bandwidth. + * + * @author Balazs Racz + * @date 10 Oct 2020 + */ + +#ifndef _UTILS_BANDWIDTHMERGER_HXX_ +#define _UTILS_BANDWIDTHMERGER_HXX_ + +#include + +#include "utils/macros.h" + +/// Simple stride scheduler. Emulates a two-way split of bandwidth between a +/// first source and a second source. The percentage assigned to the first +/// source is a parameter. Over time the fraction of outputs selected from the +/// first source converges to this percentage. +struct BandwidthMerger +{ + /// Constructor. + /// @param percent is the percentage (0..100) of the bandwidth that + /// should be assigned to the first source. + BandwidthMerger(uint8_t percent) + : percentFirst_(percent) + { + HASSERT(percentFirst_ <= 100); + } + /// Runs one step. + /// @return true if the first source is selected in this step. + bool step() + { + currentState_ += percentFirst_; + if (currentState_ >= 100) + { + currentState_ -= 100; + return true; + } + return false; + } + + /// If the step function returned false, but still the first source was + /// taken (e.g. because the second source was empty), call this function to + /// clear the state. This will avoid selecting the first source twice in a + /// row for example. + void reset() + { + // Reduces the state by 100 but clips it to zero. Since the state is + // always < 100, the clipping will always win. + currentState_ = 0; + } + + /// State of the current stride. This is always between 0..99. It + /// represents the fractional steps that the first source has accumulated + /// but not paid out in the form of selections yet. + uint8_t currentState_ {0}; + /// Percentage of the bandwidth that should be assigned to the first + /// source. Range 0..100. + uint8_t percentFirst_; +}; + +#endif // _UTILS_BANDWIDTHMERGER_HXX_ diff --git a/src/utils/ClientConnection.hxx b/src/utils/ClientConnection.hxx index 60973ddde..35121c2bd 100644 --- a/src/utils/ClientConnection.hxx +++ b/src/utils/ClientConnection.hxx @@ -35,11 +35,13 @@ #ifndef _UTILS_CLIENTCONNECTION_HXX_ #define _UTILS_CLIENTCONNECTION_HXX_ -#include "utils/GridConnectHub.hxx" #include #include /* tc* functions */ #include +#include "utils/GridConnectHub.hxx" +#include "utils/socket_listener.hxx" + /// Abstract base class for the Hub's connections. class ConnectionClient { diff --git a/src/utils/EntryModel.hxx b/src/utils/EntryModel.hxx index b6c834f48..6a95495c4 100644 --- a/src/utils/EntryModel.hxx +++ b/src/utils/EntryModel.hxx @@ -120,8 +120,7 @@ public: } break; } - strncpy(data_, str.c_str(), sizeof(data_) - 1); - data_[sizeof(data_) - 1] = '\0'; + str_populate(data_, str.c_str()); hasInitial_ = true; } diff --git a/src/utils/FileUtils.cxx b/src/utils/FileUtils.cxx index c72b38011..979769910 100644 --- a/src/utils/FileUtils.cxx +++ b/src/utils/FileUtils.cxx @@ -106,10 +106,15 @@ void write_string_to_file(const string &filename, const string &data) while ((nr = fwrite(data.data() + offset, 1, data.size() - offset, f)) > 0) { offset += nr; - if (offset >= data.size()) break; + if (offset >= data.size()) + { + break; + } } - if (nr < 0) { - fprintf(stderr, "error writing: %s\n", strerror(errno)); + if (offset != data.size()) + { + fprintf(stderr, "error writing: %s, offset: %zu, size: %zu\n", + strerror(errno), offset, data.size()); } fclose(f); } diff --git a/src/utils/GcTcpHub.cxx b/src/utils/GcTcpHub.cxx index 6f94a510a..99ec6e1fe 100644 --- a/src/utils/GcTcpHub.cxx +++ b/src/utils/GcTcpHub.cxx @@ -38,17 +38,30 @@ #include "nmranet_config.h" #include "utils/GridConnectHub.hxx" -void GcTcpHub::OnNewConnection(int fd) +void GcTcpHub::on_new_connection(int fd) { const bool use_select = (config_gridconnect_tcp_use_select() == CONSTANT_TRUE); - create_gc_port_for_can_hub(canHub_, fd, nullptr, use_select); + { + AtomicHolder h(this); + numClients_++; + } + create_gc_port_for_can_hub(canHub_, fd, this, use_select); +} + +void GcTcpHub::notify() +{ + AtomicHolder h(this); + if (numClients_) + { + numClients_--; + } } GcTcpHub::GcTcpHub(CanHubFlow *can_hub, int port) : canHub_(can_hub) - , tcpListener_(port, std::bind(&GcTcpHub::OnNewConnection, this, - std::placeholders::_1)) + , tcpListener_(port, + std::bind(&GcTcpHub::on_new_connection, this, std::placeholders::_1)) { } diff --git a/src/utils/GcTcpHub.cxxtest b/src/utils/GcTcpHub.cxxtest index e8a1f6ba5..d24c6a8f3 100644 --- a/src/utils/GcTcpHub.cxxtest +++ b/src/utils/GcTcpHub.cxxtest @@ -64,6 +64,7 @@ protected: fprintf(stderr, "waiting for exiting.\r"); usleep(100000); } + EXPECT_EQ(0U, tcpHub_.get_num_clients()); } struct Client @@ -154,6 +155,7 @@ protected: TEST_F(GcTcpHubTest, CreateDestroy) { + EXPECT_EQ(0u, tcpHub_.get_num_clients()); } TEST_F(GcTcpHubTest, TwoClientsPingPong) @@ -165,6 +167,9 @@ TEST_F(GcTcpHubTest, TwoClientsPingPong) writeline(b.fd_, ":S001N01;"); EXPECT_EQ(":S001N01;", readline(a.fd_, ';')); EXPECT_EQ(3U, can_hub0.size()); + + EXPECT_EQ(2u, tcpHub_.get_num_clients()); + // Test writing outwards. send_packet(":S002N0102;"); EXPECT_EQ(":S002N0102;", readline(a.fd_, ';')); @@ -177,6 +182,7 @@ TEST_F(GcTcpHubTest, ClientCloseExpect) unsigned can_hub_size = can_hub0.size(); LOG(INFO, "can hub: %p ", &can_hub0); EXPECT_EQ(1U, can_hub_size); + EXPECT_EQ(0U, tcpHub_.get_num_clients()); { Client a; Client b; @@ -184,6 +190,7 @@ TEST_F(GcTcpHubTest, ClientCloseExpect) writeline(b.fd_, ":S001N01;"); EXPECT_EQ(":S001N01;", readline(a.fd_, ';')); EXPECT_EQ(can_hub_size + 2, can_hub0.size()); + EXPECT_EQ(2U, tcpHub_.get_num_clients()); wait(); } // Test writing outwards. diff --git a/src/utils/GcTcpHub.hxx b/src/utils/GcTcpHub.hxx index f81d55bbf..04888ed1f 100644 --- a/src/utils/GcTcpHub.hxx +++ b/src/utils/GcTcpHub.hxx @@ -43,7 +43,7 @@ class ExecutorBase; * format. Any new incoming connection will be wired into the same virtual CAN * hub. All packets will be forwarded to every participant, without * loopback. */ -class GcTcpHub +class GcTcpHub : private Notifiable, private Atomic { public: /// Constructor. @@ -60,16 +60,28 @@ public: return tcpListener_.is_started(); } + /// @return currently connected client count. + unsigned get_num_clients() + { + return numClients_; + } + private: /// Callback when a new connection arrives. /// /// @param fd filedes of the freshly established incoming connection. /// - void OnNewConnection(int fd); + void on_new_connection(int fd); + + /// Error callback from the gridconnect socket. This is invoked when a + /// client disconnects. + void notify() override; /// @param can_hub Which CAN-hub should we attach the TCP gridconnect hub /// onto. CanHubFlow *canHub_; + /// How many clients are connected right now. + unsigned numClients_ {0}; /// Helper object representing the listening on the socket. SocketListener tcpListener_; }; diff --git a/src/utils/LruCounter.cxxtest b/src/utils/LruCounter.cxxtest new file mode 100644 index 000000000..cbecefadd --- /dev/null +++ b/src/utils/LruCounter.cxxtest @@ -0,0 +1,293 @@ +/** \copyright + * Copyright (c) 2020, Balazs Racz + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * \file LruCounter.cxxtest + * + * Unit tests for LruCounter. + * + * @author Balazs Racz + * @date 18 Sep 2020 + */ + +#include "utils/LruCounter.hxx" + +#include "utils/test_main.hxx" + +class LruCounterTest : public ::testing::Test +{ +protected: + GlobalLruCounter global_; + + void tick_n(unsigned n) + { + for (unsigned i = 0; i < n; ++i) + { + global_.tick(); + cb1.tick(global_); + cb2.tick(global_); + cb3.tick(global_); + cb4.tick(global_); + cs1.tick(global_); + cs2.tick(global_); + cs3.tick(global_); + cs4.tick(global_); + } + } + + void set_bits_per_bit(unsigned bpb) + { + new (&global_) GlobalLruCounter(bpb); + } + + /// Runs the sequence test on a given set of counters. + /// @param entries the counters + /// @param num_tick how much to wait between resetting each counter. + template + void sequence_test(std::initializer_list entries, unsigned num_tick) + { + for (T *e : entries) + { + EXPECT_EQ(0u, e->value()); + } + for (unsigned i = 1; i < entries.size(); i++) + { + tick_n(num_tick); + entries.begin()[i]->touch(); + } + tick_n(num_tick); + + for (unsigned i = 1; i < entries.size(); i++) + { + EXPECT_GT( + entries.begin()[i - 1]->value(), entries.begin()[i]->value()); + } + } + + /// Expects that an entry is going to flip forward to the next value in + /// num_tick counts. + template void next_increment(T *entry, unsigned num_tick) + { + LOG(INFO, "Next increment from %u", entry->value()); + unsigned current = entry->value(); + tick_n(num_tick - 1); + EXPECT_EQ(current, entry->value()); + tick_n(1); + EXPECT_EQ(current + 1, entry->value()); + } + + /// Byte sized LRU counters for testing. + LruCounter cb1, cb2, cb3, cb4; + /// Short sized LRU counters for testing. + LruCounter cs1, cs2, cs3, cs4; +}; + +TEST_F(LruCounterTest, create) +{ +} + +/// Tests that the initial value is zero and the reset value is zero. +TEST_F(LruCounterTest, initial) +{ + EXPECT_EQ(0u, cb1.value()); + EXPECT_EQ(0u, cs1.value()); + + cb1.touch(); + cs1.touch(); + + EXPECT_EQ(0u, cb1.value()); + EXPECT_EQ(0u, cs1.value()); +} + +/// Increments a counter through the first few values, which take exponentially +/// increasing tick count. +TEST_F(LruCounterTest, simple_increment) +{ + set_bits_per_bit(1); + EXPECT_EQ(0u, cb1.value()); + tick_n(1); // 1 + EXPECT_EQ(1u, cb1.value()); + tick_n(1); // 2 + EXPECT_EQ(2u, cb1.value()); + tick_n(2); // 4 + EXPECT_EQ(3u, cb1.value()); + tick_n(4); // 8 + EXPECT_EQ(4u, cb1.value()); + tick_n(8); // 16 + EXPECT_EQ(5u, cb1.value()); +} + +/// Increments a 16-bit counter through the first few values, which take +/// exponentially increasing tick count. +TEST_F(LruCounterTest, simple_increment_short) +{ + set_bits_per_bit(1); + EXPECT_EQ(0u, cs1.value()); + tick_n(1); // 1 + EXPECT_EQ(1u, cs1.value()); + tick_n(1); // 2 + EXPECT_EQ(2u, cs1.value()); + tick_n(2); // 4 + EXPECT_EQ(3u, cs1.value()); + tick_n(4); // 8 + EXPECT_EQ(4u, cs1.value()); + tick_n(8); // 16 + EXPECT_EQ(5u, cs1.value()); +} + +/// Increments a 2 bit/bit counter through the first few values, which take +/// exponentially increasing tick count. +TEST_F(LruCounterTest, simple_increment_2bit) +{ + EXPECT_EQ(0u, cb1.value()); + next_increment(&cb1, 1); // old value = 0, next tick = 1 + next_increment(&cb1, 3); // old value = 1, next tick = 4 + next_increment(&cb1, 12); // old value = 2, next tick = 16 + next_increment(&cb1, 16); // old value = 3, next tick = 32 + next_increment(&cb1, 32); // old value = 4, next tick = 64 + next_increment(&cb1, 64); // old value = 5, next tick = 128 + next_increment(&cb1, 64); // old value = 6, next tick = 192 + next_increment(&cb1, 64); // old value = 7, next tick = 256 + next_increment(&cb1, 256); // old value = 8, next tick = 512 +} + +/// Increments a 16-bit 2 bit/bit counter through the first few values, which +/// take exponentially increasing tick count. +TEST_F(LruCounterTest, simple_increment_short_2bit) +{ + EXPECT_EQ(0u, cs1.value()); + next_increment(&cs1, 1); // old value = 0, next tick = 1 + next_increment(&cs1, 3); // old value = 1, next tick = 4 + next_increment(&cs1, 12); // old value = 2, next tick = 16 + next_increment(&cs1, 16); // old value = 3, next tick = 32 + next_increment(&cs1, 32); // old value = 4, next tick = 64 + next_increment(&cs1, 64); // old value = 5, next tick = 128 + next_increment(&cs1, 64); // old value = 6, next tick = 192 + next_increment(&cs1, 64); // old value = 7, next tick = 256 + next_increment(&cs1, 256); // old value = 8, next tick = 512 +} + +/// Saturates a byte sized counter and expects that no overflow has happened. +TEST_F(LruCounterTest, no_overflow) +{ + set_bits_per_bit(1); + EXPECT_EQ(0u, cb1.value()); + tick_n(100000); + EXPECT_EQ(255u, cb1.value()); + tick_n(1); + EXPECT_EQ(255u, cb1.value()); + tick_n(100000); + EXPECT_EQ(255u, cb1.value()); +} + +/// Checks that a 2 bit/bit exponent bytes sized counter can count more than a +/// few 100k ticks. +TEST_F(LruCounterTest, byte_range) +{ + set_bits_per_bit(2); + EXPECT_EQ(0u, cb1.value()); + tick_n(100000); + EXPECT_EQ(52u, cb1.value()); + tick_n(100000); + EXPECT_EQ(67u, cb1.value()); +} + +/// Tests resetting the counter, then incrementing. +TEST_F(LruCounterTest, reset) +{ + set_bits_per_bit(1); + EXPECT_EQ(0u, cb1.value()); + tick_n(16); + EXPECT_EQ(5u, cb1.value()); + + cb1.touch(); + EXPECT_EQ(0u, cb1.value()); + tick_n(1); // 1 + EXPECT_EQ(1u, cb1.value()); + tick_n(1); // 2 + EXPECT_EQ(2u, cb1.value()); + tick_n(2); // 4 + EXPECT_EQ(3u, cb1.value()); + tick_n(4); // 8 + EXPECT_EQ(4u, cb1.value()); + tick_n(8); // 16 + EXPECT_EQ(5u, cb1.value()); +} + +/// Tests several counters that were reset at different times. Their values +/// should be monotonic from their reset time. +TEST_F(LruCounterTest, sequence) +{ + set_bits_per_bit(1); + EXPECT_EQ(0u, cb1.value()); + EXPECT_EQ(0u, cb2.value()); + EXPECT_EQ(0u, cb3.value()); + EXPECT_EQ(0u, cb4.value()); + + cb1.touch(); + tick_n(50); + cb2.touch(); + tick_n(50); + cb3.touch(); + tick_n(50); + cb4.touch(); + tick_n(50); + + EXPECT_GT(cb1.value(), cb2.value()); + EXPECT_GT(cb2.value(), cb3.value()); + EXPECT_GT(cb3.value(), cb4.value()); +} + +/// Tests several counters that were reset at different times. Their values +/// should be monotonic from their reset time. 1-byte, 1-bit-per-bit exponent +TEST_F(LruCounterTest, sequence_byte_1) +{ + set_bits_per_bit(1); + sequence_test({&cb1, &cb2, &cb3, &cb4}, 50); +} + +/// Tests several counters that were reset at different times. Their values +/// should be monotonic from their reset time. 2-byte, 1-bit-per-bit exponent +TEST_F(LruCounterTest, sequence_short_1) +{ + set_bits_per_bit(1); + sequence_test({&cs1, &cs2, &cs3, &cs4}, 50); +} + +/// Tests several counters that were reset at different times. Their values +/// should be monotonic from their reset time. 1-byte 2-bit-per-bit exponent +TEST_F(LruCounterTest, sequence_byte_2) +{ + set_bits_per_bit(2); + sequence_test({&cb1, &cb2, &cb3, &cb4}, 400); +} + +/// Tests several counters that were reset at different times. Their values +/// should be monotonic from their reset time. 2-byte 2-bit-per-bit exponent +TEST_F(LruCounterTest, sequence_short_2) +{ + set_bits_per_bit(2); + sequence_test({&cs1, &cs2, &cs3, &cs4}, 400); +} diff --git a/src/utils/LruCounter.hxx b/src/utils/LruCounter.hxx new file mode 100644 index 000000000..aec479ff0 --- /dev/null +++ b/src/utils/LruCounter.hxx @@ -0,0 +1,169 @@ +/** \copyright + * Copyright (c) 2020, Balazs Racz + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * \file LruCounter.hxx + * + * A monotonic counter that is usable for approximate LRU age determination. + * + * @author Balazs Racz + * @date 18 Sep 2020 + */ + +#ifndef _UTILS_LRUCOUNTER_HXX_ +#define _UTILS_LRUCOUNTER_HXX_ + +#include + +template class LruCounter; + +/// The GlobalLruCounter and a set of LruCounter<> objects cooperate in order +/// to create an approximate LRU order over a set of objects. The particular +/// optimization criterion is that the memory storage per object should be very +/// low. (Target is one byte.) Touching an object is constant time, but there +/// is linear time background maintenance operations that need to run +/// regularly. Picking the oldest object is linear time. The oldest concept is +/// an approximation in that the older an object becomes the less time +/// granularity is available to distinguish exact age. This is generally fine +/// in applications. +/// +/// How to use: +/// +/// Create one GlobalLruCounter. Create for each tracked object an +/// LruCounter or LruCounter. +/// +/// Periodically call the tick() function once on the GlobalLruCounter, then +/// for each live object the tick(global) function. This is linear cost in the +/// number of tracked objects, so do it rather rarely (e.g. once per second). +/// +/// When a specific object is used, call the touch() function on it. +/// +/// When the oldest object needs to be selected, pick the one which has the +/// highest returned value() from its LruCounter<>. +/// +/// Theory of operation: +/// +/// The GlobalLruCounter maintains a global tick count. It gets incremented by +/// one in each tick. In the per-object local counter we only increment the +/// counter for a subset of the global ticks. How many global ticks we skip +/// between two local counter increments depends on the age of the object. The +/// older an object becomes the more rarely we increment the object's counter. +/// +/// Specifically, if the object counter has reached to be k bits long, then we +/// only increment it, when the global counter's bottom k bits are all +/// zero. Example: if the object counter is 35 (6 bits long), then we increment +/// it to 36 when the global counter is divisible by 64 (all 6 bottom bits are +/// zero). In a variant we double the zero-bits requirement, needing that the +/// bottom 12 bits are all zero. +/// +/// Example calculations, assuming 1 tick per second: +/// +/// +-------------------------------------------------------------------+ +/// | Exponent 1 bit/bit 2 bits/bit | +/// +------------+------------------------------------------------------+ +/// | data type: | | +/// | | | +/// | uint8_t | max count: ~43k max count: 9.5M | +/// | | (0.5 days) (110 days) | +/// | | end granularity: 256 end granularity: 64k | +/// | | (4 min) (0.5 days) | +/// +------------+------------------------------------------------------+ +/// | uint16_t | max count: ~2.8B max count: 161T | +/// | | (100 years) (5M years) | +/// | | end granularity: 64k end granularity: 4B | +/// | | (0.5 days) (136 years) | +/// +------------+------------------------------------------------------+ +class GlobalLruCounter +{ +public: + /// Constructor. + /// @param bits_per_bit How aggressive the exponential downsampling should + /// be. Meaningful values are 1 and 2. + GlobalLruCounter(unsigned bits_per_bit = 2) + : bitsPerBit_(bits_per_bit) + { + } + void tick() + { + ++tick_; + } + +private: + template friend class LruCounter; + /// Setting defining the exponent. + unsigned bitsPerBit_; + /// Rolling counter of global ticks. This is used by the local counters to + /// synchronize their increments. + unsigned tick_ {0}; +}; + +/// Create an instance of this type for each object whose age needs to be +/// measured with the GlobalLruCounter. For further details, see +/// { \link GlobalLruCounter }. +/// @param T is the storage type, typically uint8_t or uint16_t. +template class LruCounter +{ +public: + /// @return A value monotonic in the age of the current counter. + unsigned value() + { + return counter_; + } + + /// Increments the local counter. + /// @param global reference to the global tick counter. All calls must use + /// the same global counter. + void tick(const GlobalLruCounter &global) + { + if (!counter_) + { + ++counter_; + return; + } + if (counter_ == std::numeric_limits::max()) + { + // Counter is saturated. + return; + } + int nlz = __builtin_clz((unsigned)counter_); + int needzero = (32 - nlz) * global.bitsPerBit_; + if ((global.tick_ & ((1U << needzero) - 1)) == 0) + { + ++counter_; + } + } + + /// Signals that the object has been used now. + void touch() + { + counter_ = 0; + } + +private: + /// Internal counter. + T counter_ {0}; +}; + +#endif // _UTILS_LRUCOUNTER_HXX_ diff --git a/src/utils/MakeUnique.hxx b/src/utils/MakeUnique.hxx new file mode 100644 index 000000000..2e3b8e7ad --- /dev/null +++ b/src/utils/MakeUnique.hxx @@ -0,0 +1,50 @@ +/** + * \file MakeUnique.hxx + * + * C++11 version of std::make_unique which is only available from c++14 or + * later. + * + * This is based on https://isocpp.org/files/papers/N3656.txt. + * + * The __cplusplus constant reference is from: + * http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2014/n3938.html + * + */ + +// Check if we are building with less than C++14 and if so we need to define +// the std::make_unique() API. +#if __cplusplus < 201402L + +#include +#include +#include + +namespace std +{ + +template +unique_ptr make_unique_helper(false_type, Args&&... args) +{ + return unique_ptr(new T(forward(args)...)); +} + +template +unique_ptr make_unique_helper(true_type, Args&&... args) +{ + static_assert(extent::value == 0, + "make_unique() is forbidden, please use make_unique()."); + + typedef typename remove_extent::type U; + return unique_ptr(new U[sizeof...(Args)]{forward(args)...}); +} + +template +unique_ptr make_unique(Args&&... args) +{ + return make_unique_helper(is_array(), forward(args)...); +} + +} + +#endif // __cplusplus < 201402L + diff --git a/src/utils/Queue.hxx b/src/utils/Queue.hxx index 3a81d5f8e..bf7819c5d 100644 --- a/src/utils/Queue.hxx +++ b/src/utils/Queue.hxx @@ -38,6 +38,7 @@ #include #include +#include "openmrn_features.h" #include "executor/Executable.hxx" #include "executor/Notifiable.hxx" #include "os/OS.hxx" @@ -868,7 +869,7 @@ public: return result; } -#if !(defined(ESP_NONOS) || defined(ARDUINO)) +#if OPENMRN_FEATURE_SEM_TIMEDWAIT /** Wait for an item from the front of the queue. * @param timeout time to wait in nanoseconds * @return item retrieved from queue, else NULL with errno set: diff --git a/src/utils/SocketCan.cxx b/src/utils/SocketCan.cxx new file mode 100644 index 000000000..791b46c8a --- /dev/null +++ b/src/utils/SocketCan.cxx @@ -0,0 +1,101 @@ +/** \copyright + * Copyright (c) 2020, Balazs Racz + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * \file SocketCan.cxx + * + * Helper functions to connect to CAN devices via SocketCan. + * + * @author Balazs Racz + * @date 1 Sep 2020 + */ + +#include "utils/SocketCan.hxx" +#include "can_frame.h" +#include + +#if defined(__linux__) + +#include +#include +#include +#include +#include + +/// This macro executes an OS call, and if it returns negative result, then +/// prints the errno to stderr, and terminates the current function with -1 +/// return value. +/// @param where textual description of what function was called +/// (e.g. "socket") +/// @param x... the function call. +#define ERRNOLOG(where, x...) \ + do \ + { \ + if ((x) < 0) \ + { \ + perror(where); \ + return -1; \ + } \ + } while (0) + +int socketcan_open(const char *device, int loopback) +{ + int s; + struct sockaddr_can addr; + struct ifreq ifr; + + s = socket(PF_CAN, SOCK_RAW, CAN_RAW); + ERRNOLOG("socket", s); + + // Set the blocking limit to the minimum allowed, typically 1024 in Linux + int sndbuf = 0; + ERRNOLOG("setsockopt(sndbuf)", + setsockopt(s, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf))); + + // turn on/off loopback + ERRNOLOG("setsockopt(loopback)", + setsockopt( + s, SOL_CAN_RAW, CAN_RAW_LOOPBACK, &loopback, sizeof(loopback))); + + // setup error notifications + can_err_mask_t err_mask = CAN_ERR_TX_TIMEOUT | CAN_ERR_LOSTARB | + CAN_ERR_CRTL | CAN_ERR_PROT | CAN_ERR_TRX | CAN_ERR_ACK | + CAN_ERR_BUSOFF | CAN_ERR_BUSERROR | CAN_ERR_RESTARTED; + ERRNOLOG("setsockopt(filter)", + setsockopt( + s, SOL_CAN_RAW, CAN_RAW_ERR_FILTER, &err_mask, sizeof(err_mask))); + strcpy(ifr.ifr_name, device); + + ERRNOLOG("interface set", ::ioctl(s, SIOCGIFINDEX, &ifr)); + + addr.can_family = AF_CAN; + addr.can_ifindex = ifr.ifr_ifindex; + + ERRNOLOG("bind", bind(s, (struct sockaddr *)&addr, sizeof(addr))); + + return s; +} + +#endif diff --git a/src/utils/SocketCan.hxx b/src/utils/SocketCan.hxx new file mode 100644 index 000000000..9e14354b4 --- /dev/null +++ b/src/utils/SocketCan.hxx @@ -0,0 +1,49 @@ +/** \copyright + * Copyright (c) 2020, Balazs Racz + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * \file SocketCan.hxx + * + * Helper functions to connect to CAN devices via SocketCan. + * + * @author Balazs Racz + * @date 1 Sep 2020 + */ + +#ifndef _UTILS_SOCKETCAN_HXX_ +#define _UTILS_SOCKETCAN_HXX_ + +#if defined(__linux__) + +/// Opens a SocketCan socket. +/// @param device the name of the CAN device, e.g. can0 +/// @param loopback 1 to enable loopback locally to other open references, +/// 0 to disable loopback locally to other open references. +/// @return an open socket file descriptor, or -1 if there was an error. +int socketcan_open(const char *device, int loopback); + +#endif + +#endif // _UTILS_SOCKETCAN_HXX_ diff --git a/src/utils/SocketClient.cxxtest b/src/utils/SocketClient.cxxtest index 199bfd560..1277fb50c 100644 --- a/src/utils/SocketClient.cxxtest +++ b/src/utils/SocketClient.cxxtest @@ -151,9 +151,9 @@ TEST_F(SocketClientTest, connect_mdns) std::bind(&SocketClientTest::connect_callback, this, _1, _2))); EXPECT_CALL(*this, connect_callback(_, sc_.get())).Times(1); - usleep(10000); + usleep(20000); wait(); - usleep(10000); + usleep(20000); wait(); } diff --git a/src/utils/SocketClient.hxx b/src/utils/SocketClient.hxx index f649e9ad0..4a0b33eed 100644 --- a/src/utils/SocketClient.hxx +++ b/src/utils/SocketClient.hxx @@ -47,6 +47,7 @@ #endif #include #include +#include #include "executor/StateFlow.hxx" #include "executor/Timer.hxx" diff --git a/src/utils/SortedListMap.hxx b/src/utils/SortedListMap.hxx index 293727d58..7a65bc45d 100644 --- a/src/utils/SortedListMap.hxx +++ b/src/utils/SortedListMap.hxx @@ -59,10 +59,19 @@ public: /// Const Iterator type. typedef typename container_type::const_iterator const_iterator; - SortedListSet() + template + SortedListSet(Args &&...args) + : cmp_(std::forward(args)...) { } + /// Ensures that a given size can be reached without memory allocation. + /// @param sz the number of entries to prepare for. + void reserve(size_t sz) + { + container_.reserve(sz); + } + /// @return first iterator. iterator begin() { @@ -86,8 +95,8 @@ public: iterator lower_bound(key_type key) { lazy_init(); - return std::lower_bound(container_.begin(), container_.end(), key, - CMP()); + return std::lower_bound( + container_.begin(), container_.end(), key, cmp_); } /// @param key what to search for @return iterator, see std::upper_bound. @@ -95,8 +104,8 @@ public: iterator upper_bound(key_type key) { lazy_init(); - return std::upper_bound(container_.begin(), container_.end(), key, - CMP()); + return std::upper_bound( + container_.begin(), container_.end(), key, cmp_); } /// Searches for a single entry. @param key is what to search for. @return @@ -116,8 +125,8 @@ public: std::pair equal_range(key_type key) { lazy_init(); - return std::equal_range(container_.begin(), container_.end(), key, - CMP()); + return std::equal_range( + container_.begin(), container_.end(), key, cmp_); } /// Adds new entry to the vector. @@ -143,7 +152,8 @@ public: } /// Removes all entries. - void clear() { + void clear() + { container_.clear(); sortedCount_ = 0; } @@ -154,7 +164,7 @@ private: { if (sortedCount_ != container_.size()) { - sort(container_.begin(), container_.end(), CMP()); + sort(container_.begin(), container_.end(), cmp_); sortedCount_ = container_.size(); } } @@ -162,6 +172,9 @@ private: /// Holds the actual data elements. container_type container_; + /// Comparator instance. + CMP cmp_; + /// The first this many elements in the container are already sorted. size_t sortedCount_{0}; }; diff --git a/src/utils/StoredBitSet.cxxtest b/src/utils/StoredBitSet.cxxtest index f0cf54efe..cd67e6212 100644 --- a/src/utils/StoredBitSet.cxxtest +++ b/src/utils/StoredBitSet.cxxtest @@ -54,7 +54,7 @@ TEST(ShadowedBitSetSingleTest, size) } class BitSetMultiTest - : public ::testing::TestWithParam> + : public ::testing::TestWithParam> { protected: BitSetMultiTest() @@ -63,11 +63,11 @@ protected: unsigned get_size() { - return std::tr1::get<0>(GetParam()); + return std::get<0>(GetParam()); } uint8_t get_granularity() { - return std::tr1::get<1>(GetParam()); + return std::get<1>(GetParam()); } #define expect_all_zero(x...) \ diff --git a/src/utils/async_if_test_helper.hxx b/src/utils/async_if_test_helper.hxx index 2355a9b0b..f2af652f9 100644 --- a/src/utils/async_if_test_helper.hxx +++ b/src/utils/async_if_test_helper.hxx @@ -378,19 +378,19 @@ protected: * alias. */ void create_allocated_alias() { - inject_allocated_alias(0x33A, true); + inject_allocated_alias(0x33A); aliasSeed_ = 0x44C; pendingAliasAllocation_ = false; } - void inject_allocated_alias(NodeAlias alias, bool repeat = false) + void inject_allocated_alias(NodeAlias alias) { if (!ifCan_->alias_allocator()) { ifCan_->set_alias_allocator( new AliasAllocator(TEST_NODE_ID, ifCan_.get())); } - run_x([this, alias, repeat]() { - ifCan_->alias_allocator()->TEST_add_allocated_alias(alias, repeat); + run_x([this, alias]() { + ifCan_->alias_allocator()->TEST_add_allocated_alias(alias); }); } diff --git a/src/utils/constants.cxx b/src/utils/constants.cxx index 6f654a2d0..d25f90ca1 100644 --- a/src/utils/constants.cxx +++ b/src/utils/constants.cxx @@ -152,3 +152,15 @@ DEFAULT_CONST(gridconnect_bridge_max_incoming_packets, 1); DEFAULT_CONST(gridconnect_bridge_max_outgoing_packets, 1); DEFAULT_CONST_FALSE(gridconnect_tcp_use_select); + +#ifdef ESP32 +/// Use a stack size of 3kb for SocketListener tasks. +DEFAULT_CONST(socket_listener_stack_size, 3072); +/// Allow one socket to be pending for accept() in SocketListener. +DEFAULT_CONST(socket_listener_backlog, 1); +#else +/// Use a stack size of 1000 for SocketListener tasks. +DEFAULT_CONST(socket_listener_stack_size, 1000); +/// Allow up to five sockets to be pending for accept() in SocketListener. +DEFAULT_CONST(socket_listener_backlog, 5); +#endif \ No newline at end of file diff --git a/src/utils/format_utils.cxx b/src/utils/format_utils.cxx index dd96df507..d763e46a4 100644 --- a/src/utils/format_utils.cxx +++ b/src/utils/format_utils.cxx @@ -35,6 +35,14 @@ #include "utils/macros.h" #include "utils/format_utils.hxx" +/// Translates a number 0..15 to a hex character. +/// @param nibble input number +/// @return character in 0-9a-f +static char nibble_to_hex(unsigned nibble) +{ + return nibble <= 9 ? '0' + nibble : 'a' + (nibble - 10); +} + char* unsigned_integer_to_buffer_hex(unsigned int value, char* buffer) { int num_digits = 0; @@ -50,16 +58,8 @@ char* unsigned_integer_to_buffer_hex(unsigned int value, char* buffer) do { HASSERT(num_digits >= 0); - unsigned int tmp2 = tmp % 16; - if (tmp2 <= 9) - { - buffer[num_digits--] = '0' + tmp2; - } - else - { - buffer[num_digits--] = 'a' + (tmp2 - 10); - } - tmp /= 16; + buffer[num_digits--] = nibble_to_hex(tmp & 0xf); + tmp >>= 4; } while (tmp); HASSERT(num_digits == -1); return ret; @@ -80,16 +80,8 @@ char* uint64_integer_to_buffer_hex(uint64_t value, char* buffer) do { HASSERT(num_digits >= 0); - uint64_t tmp2 = tmp % 16; - if (tmp2 <= 9) - { - buffer[num_digits--] = '0' + tmp2; - } - else - { - buffer[num_digits--] = 'a' + (tmp2 - 10); - } - tmp /= 16; + buffer[num_digits--] = nibble_to_hex(tmp & 0xf); + tmp >>= 4; } while (tmp); HASSERT(num_digits == -1); return ret; @@ -237,6 +229,19 @@ string int64_to_string_hex(int64_t value, unsigned padding) return ret; } +string string_to_hex(const string &arg) +{ + string ret; + ret.reserve(arg.size() * 2); + for (char c : arg) + { + uint8_t cc = static_cast(c); + ret.push_back(nibble_to_hex((cc >> 4) & 0xf)); + ret.push_back(nibble_to_hex(cc & 0xf)); + } + return ret; +} + string mac_to_string(uint8_t mac[6], bool colon) { string ret; diff --git a/src/utils/format_utils.hxx b/src/utils/format_utils.hxx index d1b8890b8..0cc8985e1 100644 --- a/src/utils/format_utils.hxx +++ b/src/utils/format_utils.hxx @@ -36,6 +36,7 @@ #define _UTILS_FORMAT_UTILS_HXX_ #include +#include /** Renders an integer to string, left-justified. @param buffer must be an at * @param buffer must be an at least 10 character long array. @@ -121,6 +122,12 @@ string uint64_to_string_hex(uint64_t value, unsigned padding = 0); */ string int64_to_string_hex(int64_t value, unsigned padding = 0); +/// Converts a (binary) string into a sequence of hex digits. +/// @param arg input string +/// @return string twice the length of arg with hex digits representing the +/// original data. +string string_to_hex(const string& arg); + /// Formats a MAC address to string. Works both for Ethernet addresses as well /// as for OpenLCB node IDs. /// @@ -157,4 +164,17 @@ inline string ipv4_to_string(uint32_t ip) return ipv4_to_string((uint8_t*)&ip); } +/// Populates a character array with a C string. Copies the C string, +/// appropriately truncating if it is too long and filling the remaining space +/// with zeroes. Ensures that at least one null terminator character is +/// present. +/// @param dst a character array of fixed length, declared as char sdata[N] +/// @param src a C string to fill it with. +template +inline void str_populate(char (&dst)[N], const char *src) +{ + strncpy(dst, src, N - 1); + dst[N - 1] = 0; +} + #endif // _UTILS_FORMAT_UTILS_HXX_ diff --git a/src/utils/macros.h b/src/utils/macros.h index 7cb4670f7..142b7f9c5 100644 --- a/src/utils/macros.h +++ b/src/utils/macros.h @@ -104,7 +104,7 @@ extern const char* g_death_file; #include #ifdef NDEBUG -#define HASSERT(x) do { if (!(x)) { fprintf(stderr, "Assertion failed in file " __FILE__ " line %d: assert(" #x ")", __LINE__); g_death_file = __FILE__; g_death_lineno = __LINE__; abort();} } while(0) +#define HASSERT(x) do { if (!(x)) { fprintf(stderr, "Assertion failed in file " __FILE__ " line %d: assert(" #x ")\n", __LINE__); g_death_file = __FILE__; g_death_lineno = __LINE__; abort();} } while(0) #else /// Checks that the value of expression x is true, else terminates the current /// process. @@ -114,7 +114,7 @@ extern const char* g_death_file; /// Unconditionally terminates the current process with a message. /// @param MSG is the message to print as cause of death. -#define DIE(MSG) do { fprintf(stderr, "Crashed in file " __FILE__ " line %d: " MSG, __LINE__); g_death_file = __FILE__; g_death_lineno = __LINE__; abort(); } while(0) +#define DIE(MSG) do { fprintf(stderr, "Crashed in file " __FILE__ " line %d: " MSG "\n", __LINE__); g_death_file = __FILE__; g_death_lineno = __LINE__; abort(); } while(0) #endif diff --git a/src/utils/socket_listener.cxx b/src/utils/socket_listener.cxx index bd2f45872..5dfa241f4 100644 --- a/src/utils/socket_listener.cxx +++ b/src/utils/socket_listener.cxx @@ -40,6 +40,12 @@ #define _DEFAULT_SOURCE #endif +#include "utils/socket_listener.hxx" + +#include "nmranet_config.h" +#include "utils/logging.h" +#include "utils/macros.h" + #ifndef ESP32 // these don't exist on the ESP32 with LWiP #include #include @@ -51,34 +57,22 @@ #include #include -#include "utils/socket_listener.hxx" - -#include "utils/macros.h" -#include "utils/logging.h" - - -static void* accept_thread_start(void* arg) { +static void* accept_thread_start(void* arg) +{ SocketListener* l = static_cast(arg); l->AcceptThreadBody(); return NULL; } -#ifdef ESP32 -/// Stack size to use for the accept_thread_. -static constexpr size_t listener_stack_size = 2048; -#else -/// Stack size to use for the accept_thread_. -static constexpr size_t listener_stack_size = 1000; -#endif // ESP32 - -SocketListener::SocketListener(int port, connection_callback_t callback) +SocketListener::SocketListener(int port, connection_callback_t callback, + const char *thread_name) : startupComplete_(0), shutdownRequested_(0), shutdownComplete_(0), port_(port), callback_(callback), - accept_thread_("accept_thread", 0, listener_stack_size, - accept_thread_start, this) + accept_thread_(thread_name, 0, config_socket_listener_stack_size(), + accept_thread_start, this) { #if OPENMRN_FEATURE_BSD_SOCKETS_IGNORE_SIGPIPE // We expect write failures to occur but we want to handle them where the @@ -87,8 +81,10 @@ SocketListener::SocketListener(int port, connection_callback_t callback) #endif // OPENMRN_FEATURE_BSD_SOCKETS_IGNORE_SIGPIPE } -SocketListener::~SocketListener() { - if (!shutdownComplete_) { +SocketListener::~SocketListener() +{ + if (!shutdownComplete_) + { shutdown(); } } @@ -96,12 +92,14 @@ SocketListener::~SocketListener() { void SocketListener::shutdown() { shutdownRequested_ = 1; - while (!shutdownComplete_) { + while (!shutdownComplete_) + { usleep(1000); } } -void SocketListener::AcceptThreadBody() { +void SocketListener::AcceptThreadBody() +{ socklen_t namelen; struct sockaddr_in addr; int listenfd; @@ -129,7 +127,7 @@ void SocketListener::AcceptThreadBody() { // FreeRTOS+TCP uses the parameter to listen to set the maximum number of // connections to the given socket, so allow some room - ERRNOCHECK("listen", listen(listenfd, 5)); + ERRNOCHECK("listen", listen(listenfd, config_socket_listener_backlog())); LOG(INFO, "Listening on port %d, fd %d", ntohs(addr.sin_port), listenfd); @@ -147,16 +145,20 @@ void SocketListener::AcceptThreadBody() { startupComplete_ = 1; - while (!shutdownRequested_) { + while (!shutdownRequested_) + { namelen = sizeof(addr); connfd = accept(listenfd, (struct sockaddr *)&addr, &namelen); - if (connfd < 0) { - if (errno == EINTR || errno == EAGAIN || errno == EMFILE) { + if (connfd < 0) + { + if (errno == EINTR || errno == EAGAIN || errno == EMFILE) + { continue; } - else if (errno == ECONNABORTED) { + else if (errno == ECONNABORTED) + { break; } print_errno_and_exit("accept"); diff --git a/src/utils/socket_listener.hxx b/src/utils/socket_listener.hxx index 5ff45e26d..667406702 100644 --- a/src/utils/socket_listener.hxx +++ b/src/utils/socket_listener.hxx @@ -60,7 +60,9 @@ public: /// /// @param port which TCP port number to listen upon. /// @param callback will be called on each incoming connection. - SocketListener(int port, connection_callback_t callback); + /// @param thread_name name to assign to the OSThread. + SocketListener(int port, connection_callback_t callback, + const char *thread_name = "accept_thread"); ~SocketListener(); /// Implementation of the accept thread. diff --git a/src/utils/sources b/src/utils/sources index 4dc7e26c7..7140a788a 100644 --- a/src/utils/sources +++ b/src/utils/sources @@ -22,6 +22,7 @@ CXXSRCS += \ Queue.cxx \ JSHubPort.cxx \ ReflashBootloader.cxx \ + SocketCan.cxx \ constants.cxx \ gc_format.cxx \ logging.cxx \ diff --git a/src/utils/test_main.hxx b/src/utils/test_main.hxx index 104e03ef2..cfbd77840 100644 --- a/src/utils/test_main.hxx +++ b/src/utils/test_main.hxx @@ -44,16 +44,25 @@ #include #include #include +#include #include "gtest/gtest.h" #include "gmock/gmock.h" #include "can_frame.h" +#include "executor/CallableFlow.hxx" #include "executor/Executor.hxx" #include "executor/Service.hxx" #include "os/TempFile.hxx" #include "os/os.h" #include "utils/StringPrintf.hxx" +#ifdef WITHGPERFTOOLS +#include + +std::function profiler_enable{&ProfilerEnable}; +std::function profiler_disable{&ProfilerDisable}; +#endif + int appl_main(int argc, char *argv[]) { testing::InitGoogleMock(&argc, argv); @@ -166,9 +175,75 @@ private: /// Synchronously runs a function in the main executor. void run_x(std::function fn) { - FnExecutable e(std::move(fn)); - g_executor.add(&e); - e.n.wait_for_notification(); + g_executor.sync_run(std::move(fn)); +} + +/// Runs some code in the constructor. Useful if custom code needs to be +/// injected into the constructor initialization order. +class RunInConstruct +{ +public: + RunInConstruct(std::function f) + { + f(); + } +}; + +/// Runs some code in the constructor on the main executor. +class RunInConstructOnMain +{ +public: + RunInConstructOnMain(std::function f) + { + run_x(f); + } +}; + +/// Helper macro to make running certain test commands run on the main executor +/// simpler. +#define RX(statement) run_x([&](){ statement; }) + +/// Structure holding returned objects for an invoke_flow_nowait command. +template struct PendingInvocation +{ + /// Buffer sent to the flow. + BufferPtr b; + /// Notifiable to wait for. + SyncNotifiable notifiable; + /// Barrier notifiable given to the buffer. + BarrierNotifiable barrier {¬ifiable}; + /// True if wait has been invoked. + bool isWaited {false}; + + ~PendingInvocation() + { + wait(); + } + + void wait() + { + if (isWaited) + { + return; + } + notifiable.wait_for_notification(); + isWaited = true; + } +}; + +/// Executes a callable flow similar to invoke_flow(...) but does not wait for +/// the result to come back. Instead, returns a PendingInvocation object, where +/// there is a wait() method to be called. +template +std::unique_ptr> invoke_flow_nowait( + FlowInterface> *flow, Args &&...args) +{ + auto ret = std::make_unique>(); + ret->b.reset(flow->alloc()); + ret->b->data()->reset(std::forward(args)...); + ret->b->data()->done.reset(&ret->barrier); + flow->send(ret->b->ref()); + return ret; } /** Utility class to block an executor for a while. diff --git a/targets/Makefile b/targets/Makefile index 41b678b59..82b88cf30 100644 --- a/targets/Makefile +++ b/targets/Makefile @@ -6,7 +6,7 @@ SUBDIRS = linux.x86 \ freertos.armv6m \ freertos.armv4t \ freertos.mips4k.pic32mx \ - mach.x86 mach.x86_64 \ + mach.x86_64 \ bare.armv7m \ js.emscripten \ mingw.x86 \ diff --git a/targets/mach.x86/Makefile b/targets/linux.aarch64/Makefile similarity index 100% rename from targets/mach.x86/Makefile rename to targets/linux.aarch64/Makefile diff --git a/targets/mach.x86/os/Makefile b/targets/linux.aarch64/console/Makefile similarity index 100% rename from targets/mach.x86/os/Makefile rename to targets/linux.aarch64/console/Makefile diff --git a/targets/mach.x86/console/Makefile b/targets/linux.aarch64/cue/Makefile similarity index 100% rename from targets/mach.x86/console/Makefile rename to targets/linux.aarch64/cue/Makefile diff --git a/targets/mach.x86/cue/Makefile b/targets/linux.aarch64/dcc/Makefile similarity index 100% rename from targets/mach.x86/cue/Makefile rename to targets/linux.aarch64/dcc/Makefile diff --git a/targets/mach.x86/dcc/Makefile b/targets/linux.aarch64/executor/Makefile similarity index 100% rename from targets/mach.x86/dcc/Makefile rename to targets/linux.aarch64/executor/Makefile diff --git a/targets/mach.x86/lib/Makefile b/targets/linux.aarch64/lib/Makefile similarity index 100% rename from targets/mach.x86/lib/Makefile rename to targets/linux.aarch64/lib/Makefile diff --git a/targets/mach.x86/executor/Makefile b/targets/linux.aarch64/openlcb/Makefile similarity index 100% rename from targets/mach.x86/executor/Makefile rename to targets/linux.aarch64/openlcb/Makefile diff --git a/targets/linux.aarch64/os/Makefile b/targets/linux.aarch64/os/Makefile new file mode 100644 index 000000000..0c54f81dd --- /dev/null +++ b/targets/linux.aarch64/os/Makefile @@ -0,0 +1,3 @@ +OPENMRNPATH ?= $(realpath ../../..) +include $(OPENMRNPATH)/etc/lib.mk + diff --git a/targets/mach.x86/openlcb/Makefile b/targets/linux.aarch64/utils/Makefile similarity index 100% rename from targets/mach.x86/openlcb/Makefile rename to targets/linux.aarch64/utils/Makefile diff --git a/targets/mach.x86/utils/Makefile b/targets/linux.aarch64/withrottle/Makefile similarity index 100% rename from targets/mach.x86/utils/Makefile rename to targets/linux.aarch64/withrottle/Makefile diff --git a/targets/mach.x86/withrottle/Makefile b/targets/mach.x86/withrottle/Makefile deleted file mode 100644 index 5e5bf973e..000000000 --- a/targets/mach.x86/withrottle/Makefile +++ /dev/null @@ -1 +0,0 @@ -include $(OPENMRNPATH)/etc/lib.mk