├── lib ├── redisTools │ ├── redisTools_test.c │ ├── redisTools.h │ ├── redisTools.c │ └── redisTools.py ├── tictoc │ ├── test │ ├── Test_Tictoc.c │ ├── tictoc.h │ ├── tictoc.c │ └── Tictoc.c.bak ├── python │ ├── setup.py │ └── brand │ │ ├── __init__.py │ │ ├── exceptions.py │ │ ├── timing.py │ │ ├── redis.py │ │ ├── node.py │ │ ├── tools.py │ │ ├── booter.py │ │ └── supervisor.py ├── c_code │ └── brands │ │ ├── tools_CParser.h │ │ ├── tools_CParser.py │ │ └── tools_CParser.c ├── utilityFunctions │ ├── utilityFunctions.h │ ├── utilityFunctions.c │ └── constants.h └── c │ └── brand │ ├── brand.h │ └── brand.c ├── doc ├── preempt_rt_example_latency_plot.png ├── plot_cyclictest.sh ├── DataSyncGuidelines.md ├── NewUserSetup.md ├── BRAND_spec.md └── preempt_rt.md ├── supervisor ├── supervisor.py ├── booter.py └── README.md ├── .gitmodules ├── setenv.mk ├── setup.sh ├── .gitignore ├── LICENSE ├── CITATION.cff ├── bootstrap.sh ├── Makefile ├── environment.yaml └── README.md /lib/redisTools/redisTools_test.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | -------------------------------------------------------------------------------- /lib/tictoc/test: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brandbci/brand/HEAD/lib/tictoc/test -------------------------------------------------------------------------------- /doc/preempt_rt_example_latency_plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brandbci/brand/HEAD/doc/preempt_rt_example_latency_plot.png -------------------------------------------------------------------------------- /supervisor/supervisor.py: -------------------------------------------------------------------------------- 1 | from brand import Supervisor 2 | 3 | if __name__ == "__main__": 4 | supervisor = Supervisor() 5 | supervisor.main() -------------------------------------------------------------------------------- /lib/python/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='brand', 5 | version='0.0.0', 6 | packages=['brand'], 7 | ) 8 | -------------------------------------------------------------------------------- /supervisor/booter.py: -------------------------------------------------------------------------------- 1 | from brand import Booter 2 | 3 | if __name__ == '__main__': 4 | # parse command line arguments 5 | args = Booter.parse_booter_args() 6 | kwargs = vars(args) 7 | # Run Booter 8 | booter = Booter(**kwargs) 9 | booter.run() 10 | -------------------------------------------------------------------------------- /lib/tictoc/Test_Tictoc.c: -------------------------------------------------------------------------------- 1 | #include "Tictoc.h" 2 | #include 3 | #include 4 | 5 | int main() 6 | { 7 | struct timespec tStart = Tic(); 8 | 9 | usleep(1000); 10 | 11 | struct timespec tEnd = Toc(&tStart); 12 | PrintToc(&tEnd); 13 | 14 | return 0; 15 | } 16 | 17 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/hiredis"] 2 | path = lib/hiredis 3 | url = git@github.com:redis/hiredis.git 4 | ignore = untracked 5 | branch = master 6 | [submodule "lib/redis"] 7 | path = lib/redis 8 | url = git@github.com:redis/redis.git 9 | ignore = untracked 10 | branch = 7.0 11 | [submodule "lib/nxjson"] 12 | path = lib/nxjson 13 | url = git@github.com:thestr4ng3r/nxjson.git 14 | branch = master 15 | -------------------------------------------------------------------------------- /setenv.mk: -------------------------------------------------------------------------------- 1 | ifeq ($(ROOT), ) 2 | $(error "ROOT is undefined") 3 | endif 4 | 5 | export HIREDIS_PATH=$(ROOT)/lib/hiredis 6 | export REDIS_PATH=$(ROOT)/lib/redis 7 | 8 | # save all compiled nodes in local node folder 9 | # so that we have consistency in where the run 10 | # command will look for nodes 11 | export BIN_PATH=$(ROOT)/bin 12 | export GENERATED_PATH=$(BIN_PATH)/generated 13 | $(shell mkdir -p $(GENERATED_PATH)) 14 | -------------------------------------------------------------------------------- /lib/tictoc/tictoc.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * @author David Brandman 4 | * @brief Calculate time elapsed between Tic() and Toc(). 5 | */ 6 | 7 | #include 8 | #include 9 | 10 | /** Store the current system time. */ 11 | struct timespec Tic(); 12 | 13 | /** Return time elapsed since call to Tic(). */ 14 | struct timespec Toc(struct timespec *time1); 15 | 16 | /** Print any `struct timespec` to console. */ 17 | void PrintToc(struct timespec *t); 18 | 19 | -------------------------------------------------------------------------------- /lib/python/brand/__init__.py: -------------------------------------------------------------------------------- 1 | from .tools import (get_node_parameter_value, get_parameter_value, 2 | initializeRedisFromYAML, get_node_parameter_dump, 3 | get_redis_info, main, get_node_io, unpack_string, 4 | node_stage) 5 | 6 | from .node import BRANDNode 7 | 8 | from .supervisor import Supervisor 9 | 10 | from .booter import Booter 11 | 12 | from .exceptions import (GraphError, NodeError, 13 | BooterError, DerivativeError, 14 | CommandError, RedisError) -------------------------------------------------------------------------------- /lib/redisTools/redisTools.h: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | 4 | #define ROOT_PATH "../.." 5 | 6 | int load_YAML_variable_string(char *process, char *yaml_path, char *value, char *buffer, int n); 7 | int initialize_redis_from_YAML(char *process); 8 | int load_redis_context(redisContext **redis_context, char *redis_ip, char *redis_port); 9 | 10 | int redis_string(redisContext *redis_context, char *command, char *string, int n); 11 | int redis_int(redisContext *redis_context, char *command, int *value); 12 | int redis_succeed(redisContext *redis_context, char *command); 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /lib/c_code/brands/tools_CParser.h: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | 4 | #define ROOT_PATH ".." 5 | 6 | int load_YAML_variable_string(char *process, char *yaml_path, char *value, char *buffer, int n); 7 | int initialize_redis_from_YAML(char *process); 8 | int load_redis_context(redisContext **redis_context, char *redis_ip, char *redis_port); 9 | 10 | int redis_string(redisContext *redis_context, char *command, char *string, int n); 11 | int redis_int(redisContext *redis_context, char *command, int *value); 12 | int redis_succeed(redisContext *redis_context, char *command); 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ################################################ 4 | # variables graphs and sites 5 | ################################################ 6 | 7 | BRAND_BASE_DIR=$(pwd) 8 | BRAND_MOD_DIR=$BRAND_BASE_DIR/../brand-modules/ 9 | export BRAND_BASE_DIR # save brand base dir to the environment 10 | 11 | # Activate the rt environment to get to work 12 | conda activate rt 13 | 14 | # Make aliases for booter and supervisor 15 | alias booter='sudo -E env "PATH=$PATH" python supervisor/booter.py' 16 | alias supervisor='sudo -E env "PATH=$PATH" python supervisor/supervisor.py' 17 | 18 | export PATH=$(pwd)/bin:$PATH 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | tmp/ 3 | proc/rest/elm-stuff/ 4 | 5 | # vim swap files 6 | *.swp 7 | 8 | # python compiles 9 | __pycache__ 10 | *.pyc 11 | 12 | # binaries, compiled objects, and generated code 13 | bin/ 14 | 15 | # should be in bin, but no compiled c or cython objects 16 | *.o 17 | *.bin 18 | 19 | # All backup code should be saved in subfolders with "old": 20 | **/old 21 | 22 | # dump files -- in case they end up outside of /run/ 23 | *.rdb 24 | 25 | # images and media 26 | *.jbp 27 | *.bmp 28 | *.avi 29 | *.mp[0-9] 30 | 31 | # matlab data 32 | *.mat 33 | 34 | # Duplicate .pyx files 35 | pyglet_display.pyx 36 | 37 | # Python 38 | *.egg-info 39 | *.pkl 40 | -------------------------------------------------------------------------------- /doc/plot_cyclictest.sh: -------------------------------------------------------------------------------- 1 | max=`grep "Max Latencies" $1 | tr " " "\n" | sort -n | tail -1 | sed s/^0*//` 2 | grep -v -e "^#" -e "^$" $1 | tr " " "\t" >histogram 3 | cores=12 4 | 5 | for i in `seq 1 $cores` 6 | do 7 | column=`expr $i + 1` 8 | cut -f1,$column histogram >histogram$i 9 | done 10 | 11 | echo -n "set title \"Latency plot\"\n\ 12 | set terminal png\n\ 13 | set xlabel \"Latency (us), max $max us\"\n\ 14 | set logscale y\n\ 15 | set xrange [0:400]\n\ 16 | set yrange [0.8:*]\n\ 17 | set ylabel \"Number of latency samples\"\n\ 18 | set output \"plot.png\"\n\ 19 | plot " >plotcmd 20 | 21 | for i in `seq 1 $cores` 22 | do 23 | if test $i != 1 24 | then 25 | echo -n ", " >>plotcmd 26 | fi 27 | cpuno=`expr $i - 1` 28 | if test $cpuno -lt 10 29 | then 30 | title=" CPU$cpuno" 31 | else 32 | title="CPU$cpuno" 33 | fi 34 | echo -n "\"histogram$i\" using 1:2 title \"$title\" with histeps" >>plotcmd 35 | done 36 | 37 | gnuplot -persist 2 | #include 3 | #include 4 | 5 | #define __GNU_SOURCE 6 | 7 | #ifndef _UTILITY_FUNCTIONS_ 8 | #define _UTILITY_FUNCTIONS_ 9 | 10 | /* 11 | * initialize utilities 12 | * set function to run on exit 13 | */ 14 | void init_utils(void (*pHandleExit)(int exitStatus), sigset_t *pExitMask); 15 | 16 | /* 17 | * finish necessary real-time setup before process execution begins 18 | */ 19 | void make_realtime(); 20 | 21 | /* 22 | * run the function specified by exit_handler and print the given error message 23 | */ 24 | void die(char *errorStr); 25 | 26 | /* 27 | * create a signal handler that handles signal signum and runs the function *psh 28 | * when signum is raised 29 | */ 30 | void open_shared_mem(uint8_t **ppmem, const char *pName, size_t numBytes, int shm_flags, int mmap_flags); 31 | 32 | /* 33 | * open a shared memory block with: 34 | * name: pName 35 | * size: numPages * PAGESIZE 36 | * shm_open flags: shm_flags 37 | * mmap flags: mmap_flags 38 | * *ppmem then points to the beginning of this block of memory once the function has run 39 | * 40 | * If the file descriptor needs to be ftruncated (i.e., this is the first process opening) 41 | * this shared memory, then make sure the O_CREAT flag is set in shm_flags 42 | */ 43 | void set_sighandler(int signum, void *psh, sigset_t *block_mask); 44 | 45 | #endif -------------------------------------------------------------------------------- /lib/tictoc/tictoc.c: -------------------------------------------------------------------------------- 1 | #include "Tictoc.h" 2 | #include 3 | #include 4 | 5 | 6 | // Copied from: https://www.gnu.org/software/libc/manual/html_node/Elapsed-Time.html 7 | static void timespec_subtract (struct timespec *result, struct timespec *x, struct timespec *y) 8 | { 9 | /* Perform the carry for the later subtraction by updating y. */ 10 | if (x->tv_nsec < y->tv_nsec) 11 | { 12 | int nsec = (y->tv_nsec - x->tv_nsec) / 1000000000L + 1; 13 | y->tv_nsec -= 1000000000L * nsec; 14 | y->tv_sec += nsec; 15 | } 16 | if (x->tv_nsec - y->tv_nsec > 1000000000L) 17 | { 18 | int nsec = (x->tv_nsec - y->tv_nsec) / 1000000000L; 19 | y->tv_nsec += 1000000000L * nsec; 20 | y->tv_sec -= nsec; 21 | } 22 | 23 | /* Compute the time remaining to wait. 24 | tv_nsec is certainly positive. */ 25 | result->tv_sec = x->tv_sec - y->tv_sec; 26 | result->tv_nsec = x->tv_nsec - y->tv_nsec; 27 | } 28 | 29 | struct timespec Tic() 30 | { 31 | struct timespec res; 32 | clock_gettime(CLOCK_MONOTONIC, &res); 33 | return res; 34 | } 35 | 36 | struct timespec Toc(struct timespec *time1) 37 | { 38 | struct timespec time2, timeDifference; 39 | clock_gettime(CLOCK_MONOTONIC, &time2); 40 | timespec_subtract(&timeDifference, &time2, time1); 41 | /*PrintToc(&timeDifference);*/ 42 | return timeDifference; 43 | } 44 | 45 | void PrintToc(struct timespec *t) 46 | { 47 | printf("Elapsed time: %ld seconds, %ld microseconds\n", t->tv_sec, (long) t->tv_nsec); 48 | } 49 | 50 | -------------------------------------------------------------------------------- /lib/tictoc/Tictoc.c.bak: -------------------------------------------------------------------------------- 1 | #include "Tictoc.h" 2 | #include 3 | #include 4 | 5 | 6 | // Copied from: https://www.gnu.org/software/libc/manual/html_node/Elapsed-Time.html 7 | static void timeval_subtract (struct timeval *result, struct timeval *x, struct timeval *y) 8 | { 9 | /* Perform the carry for the later subtraction by updating y. */ 10 | if (x->tv_usec < y->tv_usec) 11 | { 12 | int nsec = (y->tv_usec - x->tv_usec) / 1000000 + 1; 13 | y->tv_usec -= 1000000 * nsec; 14 | y->tv_sec += nsec; 15 | } 16 | if (x->tv_usec - y->tv_usec > 1000000) 17 | { 18 | int nsec = (x->tv_usec - y->tv_usec) / 1000000; 19 | y->tv_usec += 1000000 * nsec; 20 | y->tv_sec -= nsec; 21 | } 22 | 23 | /* Compute the time remaining to wait. 24 | tv_usec is certainly positive. */ 25 | result->tv_sec = x->tv_sec - y->tv_sec; 26 | result->tv_usec = x->tv_usec - y->tv_usec; 27 | } 28 | 29 | struct timeval Tic() 30 | { 31 | struct timeval time1; 32 | gettimeofday(&time1, NULL); 33 | return time1; 34 | } 35 | 36 | struct timeval Toc(struct timeval time1) 37 | { 38 | struct timeval time2, timeDifference; 39 | gettimeofday(&time2, NULL); 40 | 41 | timeval_subtract(&timeDifference, &time2, &time1); 42 | return timeDifference; 43 | } 44 | 45 | void PrintToc(struct timeval t) 46 | { 47 | printf("Elapsed time: %ld seconds, %ld microseconds\n", t.tv_sec, (long) t.tv_usec); 48 | } 49 | 50 | void PrintElapsedTime(struct timeval tStart) 51 | { 52 | struct timeval tDifference = Toc(tStart); 53 | PrintToc(tDifference); 54 | } 55 | -------------------------------------------------------------------------------- /lib/python/brand/exceptions.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | # brand-specific exceptions 4 | class GraphError(Exception): 5 | def __init__(self, message='', graph=''): 6 | super().__init__(message) 7 | self.graph = graph 8 | 9 | def __repr__(self): 10 | return f"GraphError(message={str(self)}, graph={self.graph})" 11 | 12 | class NodeError(Exception): 13 | def __init__(self, message='', graph='', node=''): 14 | super().__init__(message) 15 | self.graph = graph 16 | self.node = node 17 | 18 | def __repr__(self): 19 | return f"NodeError(message={str(self)}, graph={self.graph}, node={self.node})" 20 | 21 | class BooterError(Exception): 22 | def __init__(self, message='', machine='', graph='', booter_tb='', source_exc=''): 23 | super().__init__(message) 24 | self.machine = machine 25 | self.graph = graph 26 | self.booter_tb = booter_tb 27 | self.source_exc = source_exc 28 | 29 | def __repr__(self): 30 | return f"BooterError(message={str(self)}, machine={self.machine}, graph={self.graph}, booter_tb={self.booter_tb}, source_exc={self.source_exc})" 31 | 32 | class DerivativeError(Exception): 33 | def __init__(self, message='', derivative='', graph='', process:subprocess.CompletedProcess=subprocess.CompletedProcess([], 0)): 34 | super().__init__(message) 35 | self.derivative = derivative 36 | self.graph = graph 37 | self.process = process 38 | 39 | class CommandError(Exception): 40 | def __init__(self, message='', process='', command='', details=''): 41 | super().__init__(message) 42 | self.process = process 43 | self.command = command 44 | self.details = details 45 | 46 | class RedisError(Exception): 47 | pass -------------------------------------------------------------------------------- /lib/c/brand/brand.h: -------------------------------------------------------------------------------- 1 | /* Utilities for working with BRAND in Redis */ 2 | 3 | #include 4 | #include 5 | #include "nxjson.h" 6 | 7 | //-------------------------------------------------------------- 8 | // Parse command line arguments and connect to Redis 9 | //-------------------------------------------------------------- 10 | 11 | redisContext* parse_command_line_args_init_redis(int argc, char **argv, char* NICKNAME); 12 | 13 | //-------------------------------------------------------------- 14 | // Tools for working with nxson 15 | //-------------------------------------------------------------- 16 | 17 | const nx_json *get_supergraph_json(redisContext *c, redisReply *reply, char *supergraph_id); 18 | char* get_parameter_string(const nx_json *json, const char *node, const char *parameter); 19 | int get_parameter_int(const nx_json *json, const char *node, const char *parameter); 20 | int ** get_parameter_list_int(const nx_json *json, const char *node, const char *parameter, int **output, int *n); 21 | char*** get_parameter_list_string(const nx_json *json, const char *node, const char *parameter, char ***output, int *n); 22 | unsigned long get_graph_load_ts_long(const nx_json *json); 23 | //void get_parameter_float(const nx_json *json, const char *node, const char *parameter, float *output); 24 | //void get_parameter_bool(const nx_json *json, const char *node, const char *parameter, bool *output); 25 | 26 | //-------------------------------------------------------------- 27 | // Emit node state 28 | //-------------------------------------------------------------- 29 | 30 | enum node_state {NODE_STARTED, NODE_READY, NODE_SHUTDOWN, NODE_FATAL_ERROR, NODE_WARNING, NODE_SUPERGRAPH_UPDATE, NODE_INFO}; 31 | void emit_status(redisContext *c, const char *node_name, enum node_state state, const char *node_message); 32 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | message: "If you use this work, please cite the following paper." 3 | preferred-citation: 4 | type: article 5 | authors: 6 | - family-names: Ali 7 | given-names: Yahia Hassan 8 | orcid: 0000-0001-8618-3837 9 | - family-names: Bodkin 10 | given-names: Kevin L 11 | orcid: 0000-0002-6329-7353 12 | - family-names: Rigotti-Thompson 13 | given-names: Mattia 14 | orcid: 0009-0001-7298-274X 15 | - family-names: Patel 16 | given-names: Kushant 17 | - family-names: Card 18 | given-names: Nicholas S 19 | orcid: 0000-0002-6858-268X 20 | - family-names: Bhaduri 21 | given-names: Bareesh 22 | orcid: 0000-0002-4564-2555 23 | - family-names: Nason-Tomaszewski 24 | given-names: Samuel R 25 | orcid: 0000-0002-7127-0986 26 | - family-names: Mifsud 27 | given-names: Domenick M 28 | orcid: 0000-0001-8200-8193 29 | - family-names: Hou 30 | given-names: Xianda 31 | orcid: 0009-0002-2066-8561 32 | - family-names: Nicolas 33 | given-names: Claire 34 | orcid: 0000-0002-7761-3943 35 | - family-names: Allcroft 36 | given-names: Shane 37 | orcid: 0000-0002-7903-5091 38 | - family-names: Hochberg 39 | given-names: Leigh 40 | orcid: 0000-0003-0261-2273 41 | - family-names: Au Yong 42 | given-names: Nicholas 43 | orcid: 0000-0002-7898-7832 44 | - family-names: Stavisky 45 | given-names: Sergey D 46 | orcid: 0000-0002-5238-0573 47 | - family-names: Miller 48 | given-names: Lee E 49 | orcid: 0000-0001-8675-7140 50 | - family-names: Brandman 51 | given-names: David 52 | orcid: 0000-0003-3224-7019 53 | - family-names: Pandarinath 54 | given-names: Chethan 55 | orcid: 0000-0003-1241-1432 56 | title: "BRAND: A platform for closed-loop experiments with deep network models" 57 | doi: 10.1088/1741-2552/ad3b3a 58 | url: http://iopscience.iop.org/article/10.1088/1741-2552/ad3b3a 59 | date-released: 2024-04-05 60 | keywords: 61 | - closed-loop experiments 62 | - deep network models 63 | journal: Journal of Neural Engineering 64 | issn: 1741-2552 65 | -------------------------------------------------------------------------------- /lib/c_code/brands/tools_CParser.py: -------------------------------------------------------------------------------- 1 | #!/usr/env/python 2 | 3 | import argparse 4 | from brand import * 5 | 6 | 7 | 8 | # ----------------------------------------------------------- 9 | # running the function as a script -- for C and Bash usage 10 | def main(): 11 | 12 | description = """ 13 | Tools for initializing processes. The default behavior is to look into a YAML file 14 | and then initialize all of the variables from the YAML script into Redis. This 15 | behavior, by default, is verbose. If you supply an --ip or --port flag, then 16 | the script will look specifically for the redis_ip or redis_port variable from 17 | the script and print it. This should be used only for .c processes""" 18 | 19 | parser = argparse.ArgumentParser(description=description) 20 | parser.add_argument('--name', help='Return the value in the YAML file', type=str) 21 | parser.add_argument('--node', help='Which node to use', type=str) 22 | parser.add_argument('file', default="", type=str, help='The YAML file to be loaded') 23 | parser.add_argument('--redis', help="Return the port and ip for the redis instance") 24 | redisGroup = parser.add_mutually_exclusive_group() 25 | redisGroup.add_argument('--ip', help='IP for the redis instance', action="store_true") 26 | redisGroup.add_argument('--port', help='port for the redis instance', action="store_true") 27 | 28 | args = parser.parse_args() 29 | 30 | if args.ip: 31 | print(get_redis_info(args.file,'redis_realtime_ip')) 32 | elif args.port: 33 | print(get_redis_info(args.file,'redis_realtime_port')) 34 | elif args.node: ## if we got a node name, look inside of that specific node -- standard behavior now! 35 | if args.name: # if we have a particular value 36 | print(get_node_parameter_value(args.file, args.node, args.name), end="") 37 | else: # return all values 38 | print(get_node_parameters(args.file, args.node), end="") 39 | elif args.name: # if no node name is supplied... probably mostly for the redis connection 40 | print(get_parameter_value(args.file, args.name), end="") 41 | 42 | 43 | if __name__ == '__main__': 44 | main() 45 | 46 | -------------------------------------------------------------------------------- /bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script will install debian pkg dependencies and activate 4 | # the reat-time conda environment (rt) defined by environment.yaml 5 | 6 | # Notes 7 | # - this has only been tested on Ubuntu 18.04 8 | # - libevent-2.1-6 was already installed on my machine, 9 | # so assuming it comes default with Ubuntu 18.04 10 | 11 | RED="\e[31m" 12 | GREEN="\e[32m" 13 | DEFAULT="\e[39m" 14 | 15 | error () { 16 | echo -e "${RED}Error: ${DEFAULT}$1" 17 | exit 1 18 | } 19 | 20 | info () { 21 | echo -e "${GREEN}$1${DEFAULT}" 22 | } 23 | 24 | checkStatus () { 25 | [ "$1" == "0" ] || error "$2" 26 | } 27 | 28 | # List of apt packages to install as dependencies. 29 | # All of these should be found in Ubuntu 18.04 default repos. 30 | # To add a dependency, just add the pkg name to the list. 31 | dependencies=( 32 | libsqlite3-dev 33 | automake 34 | libtool 35 | curl 36 | libsdl2-2.0-0 37 | libsdl2-dev 38 | libsdl2-image-2.0-0 39 | libsdl2-image-dev 40 | libsdl2-gfx-1.0-0 41 | libsdl2-gfx-dev 42 | libsdl2-ttf-dev 43 | ) 44 | 45 | # install pkgs in $dependencies 46 | for dep in ${dependencies[@]}; do 47 | info "Installing ${dep}" 48 | [ "${dep}" == "redis-server" ] && sudo add-apt-repository -y ppa:chris-lea/redis-server 49 | sudo apt-get update 50 | sudo apt-get -y install ${dep} 51 | checkStatus $? "failed to install ${dep}" 52 | info "Successfully installed ${dep}" 53 | done 54 | 55 | # check if elm command is available. If not prompt user for installation. 56 | install_elm=false 57 | ROOT=`dirname "$0"` 58 | elmPath=${ROOT}/bin 59 | [ -d "${elmPath}" ] || mkdir -p "${elmPath}" # make bin/ sense it will be used by make anyway 60 | [ -x "${elmPath}/elm" ] || install_elm=true 61 | 62 | # install elm to the project bin path 63 | if ${install_elm}; then 64 | info "Installing elm to ${elmPath}" 65 | pushd ${elmPath} 66 | curl -L -o elm.gz https://github.com/elm/compiler/releases/download/0.19.1/binary-for-linux-64-bit.gz 67 | gunzip elm.gz 68 | chmod +x elm 69 | popd 70 | fi 71 | 72 | # check conda is installed 73 | # which conda should return a path of > 0 length if installed 74 | [ "`which conda`" ] || error "conda is not installed. Please install it and rerun this script" 75 | 76 | # create conda env from file - in case it has been created, just update it. 77 | info "Updating real-time conda env" 78 | conda env update --file environment.yaml --prune 79 | checkStatus $? "conda update failed" 80 | info "conda env succesfully updated" 81 | 82 | info "Updating git submodules" 83 | git submodule update --init --recursive 84 | checkStatus $? "failed to update git submodules" 85 | info "Your environment is ready!" 86 | info "Run \`conda activate rt\` before running make" 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /lib/utilityFunctions/utilityFunctions.c: -------------------------------------------------------------------------------- 1 | #include "utilityFunctions.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "constants.h" 8 | 9 | static sigset_t exitMask; 10 | static void (*exit_handler)(int errorStr); 11 | 12 | /* 13 | * prefault stack to avoid faults during execution 14 | */ 15 | static void stack_prefault() { 16 | unsigned char dummy[MAX_SAFE_STACK]; 17 | memset(dummy, 0, MAX_SAFE_STACK); 18 | } 19 | 20 | /* 21 | * initialize utilities 22 | * set function to run on exit 23 | */ 24 | void init_utils(void (*pHandleExit)(int errorStr), sigset_t *pExitMask) { 25 | exit_handler = pHandleExit; 26 | exitMask = *pExitMask; 27 | } 28 | 29 | /* 30 | * finish necessary real-time setup before process execution begins 31 | */ 32 | void make_realtime() { 33 | // lock stack mem 34 | if(mlockall(MCL_CURRENT|MCL_FUTURE) == -1) { 35 | die("NETWORK ERROR: memory lock error.\n"); 36 | } 37 | stack_prefault(); 38 | } 39 | 40 | /* 41 | * run the function specified by exit_handler and print the given error message 42 | */ 43 | void die(char *errorStr) { 44 | // void die(const char *format, ...) { 45 | sigprocmask(SIG_BLOCK, &exitMask, NULL); 46 | /* can try to allow for printf-like argument as input 47 | va_list argptr; 48 | va_start(argptr, format); 49 | vfprintf(stderr, format, argptr); 50 | va_end(argptr); 51 | */ 52 | perror(errorStr); 53 | exit_handler(1); 54 | } 55 | 56 | /* 57 | * create a signal handler that handles signal signum and runs the function *psh 58 | * when signum is raised 59 | */ 60 | void set_sighandler(int signum, void *psh, sigset_t *block_mask) { 61 | struct sigaction sa; 62 | memset(&sa, 0, sizeof(sa)); 63 | sa.sa_handler = psh; 64 | if (block_mask) 65 | sa.sa_mask = *block_mask; 66 | sa.sa_flags = SA_RESTART; 67 | if (signum == SIGCHLD) { 68 | sa.sa_flags |= SA_NOCLDSTOP; 69 | } 70 | if (sigaction(signum, &sa, NULL) == -1) { 71 | die("sigaction failed \n"); 72 | } 73 | } 74 | 75 | /* 76 | * open a shared memory block with: 77 | * name: pName 78 | * size: numBytes 79 | * flags: shm_flags 80 | * *ppmem then points to the beginning of this block of memory once the function has run 81 | * 82 | * If the file descriptor needs to be ftruncated (i.e., this is the first process opening) 83 | * this shared memory, then make sure the O_CREAT flag is set in shm_flags 84 | */ 85 | void open_shared_mem(uint8_t **ppmem, const char *pName, size_t numBytes, int shm_flags, int mmap_flags) { 86 | int fd = shm_open(pName, shm_flags, 0600); 87 | if(fd == -1) { 88 | die("shm_open failed\n"); 89 | } 90 | // check if O_CREAT (1 << 6) is set 91 | if (shm_flags & O_CREAT) { 92 | if (ftruncate(fd, numBytes)) { 93 | die("ftruncate failed.\n"); 94 | exit(1); 95 | } 96 | } 97 | *ppmem = (uint8_t*) mmap(NULL, numBytes, mmap_flags, MAP_SHARED, fd, 0); 98 | if (*ppmem == (void*)-1) { 99 | die("mmap failed\n"); 100 | } 101 | close(fd); 102 | } 103 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export ROOT ?= $(shell pwd) 2 | include $(ROOT)/setenv.mk 3 | 4 | # Get all directories in nodes/ and derivatives/ 5 | SUBDIRS_NODES=$(wildcard nodes/*) 6 | SUBDIRS_DERIVS=$(wildcard derivatives/*) 7 | 8 | # make some clean targets for all subdirs 9 | CLEANDIRS_NODES = $(SUBDIRS_NODES:%=clean-%) 10 | CLEANDIRS_DERIVS = $(SUBDIRS_DERIVS:%=clean-%) 11 | 12 | # Get all directories in ../brand-modules/*/nodes/ and ../brand-modules/*/derivatives/ 13 | MODULES_BASE_PATH=../brand-modules 14 | MODULES_NODES=$(wildcard $(MODULES_BASE_PATH)/*/nodes/*) 15 | MODULES_DERIVS=$(wildcard $(MODULES_BASE_PATH)/*/derivatives/*) 16 | 17 | # make some clean targets for all subdirs 18 | MODULES_CLEANDIRS_NODES = $(MODULES_NODES:%=clean-%) 19 | MODULES_CLEANDIRS_DERIVS = $(MODULES_DERIVS:%=clean-%) 20 | 21 | all: $(SUBDIRS_NODES) $(SUBDIRS_DERIVS) $(MODULES_NODES) $(MODULES_DERIVS) hiredis redis 22 | 23 | .PHONY: subdirs $(SUBDIRS_NODES) 24 | .PHONY: subdirs $(SUBDIRS_DERIVS) 25 | .PHONY: subdirs $(CLEANDIRS_NODES) 26 | .PHONY: subdirs $(CLEANDIRS_DERIVS) 27 | .PHONY: modules $(MODULES_NODES) 28 | .PHONY: modules $(MODULES_DERIVS) 29 | .PHONY: modules $(MODULES_CLEANDIRS_NODES) 30 | .PHONY: modules $(MODULES_CLEANDIRS_DERIVS) 31 | 32 | # function that tests if a path $(1) is in a Git repository, and writes the Git hash to git_hash.o if so 33 | write_git_hash = @\ 34 | git -C $(1) rev-parse; \ 35 | if [ $$? = 0 ]; then \ 36 | test -s $(1)/git_hash.o; \ 37 | if [ $$? = 0 ]; then \ 38 | rm -f $(1)/git_hash.o; \ 39 | fi; \ 40 | echo -n $$(git -C $(1) rev-parse HEAD) > $(1)/git_hash.o; \ 41 | fi 42 | 43 | # function that tests if a Makefile exists in a path $(1), and runs make if so 44 | test_and_make = @\ 45 | test -s $(1)/Makefile; \ 46 | if [ $$? = 0 ]; then \ 47 | test -s $(1)/$$(basename $(1)).bin; \ 48 | if [ $$? = 0 ]; then \ 49 | rm -f $(1)/$$(basename $(1)).bin; \ 50 | fi; \ 51 | $(MAKE) -C $(1); \ 52 | fi 53 | 54 | # make targets for all paths under nodes/ 55 | $(SUBDIRS_NODES): hiredis redis 56 | $(call write_git_hash,$@) 57 | $(call test_and_make,$@) 58 | 59 | # make targets for all paths under derivatives/ 60 | $(SUBDIRS_DERIVS): hiredis redis 61 | $(call write_git_hash,$@) 62 | $(call test_and_make,$@) 63 | 64 | # make targets for all relevant paths under ../brand-modules/*/nodes/ 65 | $(MODULES_NODES): hiredis redis 66 | $(call write_git_hash,$@) 67 | $(call test_and_make,$@) 68 | 69 | # make targets for all relevant paths under ../brand-modules/*/derivatives/ 70 | $(MODULES_DERIVS): hiredis redis 71 | $(call write_git_hash,$@) 72 | $(call test_and_make,$@) 73 | 74 | # Linking to hiredis seems to have a bug, where make 75 | # attempt to link to an so filename with the full ver. 76 | # ldconfig to automatically creates that file, and 77 | # a tmp cache is specified to avoid requiring root perms. 78 | hiredis: redis 79 | $(MAKE) -C $(HIREDIS_PATH) 80 | ldconfig -C /tmp/cache $(HIREDIS_PATH) 81 | $(RM) /tmp/cache 82 | 83 | redis: 84 | $(MAKE) -C $(REDIS_PATH) redis-server redis-cli 85 | mv -f $(REDIS_PATH)/src/redis-server $(BIN_PATH) 86 | mv -f $(REDIS_PATH)/src/redis-cli $(BIN_PATH) 87 | 88 | redis-test: 89 | $(MAKE) -C $(REDIS_PATH) test 90 | 91 | clean-all: clean clean-hiredis 92 | 93 | clean: $(CLEANDIRS_NODES) $(CLEANDIRS_DERIVS) $(MODULES_CLEANDIRS_NODES) $(MODULES_CLEANDIRS_DERIVS) 94 | 95 | $(CLEANDIRS_NODES): 96 | $(MAKE) -C $(@:clean-%=%) clean 97 | 98 | $(CLEANDIRS_DERIVS): 99 | $(MAKE) -C $(@:clean-%=%) clean 100 | 101 | $(MODULES_CLEANDIRS_NODES): 102 | $(MAKE) -C $(@:clean-%=%) clean 103 | 104 | $(MODULES_CLEANDIRS_DERIVS): 105 | $(MAKE) -C $(@:clean-%=%) clean 106 | 107 | clean-hiredis: 108 | $(MAKE) -C $(HIREDIS_PATH) clean 109 | $(RM) $(HIREDIS_PATH)/*.so* 110 | 111 | clean-redis: 112 | $(MAKE) -C $(REDIS_PATH) clean 113 | $(RM) $(BIN_PATH)/redis-server $(BIN_PATH)/redis-cli 114 | 115 | -------------------------------------------------------------------------------- /environment.yaml: -------------------------------------------------------------------------------- 1 | name: rt 2 | channels: 3 | - defaults 4 | dependencies: 5 | - _libgcc_mutex=0.1 6 | - ca-certificates=2020.1.1 7 | - certifi=2020.4.5.1 8 | - ld_impl_linux-64=2.33.1 9 | - libedit=3.1.20181209 10 | - libffi=3.3 11 | - libgcc-ng=9.1.0 12 | - libstdcxx-ng=9.1.0 13 | - ncurses=6.2 14 | - openssl=1.1.1g 15 | - pip=20.0.2 16 | - python=3.8.2 17 | - readline=8.0 18 | - setuptools=46.4.0 19 | - sqlite=3.31.1 20 | - tk=8.6.8 21 | - wheel=0.34.2 22 | - xz=5.2.5 23 | - zlib=1.2.11 24 | - pip: 25 | - absl-py==1.2.0 26 | - aiohttp==3.8.1 27 | - aiosignal==1.2.0 28 | - argparse==1.4.0 29 | - async-timeout==4.0.2 30 | - attrs==19.3.0 31 | - backcall==0.1.0 32 | - bidict==0.22.0 33 | - bleach==3.1.5 34 | - cachetools==5.2.0 35 | - charset-normalizer==2.1.0 36 | - click==7.1.2 37 | - coloredlogs==15.0.1 38 | - cycler==0.10.0 39 | - cython==0.29.18 40 | - decorator==4.4.2 41 | - defusedxml==0.6.0 42 | - dnspython==2.2.1 43 | - entrypoints==0.3 44 | - eventlet==0.25.1 45 | - flask==1.1.2 46 | - flask-cors==3.0.8 47 | - flask-socketio==4.2.1 48 | - frozenlist==1.3.1 49 | - fsspec==2022.7.1 50 | - google-auth==2.10.0 51 | - google-auth-oauthlib==0.4.6 52 | - greenlet==1.1.2 53 | - grpcio==1.47.0 54 | - h5py==3.3.0 55 | - hdmf==3.3.1 56 | - humanfriendly==10.0 57 | - idna==3.3 58 | - importlib-metadata==4.12.0 59 | - ipykernel==5.2.1 60 | - ipython==7.14.0 61 | - ipython-genutils==0.2.0 62 | - ipywidgets==7.5.1 63 | - itsdangerous==1.1.0 64 | - jedi==0.17.0 65 | - jinja2==2.11.2 66 | - joblib==1.1.0 67 | - jsonschema==3.2.0 68 | - jupyter==1.0.0 69 | - jupyter-client==6.1.3 70 | - jupyter-console==6.1.0 71 | - jupyter-core==4.6.3 72 | - kiwisolver==1.2.0 73 | - markdown==3.4.1 74 | - markupsafe==1.1.1 75 | - matplotlib==3.2.1 76 | - mistune==0.8.4 77 | - monotonic==1.6 78 | - mpmath==1.1.0 79 | - multidict==6.0.2 80 | - nbconvert==5.6.1 81 | - nbformat==5.0.6 82 | - nose==1.3.7 83 | - notebook==6.0.3 84 | - numpy==1.18.4 85 | - oauthlib==3.2.0 86 | - packaging==20.3 87 | - pandas==1.3.2 88 | - pandocfilters==1.4.2 89 | - parso==0.7.0 90 | - pexpect==4.8.0 91 | - pickle5==0.0.11 92 | - pickleshare==0.7.5 93 | - prometheus-client==0.7.1 94 | - prompt-toolkit==3.0.5 95 | - protobuf==3.19.4 96 | - psutil==5.9.1 97 | - ptyprocess==0.6.0 98 | - pyasn1==0.4.8 99 | - pyasn1-modules==0.2.8 100 | - pydeprecate==0.3.2 101 | - pygame==2.0.0 102 | - pyglet==1.5.11 103 | - pygments==2.6.1 104 | - pynwb==2.0.0 105 | - pyparsing==2.4.7 106 | - pyrsistent==0.16.0 107 | - python-dateutil==2.8.1 108 | - python-engineio==4.3.2 109 | - python-socketio==5.6.0 110 | - pytorch-lightning==1.7.1 111 | - pytz==2020.1 112 | - pyyaml==6.0 113 | - pyzmq==19.0.1 114 | - qtconsole==4.7.4 115 | - qtpy==1.9.0 116 | - redis==3.5.3 117 | - requests==2.28.1 118 | - requests-oauthlib==1.3.1 119 | - rsa==4.9 120 | - ruamel-yaml==0.17.21 121 | - ruamel-yaml-clib==0.2.6 122 | - scikit-learn==1.1.1 123 | - scipy==1.4.1 124 | - send2trash==1.5.0 125 | - sh==1.14.3 126 | - six==1.14.0 127 | - sympy==1.5.1 128 | - tensorboard==2.10.0 129 | - tensorboard-data-server==0.6.1 130 | - tensorboard-plugin-wit==1.8.1 131 | - terminado==0.8.3 132 | - testpath==0.4.4 133 | - threadpoolctl==3.1.0 134 | - torch==1.12.1 135 | - torchmetrics==0.9.3 136 | - tornado==6.0.4 137 | - tqdm==4.64.0 138 | - traitlets==4.3.3 139 | - typing-extensions==4.3.0 140 | - urllib3==1.26.11 141 | - wcwidth==0.1.9 142 | - webencodings==0.5.1 143 | - werkzeug==1.0.1 144 | - widgetsnbextension==3.5.1 145 | - yapf==0.32.0 146 | - yarl==1.8.1 147 | - zipp==3.8.1 148 | - -e ./lib/python 149 | -------------------------------------------------------------------------------- /lib/python/brand/timing.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | import time 3 | from ctypes import Structure, c_long, pointer 4 | from datetime import datetime 5 | 6 | TIMEVAL_LEN = 16 # bytes 7 | TIMESPEC_LEN = 16 # bytes 8 | TIMER_ABSTIME = 1 9 | 10 | libc = ctypes.CDLL('libc.so.6') 11 | 12 | 13 | class timespec(Structure): 14 | """ 15 | timespec struct from sys/time.h 16 | """ 17 | _fields_ = [("tv_sec", c_long), ("tv_nsec", c_long)] 18 | 19 | 20 | class timeval(Structure): 21 | """ 22 | timeval struct from sys/time.h 23 | """ 24 | _fields_ = [("tv_sec", c_long), ("tv_usec", c_long)] 25 | 26 | 27 | def clock_nanosleep(time_ns, clock=time.CLOCK_REALTIME): 28 | """ 29 | Sleep until a specified clock time. This is a wrapper for the C 30 | clock_nanosleep function. 31 | 32 | Parameters 33 | ---------- 34 | time_ns : int 35 | Absolute time (in nanoseconds) as measured by the `clock`. 36 | clock_nanosleep() suspends the execution of the calling thread until 37 | this time. 38 | clock : int, optional 39 | Clock against which the sleep interval is to be measured, by default 40 | time.CLOCK_REALTIME. Another option is time.CLOCK_MONOTONIC. 41 | 42 | Returns 43 | ------- 44 | out : int 45 | Exit code for the clock_nanosleep function. A non-zero code indicates 46 | an error. 47 | """ 48 | deadline_s = time_ns // 1_000_000_000 49 | deadline_ns = time_ns - (deadline_s * 1_000_000_000) 50 | deadline = timespec(int(deadline_s), int(deadline_ns)) 51 | out = libc.clock_nanosleep(clock, TIMER_ABSTIME, pointer(deadline), None) 52 | return out 53 | 54 | 55 | def timeval_to_datetime(val): 56 | """ 57 | Convert a C timeval object to a Python datetime 58 | Parameters 59 | ---------- 60 | val : bytes 61 | timeval object encoded as bytes 62 | Returns 63 | ------- 64 | datetime 65 | Python datetime object 66 | """ 67 | ts = timeval.from_buffer_copy(val) 68 | timestamp = datetime.fromtimestamp(ts.tv_sec + ts.tv_usec * 1e-6) 69 | return timestamp 70 | 71 | 72 | def timeval_to_timestamp(val): 73 | """ 74 | Convert a C timeval object to a timestamp 75 | Parameters 76 | ---------- 77 | val : bytes 78 | timeval object encoded as bytes 79 | Returns 80 | ------- 81 | float 82 | timestamp (in seconds) 83 | """ 84 | ts = timeval.from_buffer_copy(val) 85 | timestamp = ts.tv_sec + ts.tv_usec * 1e-6 86 | return timestamp 87 | 88 | 89 | def timespec_to_timestamp(val): 90 | """ 91 | Convert a C timespec object to a timestamp (in seconds) 92 | Parameters 93 | ---------- 94 | val : bytes 95 | timespec object encoded as bytes 96 | Returns 97 | ------- 98 | float 99 | Time in seconds 100 | """ 101 | ts = timespec.from_buffer_copy(val) 102 | timestamp = ts.tv_sec + ts.tv_nsec * 1e-9 103 | return timestamp 104 | 105 | 106 | def timevals_to_timestamps(vals): 107 | """ 108 | Convert a list of C timeval objects to a list of timestamps (in seconds) 109 | Parameters 110 | ---------- 111 | vals : bytes 112 | timeval objects encoded as bytes 113 | Returns 114 | ------- 115 | list 116 | List of timestamps in units of seconds 117 | """ 118 | tlen = TIMEVAL_LEN 119 | n_timevals = int(len(vals) / tlen) 120 | ts = [ 121 | timeval_to_timestamp(vals[i * tlen:(i + 1) * tlen]) 122 | for i in range(n_timevals) 123 | ] 124 | return ts 125 | 126 | 127 | def timespecs_to_timestamps(vals): 128 | """ 129 | Convert a list of C timespec objects to a list of timestamps (in seconds) 130 | Parameters 131 | ---------- 132 | vals : bytes 133 | timespec objects encoded as bytes 134 | Returns 135 | ------- 136 | list 137 | List of timestamps in units of seconds 138 | """ 139 | tlen = TIMESPEC_LEN 140 | n_timespecs = int(len(vals) / tlen) 141 | ts = [ 142 | timespec_to_timestamp(vals[i * tlen:(i + 1) * tlen]) 143 | for i in range(n_timespecs) 144 | ] 145 | return ts 146 | -------------------------------------------------------------------------------- /doc/DataSyncGuidelines.md: -------------------------------------------------------------------------------- 1 | # Guidelines for Data Alignment in BRAND 2 | 3 | BRAND is designed to run nodes in an asynchronous graph. To protect data integrity, we need to track flow of data through the graph. Nodes interacting with data `read` from and `add` to Redis streams. To track flow, we create a data label for every data entry to a Redis stream, and we record a global timestamp for when that data entry is queued for transfer to the Redis database. 4 | 5 | ## Data Labels 6 | 7 | There are two types of data writes within BRAND. 8 | 9 | 1. A node introduces data to the system (i.e. from a sensor) 10 | 2. A node produces processed data that derives from some consumed data 11 | 12 | Both of these types of interactions should use labels in a different way. 13 | 14 | ### New Data to the System 15 | 16 | Consider a node that parses incoming information from a different system not a part of BRAND. The data coming from that system will be introduced to BRAND, where the interfacing node is the generator of a stream with data from that external system. A few examples of this are: 17 | 18 | * The Cerebus records information from a cortical implant. A `cerebusAdapter` node is designed to read that data and introduce it to BRAND by storing it in a Redis `stream`. 19 | * A microphone records ambient audio. A `microphoneAdapter` node is designed to read in that ambient audio and introduce it to BRAND by storing it in a Redis `stream`. 20 | * A mouse tracks user movements. A `mouseAdapter` node is designed to read mouse inputs and introduce it to BRAND by storing it in a Redis `stream`. 21 | * In a more abstract case: a function generator node records the passage of time and generates some waverform as a function of time. A `functionGenerator` node is built to store this waveform in a Redis `stream`. 22 | 23 | The node that introduces data to BRAND must include a label for that data within the data's entry. Logically this can take any form, but for logging purposes it often helps if this is some clock. Within the entry, there should be a key named `sync`. This label should take the form: 24 | 25 | ``` 26 | {'sync':{: