diff --git a/.gitignore b/.gitignore index 6db19f0..9ecdf95 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ python_modules/**/*.c python_modules/**/*.h python_modules/**/*.cpp python_modules/**/*.pyc +python_modules/**/*.pyd python_modules/**/.ipynb_checkpoints .idea diff --git a/Builds/MacOSX/Config/Plugin.xcconfig b/Builds/MacOSX/Config/Plugin.xcconfig index 3bfb271..b567290 100644 --- a/Builds/MacOSX/Config/Plugin.xcconfig +++ b/Builds/MacOSX/Config/Plugin.xcconfig @@ -1,5 +1,7 @@ -PYTHON_DIR = /usr/local/anaconda -OPENEPHYS_SOURCE_DIR = ../../../plugin-GUI +PYTHON_DIR = /usr/local/anaconda/envs/oeEnv +PYTHON_VERSION = python3.6m + +OPENEPHYS_SOURCE_DIR = $(PROJECT_DIR)/../../../plugin-GUI ALWAYS_SEARCH_USER_PATHS = NO CLANG_CXX_LANGUAGE_STANDARD = c++0x @@ -40,13 +42,13 @@ GCC_WARN_UNDECLARED_SELECTOR = YES GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE GCC_WARN_UNUSED_FUNCTION = YES GCC_WARN_UNUSED_VARIABLE = YES -HEADER_SEARCH_PATHS = $(OPENEPHYS_SOURCE_DIR)/Source/Plugins/Headers $(PYTHON_DIR)/include/python3.5m $(inherited) +HEADER_SEARCH_PATHS = $(OPENEPHYS_SOURCE_DIR)/Source/Plugins/Headers $(OPENEPHYS_SOURCE_DIR)/JuceLibraryCode $(PYTHON_DIR)/include/$(PYTHON_VERSION) $(inherited) LIBRARY_SEARCH_PATHS = $(PYTHON_DIR)/lib -OTHER_LDFLAGS = -lpython3.5m $(inherited) -undefined dynamic_lookup +OTHER_LDFLAGS = -l$(PYTHON_VERSION) $(inherited) -undefined dynamic_lookup LD_RUNPATH_SEARCH_PATHS = $(PYTHON_DIR)/lib INSTALL_PATH = $(HOME)/Applications/open-ephys.app/Contents/PlugIns -MACOSX_DEPLOYMENT_TARGET = 10.9 -OTHER_LDFLAGS = $(inherited) -undefined dynamic_lookup + +OTHER_LDFLAGS = $(inherited) -undefined dynamic_lookup -L$(PYTHON_DIR)/lib SDKROOT = macosx SKIP_INSTALL = YES WARNING_CFLAGS = $(inherited) -Wpartial-availability -Wreorder -Wno-inconsistent-missing-override diff --git a/Builds/MacOSX/Config/Plugin.xcconfig.tmpl b/Builds/MacOSX/Config/Plugin.xcconfig.tmpl new file mode 100644 index 0000000..a220ca7 --- /dev/null +++ b/Builds/MacOSX/Config/Plugin.xcconfig.tmpl @@ -0,0 +1,55 @@ +PYTHON_DIR = #PYTHON_DIR# +PYTHON_VERSION = #PYTHON_VERSION# + +OPENEPHYS_SOURCE_DIR = $(PROJECT_DIR)/../../../plugin-GUI + +ALWAYS_SEARCH_USER_PATHS = NO +CLANG_CXX_LANGUAGE_STANDARD = c++0x +CLANG_CXX_LIBRARY = libc++ +CLANG_ENABLE_MODULES = NO +CLANG_ENABLE_OBJC_ARC = NO +CLANG_LINK_OBJC_RUNTIME = NO +CLANG_WARN_BOOL_CONVERSION = YES +CLANG_WARN_CONSTANT_CONVERSION = YES +CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR +CLANG_WARN_EMPTY_BODY = YES +CLANG_WARN_ENUM_CONVERSION = YES +CLANG_WARN_INT_CONVERSION = YES +CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR +CLANG_WARN_UNREACHABLE_CODE = YES +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CODE_SIGN_IDENTITY = +COMBINE_HIDPI_IMAGES = YES +CONFIGURATION_BUILD_DIR = $(PROJECT_DIR)/../../build/$(CONFIGURATION)/open-ephys.app/Contents/PlugIns +COPY_PHASE_STRIP = NO +DEBUG_INFORMATION_FORMAT = dwarf +ENABLE_STRICT_OBJC_MSGSEND = YES +GCC_C_LANGUAGE_STANDARD = c99 +GCC_GENERATE_DEBUGGING_SYMBOLS = YES +GCC_INLINES_ARE_PRIVATE_EXTERN = YES +GCC_NO_COMMON_BLOCKS = YES +GCC_OPTIMIZATION_LEVEL = 3 +COMMON_PREPROCESSOR_DEFINITIONS = $(inherited) OEPLUGIN JUCER_XCODE_MAC_F6D2F4CF=1 JUCE_APP_VERSION=0.3.5 JUCE_APP_VERSION_HEX=0x305 PYTHON_HOME=$(PYTHON_DIR) + +GCC_VERSION = com.apple.compilers.llvm.clang.1_0 +GCC_WARN_64_TO_32_BIT_CONVERSION = NO +GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR +GCC_WARN_CHECK_SWITCH_STATEMENTS = YES +GCC_WARN_MISSING_PARENTHESES = YES +GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES +GCC_WARN_TYPECHECK_CALLS_TO_PRINTF = YES +GCC_WARN_UNDECLARED_SELECTOR = YES +GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE +GCC_WARN_UNUSED_FUNCTION = YES +GCC_WARN_UNUSED_VARIABLE = YES +HEADER_SEARCH_PATHS = $(OPENEPHYS_SOURCE_DIR)/Source/Plugins/Headers $(OPENEPHYS_SOURCE_DIR)/JuceLibraryCode $(PYTHON_DIR)/include/$(PYTHON_VERSION) $(inherited) +LIBRARY_SEARCH_PATHS = $(PYTHON_DIR)/lib +OTHER_LDFLAGS = -l$(PYTHON_VERSION) $(inherited) -undefined dynamic_lookup +LD_RUNPATH_SEARCH_PATHS = $(PYTHON_DIR)/lib +INSTALL_PATH = $(HOME)/Applications/open-ephys.app/Contents/PlugIns + +OTHER_LDFLAGS = $(inherited) -undefined dynamic_lookup -L$(PYTHON_DIR)/lib +SDKROOT = macosx +SKIP_INSTALL = YES +WARNING_CFLAGS = $(inherited) -Wpartial-availability -Wreorder -Wno-inconsistent-missing-override +WRAPPER_EXTENSION = bundle diff --git a/Builds/MacOSX/Config/get_depends.sh b/Builds/MacOSX/Config/get_depends.sh new file mode 100755 index 0000000..7e8a5cb --- /dev/null +++ b/Builds/MacOSX/Config/get_depends.sh @@ -0,0 +1,23 @@ +#!/bin/bash + + +. ~/.bash_profile +. activate oeEnv + +CONFIG_DIR=${PROJECT_DIR}/Config + +PYTHON_DIR=$(python -c "from distutils import sysconfig; \ +import re; \ +z = sysconfig.get_config_var('prefix'); \ +print(z)") + +PYTHON_VERSION=$(python -c "from distutils import sysconfig; \ +import re; \ +z = sysconfig.get_config_var('BLDLIBRARY'); \ +m = re.search(r'-l(.*)', z); \ +print(m.group(1))") + +echo $PYTHON_DIR +echo $PYTHON_VERSION + +sed -e "s%#PYTHON_DIR#%${PYTHON_DIR}%" -e "s%#PYTHON_VERSION#%${PYTHON_VERSION}%" <${CONFIG_DIR}/Plugin.xcconfig.tmpl >${CONFIG_DIR}/Plugin.xcconfig diff --git a/Builds/MacOSX/PythonPlugin.xcodeproj/project.pbxproj b/Builds/MacOSX/PythonPlugin.xcodeproj/project.pbxproj index 6008705..0ecf21c 100644 --- a/Builds/MacOSX/PythonPlugin.xcodeproj/project.pbxproj +++ b/Builds/MacOSX/PythonPlugin.xcodeproj/project.pbxproj @@ -3,10 +3,11 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 48; objects = { /* Begin PBXBuildFile section */ + F7804A9121707A2E00401274 /* get_depends.sh in Resources */ = {isa = PBXBuildFile; fileRef = F7804A9021707A2E00401274 /* get_depends.sh */; }; F7956D431D58C04C00C2EF56 /* OpenEphysLib.cpp in Sources */ = {isa = PBXBuildFile; fileRef = F7956D421D58C04C00C2EF56 /* OpenEphysLib.cpp */; }; F7F7D1671D58C55E00DCF6CF /* PythonEditor.cpp in Sources */ = {isa = PBXBuildFile; fileRef = F7F7D1571D58C55E00DCF6CF /* PythonEditor.cpp */; }; F7F7D16A1D58C55E00DCF6CF /* PythonPlugin.cpp in Sources */ = {isa = PBXBuildFile; fileRef = F7F7D15F1D58C55E00DCF6CF /* PythonPlugin.cpp */; }; @@ -16,8 +17,9 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - F7956D281D579E1900C2EF56 /* PythonPlugin.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PythonPlugin.bundle; sourceTree = BUILT_PRODUCTS_DIR; }; - F7956D2B1D579E1900C2EF56 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = /Users/fpbatta/src/PythonPlugin/Builds/MacOSX/PythonPlugin/Info.plist; sourceTree = ""; }; + 3CB430A8215819B600F21E3C /* en */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; name = en; path = en.lproj/PythonPlugin.bundle; sourceTree = ""; }; + 3CB430AA21581E6E00F21E3C /* PythonPlugin.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PythonPlugin.bundle; sourceTree = BUILT_PRODUCTS_DIR; }; + F7804A9021707A2E00401274 /* get_depends.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = get_depends.sh; sourceTree = ""; }; F7956D381D58BBA100C2EF56 /* Plugin_Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Plugin_Debug.xcconfig; sourceTree = ""; }; F7956D391D58BBA100C2EF56 /* Plugin_Release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Plugin_Release.xcconfig; sourceTree = ""; }; F7956D3A1D58BBA100C2EF56 /* Plugin.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Plugin.xcconfig; sourceTree = ""; }; @@ -59,7 +61,8 @@ F7956D291D579E1900C2EF56 /* Products */ = { isa = PBXGroup; children = ( - F7956D281D579E1900C2EF56 /* PythonPlugin.bundle */, + 3CB430A9215819B600F21E3C /* PythonPlugin.bundle */, + 3CB430AA21581E6E00F21E3C /* PythonPlugin.bundle */, ); name = Products; sourceTree = ""; @@ -80,7 +83,6 @@ F7F7D1631D58C55E00DCF6CF /* PythonSource.cpp */, F7F7D1641D58C55E00DCF6CF /* PythonSource.h */, F7956D421D58C04C00C2EF56 /* OpenEphysLib.cpp */, - F7956D2B1D579E1900C2EF56 /* Info.plist */, ); path = PythonPlugin; sourceTree = ""; @@ -88,6 +90,7 @@ F7956D311D58BB2600C2EF56 /* Config */ = { isa = PBXGroup; children = ( + F7804A9021707A2E00401274 /* get_depends.sh */, F7956D381D58BBA100C2EF56 /* Plugin_Debug.xcconfig */, F7956D391D58BBA100C2EF56 /* Plugin_Release.xcconfig */, F7956D3A1D58BBA100C2EF56 /* Plugin.xcconfig */, @@ -102,6 +105,7 @@ isa = PBXNativeTarget; buildConfigurationList = F7956D2C1D579E1900C2EF56 /* Build configuration list for PBXNativeTarget "PythonPlugin" */; buildPhases = ( + F76C0DB8216FD73B000197D9 /* ShellScript */, F7956D241D579E1900C2EF56 /* Sources */, F7956D251D579E1900C2EF56 /* Frameworks */, F7956D261D579E1900C2EF56 /* Resources */, @@ -113,7 +117,7 @@ ); name = PythonPlugin; productName = PythonPlugin; - productReference = F7956D281D579E1900C2EF56 /* PythonPlugin.bundle */; + productReference = 3CB430AA21581E6E00F21E3C /* PythonPlugin.bundle */; productType = "com.apple.product-type.bundle"; }; /* End PBXNativeTarget section */ @@ -122,7 +126,7 @@ F7956D1E1D579DDF00C2EF56 /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 0920; + LastUpgradeCheck = 1020; TargetAttributes = { F7956D271D579E1900C2EF56 = { CreatedOnToolsVersion = 7.3.1; @@ -130,10 +134,11 @@ }; }; buildConfigurationList = F7956D211D579DDF00C2EF56 /* Build configuration list for PBXProject "PythonPlugin" */; - compatibilityVersion = "Xcode 3.2"; + compatibilityVersion = "Xcode 8.0"; developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, ); mainGroup = F7956D1D1D579DDF00C2EF56; @@ -151,12 +156,30 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + F7804A9121707A2E00401274 /* get_depends.sh in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + F76C0DB8216FD73B000197D9 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "${PROJECT_DIR}/Config/get_depends.sh\n"; + }; F7F7D16F1D58E44D00DCF6CF /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -168,7 +191,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "cp -R ~/src/PythonPlugin/build/Debug/open-ephys.app/Contents/PlugIns/PythonPlugin.bundle ~/src/plugin-GUI/Builds/MacOSX/build/Debug/open-ephys.app/Contents/PlugIns"; + shellScript = "cp -R ${PROJECT_DIR}/../../build/Debug/open-ephys.app/Contents/PlugIns/PythonPlugin.bundle ${PROJECT_DIR}/../../../plugin-GUI/Builds/MacOSX/build/Debug/open-ephys.app/Contents/PlugIns\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -188,34 +211,35 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXVariantGroup section */ + 3CB430A9215819B600F21E3C /* PythonPlugin.bundle */ = { + isa = PBXVariantGroup; + children = ( + 3CB430A8215819B600F21E3C /* en */, + ); + name = PythonPlugin.bundle; + sourceTree = BUILT_PRODUCTS_DIR; + }; +/* End PBXVariantGroup section */ + /* Begin XCBuildConfiguration section */ F7956D221D579DDF00C2EF56 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = F7956D381D58BBA100C2EF56 /* Plugin_Debug.xcconfig */; buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_COMMA = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; - HEADER_SEARCH_PATHS = ( - "$(OPENEPHYS_SOURCE_DIR)/Source/Plugins/Headers/**", - "$(OPENEPHYS_SOURCE_DIR)/JuceLibraryCode", - "$(OPENEPHYS_SOURCE_DIR)/JuceLibraryCode/modules/**", - "$(PYTHON_DIR)/include/python3.5m", - "$(inherited)", - ); - LD_RUNPATH_SEARCH_PATHS = /usr/local/anaconda/lib; - LIBRARY_SEARCH_PATHS = /usr/local/anaconda/lib; - OTHER_LDFLAGS = ( - "-lpython3.5m", - "$(inherited)", - "-undefined", - dynamic_lookup, - ); + LD_RUNPATH_SEARCH_PATHS = ""; }; name = Debug; }; @@ -223,29 +247,19 @@ isa = XCBuildConfiguration; baseConfigurationReference = F7956D391D58BBA100C2EF56 /* Plugin_Release.xcconfig */; buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_COMMA = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; - HEADER_SEARCH_PATHS = ( - "$(OPENEPHYS_SOURCE_DIR)/Source/Plugins/Headers/**", - "$(OPENEPHYS_SOURCE_DIR)/JuceLibraryCode", - "$(OPENEPHYS_SOURCE_DIR)/JuceLibraryCode/modules/**", - "$(PYTHON_DIR)/include/python3.5m", - "$(inherited)", - ); - LD_RUNPATH_SEARCH_PATHS = /usr/local/anaconda/lib; - LIBRARY_SEARCH_PATHS = /usr/local/anaconda/lib; - OTHER_LDFLAGS = ( - "-lpython3.5m", - "$(inherited)", - "-undefined", - dynamic_lookup, - ); + LD_RUNPATH_SEARCH_PATHS = ""; }; name = Release; }; @@ -253,7 +267,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = F7956D381D58BBA100C2EF56 /* Plugin_Debug.xcconfig */; buildSettings = { - ALWAYS_SEARCH_USER_PATHS = YES; + ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "c++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -271,6 +285,7 @@ CODE_SIGN_IDENTITY = ""; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -290,12 +305,24 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - HEADER_SEARCH_PATHS = "$(inherited)"; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "$(OPENEPHYS_SOURCE_DIR)/JuceLibraryCode/modules/**", + "(OPENEPHYS_SOURCE_DIR)/JuceLibraryCode", + "$(OPENEPHYS_SOURCE_DIR)/Source/Plugins/Headers/**", + ); INFOPLIST_FILE = PythonPlugin/Info.plist; INSTALL_PATH = "$(HOME)/Applications/open-ephys.app/Contents/PlugIns"; - MACOSX_DEPLOYMENT_TARGET = 10.11; + LIBRARY_SEARCH_PATHS = ""; + MACOSX_DEPLOYMENT_TARGET = 10.12; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; + OTHER_LDFLAGS = ( + "$(inherited)", + "-bundle", + "-undefined", + dynamic_lookup, + ); PRODUCT_BUNDLE_IDENTIFIER = nl.battaglia.PythonPlugin; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; @@ -313,7 +340,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = F7956D391D58BBA100C2EF56 /* Plugin_Release.xcconfig */; buildSettings = { - ALWAYS_SEARCH_USER_PATHS = YES; + ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "c++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -332,6 +359,7 @@ COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; + "DEAD_CODE_STRIPPING[arch=*]" = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -351,11 +379,23 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - HEADER_SEARCH_PATHS = "$(inherited)"; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "$(OPENEPHYS_SOURCE_DIR)/JuceLibraryCode/modules/**", + "(OPENEPHYS_SOURCE_DIR)/JuceLibraryCode", + "$(OPENEPHYS_SOURCE_DIR)/Source/Plugins/Headers/**", + ); INFOPLIST_FILE = PythonPlugin/Info.plist; INSTALL_PATH = "$(HOME)/Applications/open-ephys.app/Contents/PlugIns"; - MACOSX_DEPLOYMENT_TARGET = 10.11; + LIBRARY_SEARCH_PATHS = ""; + MACOSX_DEPLOYMENT_TARGET = 10.12; MTL_ENABLE_DEBUG_INFO = NO; + OTHER_LDFLAGS = ( + "$(inherited)", + "-bundle", + "-undefined", + dynamic_lookup, + ); PRODUCT_BUNDLE_IDENTIFIER = nl.battaglia.PythonPlugin; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; diff --git a/Builds/MacOSX/PythonPlugin.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Builds/MacOSX/PythonPlugin.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Builds/MacOSX/PythonPlugin.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/PythonPlugin/Info.plist b/PythonPlugin/Info.plist new file mode 100644 index 0000000..95e09f8 --- /dev/null +++ b/PythonPlugin/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + NSPrincipalClass + + + diff --git a/PythonPlugin/Makefile b/PythonPlugin/Makefile deleted file mode 120000 index 0c9d60c..0000000 --- a/PythonPlugin/Makefile +++ /dev/null @@ -1 +0,0 @@ -../Builds/Linux/Makefile \ No newline at end of file diff --git a/PythonPlugin/Makefile b/PythonPlugin/Makefile new file mode 100644 index 0000000..5dfa3c3 --- /dev/null +++ b/PythonPlugin/Makefile @@ -0,0 +1,51 @@ +PYTHON_HOME := $(CONDA_HOME) +PYTHON_INCDIR := $(CONDA_HOME)/include/$(PYTHON_VERSION) +PYTHON_LIBDIR := $(CONDA_HOME)/lib +PYTHON_RTLIBDIR := $(PYTHON_LIBDIR) + +LIBNAME := $(notdir $(CURDIR)) +OBJDIR := $(OBJDIR)/$(LIBNAME) +TARGET := $(LIBNAME).so +OS := $(shell uname) + +SRC_DIR := ${shell find ./ -type d -print} +VPATH := $(SOURCE_DIRS) + +SRC := $(foreach sdir,$(SRC_DIR),$(wildcard $(sdir)/*.cpp)) +OBJ := $(addprefix $(OBJDIR)/,$(notdir $(SRC:.cpp=.o))) + + + +#Extra macros and libraries needed by the plugin +#CXXFLAGS := $(CXXFLAGS) -D "PYTHON_HOME=$(PYTHON_HOME)" -I $(PYTHON_INCDIR) +CXXFLAGS := $(CXXFLAGS) -D "PYTHON_HOME=$(PYTHON_HOME)" -I $(PYTHON_INCDIR) +LDFLAGS := $(LDFLAGS) -l$(PYTHON_VERSION) -L$(PYTHON_LIBDIR) -Wl,-rpath=$(PYTHON_RTLIBDIR) + + +BLDCMD := $(CXX) -shared -o $(OUTDIR)/$(TARGET) $(OBJ) $(LDFLAGS) $(RESOURCES) $(TARGET_ARCH) + +VPATH = $(SRC_DIR) + +.PHONY: objdir + +$(OUTDIR)/$(TARGET): objdir $(OBJ) + -@mkdir -p $(BINDIR) + -@mkdir -p $(LIBDIR) + -@mkdir -p $(OUTDIR) + @echo "Building $(TARGET)" + @$(BLDCMD) + +$(OBJDIR)/%.o : %.cpp + @echo "Compiling $<" + @$(CXX) $(CXXFLAGS) -o "$@" -c "$<" + + +objdir: + -@mkdir -p $(OBJDIR) + +clean: + @echo "Cleaning $(LIBNAME)" + -@rm -rf $(OBJDIR) + -@rm -f $(OUTDIR)/$(TARGET) + +-include $(OBJ:%.o=%.d) diff --git a/PythonPlugin/OpenEphysLib.cpp b/PythonPlugin/OpenEphysLib.cpp index a324e34..14dd25c 100644 --- a/PythonPlugin/OpenEphysLib.cpp +++ b/PythonPlugin/OpenEphysLib.cpp @@ -25,6 +25,10 @@ */ +#include "PythonFilter.h" +#include "PythonSource.h" +#include "PythonSink.h" + #include #include #ifdef WIN32 @@ -34,10 +38,6 @@ #define EXPORT #endif -#include "PythonFilter.h" -#include "PythonSource.h" -#include "PythonSink.h" - using namespace Plugin; //Number of plugins defined on the library. Can be of different types (Processors, RecordEngines, etc...) #define NUM_PLUGINS 3 @@ -46,7 +46,7 @@ extern "C" EXPORT void getLibInfo(Plugin::LibraryInfo* info) { info->apiVersion = PLUGIN_API_VER; /*API version, defined by the GUI source. Should not be changed to ensure it is always equal to the one used in the latest codebase. The GUI refueses to load plugins with mismatched API versions */ - info->name = "Example library"; //Name of the Library, used only for information + info->name = "Python Plugin"; //Name of the Library, used only for information info->libVersion = 1; //Version of the library, used only for information info->numPlugins = NUM_PLUGINS; } diff --git a/PythonPlugin/PythonEditor.cpp b/PythonPlugin/PythonEditor.cpp index c589e98..86bcb03 100644 --- a/PythonPlugin/PythonEditor.cpp +++ b/PythonPlugin/PythonEditor.cpp @@ -34,9 +34,8 @@ ============================================================================== */ -#include "PythonEditor.h" - #include "PythonPlugin.h" +#include "PythonEditor.h" #include @@ -67,7 +66,7 @@ PythonEditor::PythonEditor(GenericProcessor* parentNode, bool useDefaultParamete PythonEditor::~PythonEditor() { - for(int i; i < parameterInterfaces.size(); i++) + for(int i=0; i < parameterInterfaces.size(); i++) { removeChildComponent(parameterInterfaces[i]); } @@ -147,6 +146,11 @@ void PythonEditor::buttonEvent(Button* button) } } +void PythonEditor::channelChanged(int chan, bool newState) +{ + pythonPlugin->channelChanged(chan, newState); +} + void PythonEditor::saveCustomParameters(XmlElement* xml) { diff --git a/PythonPlugin/PythonEditor.h b/PythonPlugin/PythonEditor.h index 719ab3b..a979628 100644 --- a/PythonPlugin/PythonEditor.h +++ b/PythonPlugin/PythonEditor.h @@ -51,6 +51,8 @@ class PythonEditor : public GenericEditor void buttonEvent(Button* button); + void channelChanged(int chan, bool newState) override; + void setFile(String file); void saveCustomParameters(XmlElement*); diff --git a/PythonPlugin/PythonPlugin b/PythonPlugin/PythonPlugin new file mode 120000 index 0000000..4237948 --- /dev/null +++ b/PythonPlugin/PythonPlugin @@ -0,0 +1 @@ +../../../PythonPlugin/PythonPlugin/ \ No newline at end of file diff --git a/PythonPlugin/PythonPlugin.cpp b/PythonPlugin/PythonPlugin.cpp index 16aa592..2bd26ed 100644 --- a/PythonPlugin/PythonPlugin.cpp +++ b/PythonPlugin/PythonPlugin.cpp @@ -34,17 +34,18 @@ v ============================================================================== */ -#include "PythonPlugin.h" - - - - #include "PythonPlugin.h" #include "PythonEditor.h" +#ifdef _WIN32 +#include +#else #include +#endif + #include #include +#include #ifdef DEBUG #define PYTHON_DEBUG @@ -54,74 +55,38 @@ v #if defined(__linux__) #include #include -#else +#elif !defined(_WIN32) #include #endif #endif - - - -PythonPlugin::PythonPlugin(const String &processorName) - : GenericProcessor(processorName) //, threshold(200.0), state(true) - -{ - - //parameters.add(Parameter("thresh", 0.0, 500.0, 200.0, 0)); - filePath = ""; - plugin = 0; -#define QUOTE(name) #name -#define STR(macro) QUOTE(macro) -#define PYTHON_HOME_NAME STR(PYTHON_HOME) - char * old_python_home = getenv("PYTHONHOME"); - if (old_python_home == NULL) - { -#ifdef PYTHON_DEBUG - std::cout << "setting PYTHONHOME" << std::endl; -#endif - setenv("PYTHONHOME", PYTHON_HOME_NAME, 1); - } - // setenv("PYTHONHOME", "/usr/local/anaconda", 1); // FIXME hardcoded PYTHONHOME! - -#ifdef PYTHON_DEBUG - std::cout << "PYTHONHOME: " << getenv("PYTHONHOME") << std::endl; -#endif - +// debug logs when entering function #ifdef PYTHON_DEBUG #if defined(__linux__) - pid_t tid; - tid = syscall(SYS_gettid); +#define GET_TID pid_t tid = syscall(SYS_gettid) +#elif defined(_WIN32) +#define GET_TID DWORD tid = GetCurrentThreadId() #else - uint64_t tid; - pthread_threadid_np(NULL, &tid); +#define GET_TID uint64_t tid; pthread_threadid_np(NULL, &tid) #endif - std::cout << "in constructor pthread_threadid_np()=" << tid << std::endl; -#endif - +#define DEBUG_LOG(str) JUCE_BLOCK_WITH_FORCED_SEMICOLON(std::cout << str << std::endl;) -#if PY_MAJOR_VERSION==3 - Py_SetProgramName ((wchar_t *)"PythonPlugin"); -#else - Py_SetProgramName ((char *)"PythonPlugin"); -#endif - Py_Initialize (); - PyEval_InitThreads(); +#define LOG_ENTER(fname) \ + GET_TID; \ + std::cout << "in " << fname << " pthread_threadid_np()=" << tid << std::endl - - PyRun_SimpleString("import sys"); - PyRun_SimpleString("sys.setcheckinterval(10000)"); -#ifdef PYTHON_DEBUG - std::cout << Py_GetPrefix() << std::endl; - std::cout << Py_GetVersion() << std::endl; +#else // not debugging +#define DEBUG_LOG(str) +#define LOG_ENTER(fname) #endif - GUIThreadState = PyEval_SaveThread(); -} -PythonPlugin::~PythonPlugin() +PythonPlugin::PythonPlugin(const String &processorName) + : GenericProcessor(processorName) //, threshold(200.0), state(true) { - dlclose(plugin); + LOG_ENTER("constructor"); } + void PythonPlugin::createEventChannels() { EventChannel* ev = new EventChannel(EventChannel::TTL, 8, 1, CoreServices::getGlobalSampleRate(), this); @@ -141,142 +106,46 @@ void PythonPlugin::createEventChannels() AudioProcessorEditor* PythonPlugin::createEditor() { - -// std::cout << "in PythonEditor::createEditor()" << std::endl; editor = new PythonEditor(this, true); return editor; - } bool PythonPlugin::isReady() { -#ifdef PYTHON_DEBUG -#if defined(__linux__) - pid_t tid; - tid = syscall(SYS_gettid); -#else - uint64_t tid; - pthread_threadid_np(NULL, &tid); -#endif - std::cout << "in isReady pthread_threadid_np()=" << tid << std::endl; -#endif - + LOG_ENTER("isReady"); - bool ret; - PyEval_RestoreThread(GUIThreadState); - if (plugin == 0 ) + if (plugin.getNativeHandle() == nullptr) { - // sendActionMessage("No plugin selected in Python Plugin."); // FIXME how to send error message? - ret = false; - } - else if (pluginIsReady && !(*pluginIsReady)()) - { - // sendActionMessage("Plugin is not ready"); // FIXME how to send error message? - ret = false; + CoreServices::sendStatusMessage ("No plugin selected in Python Plugin."); + return false; } else { - ret = true; + const PythonLock pyLock; + if (pluginIsReady && !(*pluginIsReady)()) + { + CoreServices::sendStatusMessage("Python Plugin is not ready"); + return false; + } + return true; } - GUIThreadState = PyEval_SaveThread(); - return ret; - } -void PythonPlugin::setParameter(int parameterIndex, float newValue) -{ - editor->updateParameterButtons(parameterIndex); - - //Parameter& p = parameters.getReference(parameterIndex); - //p.setValue(newValue, 0); - //threshold = newValue; - - //std::cout << float(p[0]) << std::endl; - editor->updateParameterButtons(parameterIndex); -} - - -void PythonPlugin::resetConnections() +void PythonPlugin::process(AudioSampleBuffer& buffer) { - -#ifdef PYTHON_DEBUG -#if defined(__linux__) - pid_t tid; - tid = syscall(SYS_gettid); -#else - uint64_t tid; - pthread_threadid_np(NULL, &tid); -#endif - std::cout << "in resetConnection pthread_threadid_np()=" << tid << std::endl; -#endif + LOG_ENTER("process"); - nextAvailableChannel = 0; - - wasConnected = false; -#ifdef PYTHON_DEBUG - std::cout << "resetting ThreadState, which was " << processThreadState << std::endl; -#endif - processThreadState = 0; -} + PythonEvent *pyEvents = (PythonEvent *)calloc(1, sizeof(PythonEvent)); + pyEvents->type = 0; // this marks an empty event -void PythonPlugin::process(AudioSampleBuffer& buffer) -{ - checkForEvents(); -#ifdef PYTHON_DEBUG -#if defined(__linux__) - pid_t tid; - tid = syscall(SYS_gettid); -#else - uint64_t tid; - pthread_threadid_np(NULL, &tid); -#endif - // std::cout << "in process pthread_threadid_np()=" << tid << std::endl; -#endif - - - if(!processThreadState) { - - //DEBUG - PyThreadState *nowState; - nowState = PyGILState_GetThisThreadState(); -#ifdef PYTHON_DEBUG - std::cout << "currentState: " << nowState << std::endl; - std::cout << "initialiting ThreadState" << std::endl; -#endif - if(nowState) //UGLY HACK!!! - { - processThreadState = nowState; - } - else - { - processThreadState = PyThreadState_New(GUIThreadState->interp); - } - if(!processThreadState) - std::cout << "ThreadState is Null!" << std::endl; + const PythonLock pyLock; + (*pluginFunction)(*(buffer.getArrayOfWritePointers()), buffer.getNumChannels(), buffer.getNumSamples(), getNumSamples(0), pyEvents); } - PyEval_RestoreThread(processThreadState); - - PythonEvent *pyEvents = (PythonEvent *)calloc(1, sizeof(PythonEvent)); - pyEvents->type = 0; // this marks an empty event -#ifdef PYTHON_DEBUG - // std::cout << "in process, trying to acquire lock" << std::endl; -#endif - - // PyEval_InitThreads(); -// -// std::cout << "in process, threadstate: " << PyGILState_GetThisThreadState() << std::endl; -// PyGILState_STATE gstate; -// gstate = PyGILState_Ensure(); -// std::cout << "in process, lock acquired" << std::endl; - (*pluginFunction)(*(buffer.getArrayOfWritePointers()), buffer.getNumChannels(), buffer.getNumSamples(), getNumSamples(0), pyEvents); -// PyGILState_Release(gstate); -// std::cout << "in process, lock released" << std::endl; - if(wasTriggered) { uint8 ttlData = 0; @@ -288,12 +157,6 @@ void PythonPlugin::process(AudioSampleBuffer& buffer) } if(pyEvents->type != 0) { -#ifdef PYTHON_DEBUG - // std::cout << "Event emitted " << (int)pyEvents->type << std::endl; -#endif - // uint8 ttlData = 1 << module.outputChan; - // TTLEventPtr event = TTLEvent::createTTLEvent(moduleEventChannels[m], getTimestamp(module.inputChan) + i, &ttlData, sizeof(uint8), module.outputChan); - // addEvent(moduleEventChannels[m], event, i); lastChan = (uint16)pyEvents->eventId; uint8 ttlData = 1 << lastChan; @@ -326,13 +189,84 @@ void PythonPlugin::process(AudioSampleBuffer& buffer) wasTriggered = true; } } +} + +/** START CJB ADDED **/ + +void PythonPlugin::handleEvent(const EventChannel* eventInfo, const MidiMessage& event, int sampleNum){ + int eventType; + int sourceID; + int subProcessorIdx; + double timestamp; + int sourceIndex; + const void* ptr; - processThreadState = PyEval_SaveThread(); -#ifdef PYTHON_DEBUG - // std::cout << "Thread saved" << std::endl; -#endif + if (eventInfo->getChannelType() == EventChannel::TTL) + { + + TTLEventPtr ttl = TTLEvent::deserializeFromMessage(event, eventInfo); + + eventType = int(ttl->getEventType()); + sourceID = int(ttl->getSourceID()); + subProcessorIdx = int(ttl->getSubProcessorIdx()); + timestamp = double(ttl->getTimestamp()); + sourceIndex = int(ttl->getSourceIndex()); + //ptr = ttl->getRawDataPointer(); + sendEventPlugin(eventType, sourceID, subProcessorIdx, timestamp, sourceIndex); + } + else if (eventInfo->getChannelType() == EventChannel::TEXT) + { + + TextEventPtr txt = TextEvent::deserializeFromMessage(event, eventInfo); + eventType = int(txt->getEventType()); + sourceID = int(txt->getSourceID()); + subProcessorIdx = int(txt->getSubProcessorIdx()); + timestamp = double(txt->getTimestamp()); + sourceIndex = int(txt->getSourceIndex()); + ptr = txt->getRawDataPointer(); + sendEventPlugin(eventType, sourceID, subProcessorIdx, timestamp, sourceIndex); + } + else if (eventInfo->getChannelType() == EventChannel::TEXT) + { + + BinaryEventPtr bi = BinaryEvent::deserializeFromMessage(event, eventInfo); + eventType = int(bi->getEventType()); + sourceID = int(bi->getSourceID()); + subProcessorIdx = int(bi->getSubProcessorIdx()); + timestamp = double(bi->getTimestamp()); + sourceIndex = int(bi->getSourceIndex()); + //ptr = bi->getRawDataPointer(); + sendEventPlugin(eventType, sourceID, subProcessorIdx, timestamp, sourceIndex); + } +} + +void PythonPlugin::sendEventPlugin(int eventType, int sourceID, int subProcessorIdx, double timestamp, int sourceIndex) +{ + LOG_ENTER("sendEventPlugin"); + + const PythonLock pyLock; + (*eventFunction)(eventType, sourceID, subProcessorIdx,timestamp,sourceIndex); +} + +void PythonPlugin::handleSpike(const SpikeChannel* spikeInfo, const MidiMessage& event, int samplePosition) +{ + LOG_ENTER("handleSpike"); + + SpikeEventPtr newSpike = SpikeEvent::deserializeFromMessage(event, spikeInfo); + const float* dataPtr = newSpike->getDataPointer(); + float spikeBuf[18]; + for(int i = 0 ;i < 18;i++){ + spikeBuf[i] = dataPtr[i]; + } + //juce::uint16 + int sortedID = int(newSpike->getSortedID()); + int electrode = getSpikeChannelIndex(newSpike); + + const PythonLock pyLock; + (*spikeFunction)(electrode, sortedID, spikeBuf); } +/** END CJB ADDED **/ /* The complete API that the Cython plugin has to expose is void pluginStartup(void): a function to initialize the plugin data structures prior to start ACQ @@ -343,235 +277,258 @@ void PythonPlugin::process(AudioSampleBuffer& buffer) void set FloatParameter(char *name, float value) set float parameter */ -void PythonPlugin::setFile(String fullpath) + +String lastError() { -#ifdef PYTHON_DEBUG -#if defined(__linux__) - pid_t tid; - tid = syscall(SYS_gettid); + String message; +#ifdef _WIN32 + /*Get the error message, if any.*/ + DWORD errorMessageID = ::GetLastError(); + if (errorMessageID != 0) // Error message has been recorded + { + LPSTR messageBuffer = nullptr; + size_t size = FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, errorMessageID, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&messageBuffer, 0, NULL); + + message = String(messageBuffer, size); + + //Free the buffer. + LocalFree(messageBuffer); + } + #else - uint64_t tid; - pthread_threadid_np(NULL, &tid); + message = String(dlerror()); #endif - std::cout << "in setFile pthread_threadid_np()=" << tid << std::endl; -#endif - + return message; +} +void PythonPlugin::setFile(String fullpath) +{ + LOG_ENTER("setFile"); + filePath = fullpath; + if (!plugin.open(filePath)) + { + std::cout << "Can't open plugin " + << '"' << filePath << '"' << std::endl + << lastError() << std::endl; + return; + } - const char* path = filePath.getCharPointer(); - plugin = dlopen(path, RTLD_LAZY); - if (!plugin) - { - std::cout << "Can't open plugin " - << '"' << path << "\"" - << dlerror() - << std::endl; - return; - } - - String initPlugin = filePath.fromLastOccurrenceOf(String("/"), false, true); - - initPlugin = initPlugin.upToFirstOccurrenceOf(String("."), false, true); + String pluginName = File(filePath).getFileName().upToFirstOccurrenceOf(".", false, true); #if PY_MAJOR_VERSION>=3 String initPluginName = String("PyInit_"); #else String initPluginName = String("init"); #endif - initPluginName.append(initPlugin, 200); + initPluginName.append(pluginName, 200); std::cout << "init function is: " << initPluginName << std::endl; - void *initializer = dlsym(plugin,initPluginName.getCharPointer()); - -#ifdef PYTHON_DEBUG - std::cout << "initializer: " << initializer << std::endl; -#endif + void *initializer = plugin.getFunction(initPluginName); + DEBUG_LOG("initializer: " << initializer); if (!initializer) { - std::cout << "Can't find init function in plugin " - << '"' << path << "\"" << std::endl - << dlerror() - << std::endl; - plugin = 0; - return; + std::cout << "Can't find init function in plugin " + << '"' << pluginName << '"' << std::endl + << lastError() << std::endl; + resetPlugin(); + return; } - - initfunc_t initF = (initfunc_t) initializer; - - void *cfunc = dlsym(plugin,"pluginisready"); + initfunc_t initF = (initfunc_t)initializer; + + void *cfunc = plugin.getFunction("pluginisready"); if (!cfunc) { - std::cout << "Can't find ready function in plugin " - << '"' << path << "\"" << std::endl - << dlerror() - << std::endl; - plugin = 0; - return; + std::cout << "Can't find ready function in plugin " + << '"' << pluginName << '"' << std::endl + << lastError() << std::endl; + resetPlugin(); + return; } pluginIsReady = (isreadyfunc_t)cfunc; - cfunc = dlsym(plugin,"pluginStartup"); + cfunc = plugin.getFunction("pluginStartup"); if (!cfunc) { - std::cout << "Can't find startup function in plugin " - << '"' << path << "\"" << std::endl - << dlerror() - << std::endl; - plugin = 0; + std::cout << "Can't find startup function in plugin " + << '"' << pluginName << '"' << std::endl + << lastError() << std::endl; + resetPlugin(); return; } pluginStartupFunction = (startupfunc_t)cfunc; - - cfunc = dlsym(plugin,"getParamNum"); + std::cout << "loaded pluginStartup \n \n \n \n \n "; + + cfunc = plugin.getFunction("getParamNum"); if (!cfunc) { - std::cout << "Can't find getParamNum function in plugin " - << '"' << path << "\"" << std::endl - << dlerror() - << std::endl; - plugin = 0; + std::cout << "Can't find getParamNum function in plugin " + << '"' << pluginName << '"' << std::endl + << lastError() << std::endl; + resetPlugin(); return; } getParamNumFunction = (getparamnumfunc_t)cfunc; - - cfunc = dlsym(plugin,"getParamConfig"); + cfunc = plugin.getFunction("getParamConfig"); if (!cfunc) { - std::cout << "Can't find getParamNum function in plugin " - << '"' << path << "\"" << std::endl - << dlerror() - << std::endl; - // plugin = 0; - // return; + std::cout << "Can't find getParamConfig function in plugin " + << '"' << pluginName << '"' << std::endl + << lastError() << std::endl; + resetPlugin(); + return; } getParamConfigFunction = (getparamconfigfunc_t)cfunc; - - cfunc = dlsym(plugin,"pluginFunction"); - // std::cout << "plugin: " << cfunc << std::endl; + cfunc = plugin.getFunction("pluginFunction"); if (!cfunc) { - std::cout << "Can't find plugin function in plugin " - << '"' << path << "\"" << std::endl - << dlerror() - << std::endl; - plugin = 0; - return; + std::cout << "Can't find plugin function in plugin " + << '"' << pluginName << '"' << std::endl + << lastError() << std::endl; + resetPlugin(); + return; } pluginFunction = (pluginfunc_t)cfunc; - - cfunc = dlsym(plugin,"setIntParam"); - // std::cout << "plugin: " << cfunc << std::endl; + // CJB added start + cfunc = plugin.getFunction("eventFunction"); if (!cfunc) { - std::cout << "Can't find setIntParam function in plugin " - << '"' << path << "\"" << std::endl - << dlerror() - << std::endl; - plugin = 0; - return; + std::cout << "Can't find event function in plugin " + << '"' << pluginName << '"' << std::endl + << lastError() << std::endl; + resetPlugin(); + return; } - setIntParamFunction = (setintparamfunc_t)cfunc; - - cfunc = dlsym(plugin,"setFloatParam"); - // std::cout << "plugin: " << cfunc << std::endl; + eventFunction = (eventfunc_t)cfunc; + + cfunc = plugin.getFunction("spikeFunction"); if (!cfunc) { - std::cout << "Can't find setFloatParam function in plugin " - << '"' << path << "\"" << std::endl - << dlerror() - << std::endl; - plugin = 0; - return; + std::cout << "Can't find spike function in plugin " + << '"' << pluginName << '"' << std::endl + << lastError() << std::endl; + resetPlugin(); + return; } + spikeFunction = (spikefunc_t)cfunc; + // CJB added end + + cfunc = plugin.getFunction("updateSettings"); + if (!cfunc) + { + std::cout << "Can't find updateSettings function in plugin " + << '"' << pluginName << "\"" << std::endl + << lastError() << std::endl; + resetPlugin(); + return; + } + updateSettingsFunction = (updatefunc_t)cfunc; + + cfunc = plugin.getFunction("channelChanged"); + if (!cfunc) + { + std::cout << "Can't find channelChanged function in plugin " + << '"' << pluginName << "\"" << std::endl + << lastError() << std::endl; + resetPlugin(); + return; + } + channelChangedFunction = (chanchangefunc_t)cfunc; + + cfunc = plugin.getFunction("setIntParam"); + if (!cfunc) + { + std::cout << "Can't find setIntParam function in plugin " + << '"' << pluginName << "\"" << std::endl + << lastError() << std::endl; + resetPlugin(); + return; + } + setIntParamFunction = (setintparamfunc_t)cfunc; + + cfunc = plugin.getFunction("setFloatParam"); + if (!cfunc) + { + std::cout << "Can't find setFloatParam function in plugin " + << '"' << pluginName << "\"" << std::endl + << lastError() << std::endl; + resetPlugin(); + return; + } setFloatParamFunction = (setfloatparamfunc_t)cfunc; - cfunc = dlsym(plugin, "getIntParam"); - // std::cout << "plugin: " << cfunc << std::endl; + cfunc = plugin.getFunction("getIntParam"); if (!cfunc) { std::cout << "Can't find getIntParam function in plugin " - << '"' << path << "\"" << std::endl - << dlerror() - << std::endl; - plugin = 0; + << '"' << pluginName << "\"" << std::endl + << lastError() << std::endl; + resetPlugin(); return; } getIntParamFunction = (getintparamfunc_t)cfunc; - - - cfunc = dlsym(plugin, "getFloatParam"); - // std::cout << "plugin: " << cfunc << std::endl; + cfunc = plugin.getFunction("getFloatParam"); if (!cfunc) { std::cout << "Can't find getFloatParam function in plugin " - << '"' << path << "\"" << std::endl - << dlerror() - << std::endl; - plugin = 0; + << '"' << pluginName << "\"" << std::endl + << lastError() << std::endl; + resetPlugin(); return; } - getFloatParamFunction = (getfloatparamfunc_t)cfunc; + // now the API should be fully loaded -// now the API should be fully loaded - - PyEval_RestoreThread(GUIThreadState); + const PythonLock pyLock; // initialize the plugin -#ifdef PYTHON_DEBUG - std::cout << "before initplugin" << std::endl; // DEBUG -#endif - + + DEBUG_LOG("before initplugin"); + (*initF)(); -#ifdef PYTHON_DEBUG - std::cout << "after initplugin" << std::endl; // DEBUG -#endif - - (*pluginStartupFunction)(getSampleRate()); + DEBUG_LOG("after initplugin"); + + + (*pluginStartupFunction)(nChans, dataSampleRate, chanEnabled.getRawDataPointer()); // load the parameter configuration numPythonParams = (*getParamNumFunction)(); -#ifdef PYTHON_DEBUG - std::cout << "the plugin wants " << numPythonParams - << " parameters" << std::endl; -#endif + DEBUG_LOG("the plugin wants " << numPythonParams << " parameters"); + params = (ParamConfig *)calloc(numPythonParams, sizeof(ParamConfig)); paramsControl = (Component **)calloc(numPythonParams, sizeof(Component *)); (*getParamConfigFunction)(params); -#ifdef PYTHON_DEBUG - std::cout << "release paramconfig" << std::endl; -#endif + DEBUG_LOG("release paramconfig"); + auto ed = static_cast(getEditor()); for(int i = 0; i < numPythonParams; i++) { -#ifdef PYTHON_DEBUG - std::cout << "param " << i << " is a " << params[i].type << std::endl; - std::cout << "it is named: " << params[i].name << std::endl << std::endl; -#endif + DEBUG_LOG("param " << i << " is a " << params[i].type); + DEBUG_LOG("it is named: " << params[i].name << std::endl); + switch (params[i].type) { case TOGGLE: - paramsControl[i] = dynamic_cast(getEditor())->addToggleButton(String(params[i].name), params[i].isEnabled); + paramsControl[i] = ed->addToggleButton(String(params[i].name), params[i].isEnabled); break; case INT_SET: - paramsControl[i] = dynamic_cast(getEditor())->addComboBox(String(params[i].name), params[i].nEntries, params[i].entries); + paramsControl[i] = ed->addComboBox(String(params[i].name), params[i].nEntries, params[i].entries); break; case FLOAT_RANGE: - paramsControl[i] = dynamic_cast(getEditor())->addSlider(String(params[i].name), params[i].rangeMin, params[i].rangeMax, params[i].startValue); + paramsControl[i] = ed->addSlider(String(params[i].name), params[i].rangeMin, params[i].rangeMax, params[i].startValue); break; default: break; } } - GUIThreadState = PyEval_SaveThread(); } @@ -582,86 +539,179 @@ String PythonPlugin::getFile() void PythonPlugin::updateSettings() { + // update the sample rate... + // if you have input data channels, we'll use the first one's sample rate (sane?) + // otherwise, we'll just use the superclass' implementation if getSampleRate() + if (getNumInputs() > 0) { + dataSampleRate = getDataChannel(0)->getSampleRate(); + } else { + dataSampleRate = GenericProcessor::getSampleRate(); + } + + // update number of channels + int prevChans = nChans; + nChans = getNumInputs(); + + chanEnabled.resize(nChans); + + for (int c = prevChans; c < nChans; ++c) + { + // new channels are enabled by default + chanEnabled.set(c, true); + } + + if (updateSettingsFunction) + { + const PythonLock pyLock; + (*updateSettingsFunction)(nChans, dataSampleRate); + } +} + +void PythonPlugin::channelChanged(int chan, bool state) +{ + jassert(chan >= 0 && chan < chanEnabled.size()); + chanEnabled.set(chan, state); + if (channelChangedFunction) + { + const PythonLock pyLock; + (*channelChangedFunction)(chan, state); + } } void PythonPlugin::setIntPythonParameter(String name, int value) { + LOG_ENTER("setIntPythonParameter"); -#ifdef PYTHON_DEBUG -#if defined(__linux__) - pid_t tid; - tid = syscall(SYS_gettid); -#else - uint64_t tid; - pthread_threadid_np(NULL, &tid); -#endif - std::cout << "in setintparam pthread_threadid_np()=" << tid << std::endl; -#endif - - PyEval_RestoreThread(GUIThreadState); + const PythonLock pyLock; (*setIntParamFunction)(name.getCharPointer().getAddress(), value); - GUIThreadState = PyEval_SaveThread(); } void PythonPlugin::setFloatPythonParameter(String name, float value) { + LOG_ENTER("setFloatPythonParameter"); -#ifdef PYTHON_DEBUG -#if defined(__linux__) - pid_t tid; - tid = syscall(SYS_gettid); -#else - uint64_t tid; - pthread_threadid_np(NULL, &tid); -#endif - std::cout << "in setfloatparam pthread_threadid_np()=" << tid << std::endl; -#endif - PyEval_RestoreThread(GUIThreadState); + const PythonLock pyLock; (*setFloatParamFunction)(name.getCharPointer().getAddress(), value); - GUIThreadState = PyEval_SaveThread(); } int PythonPlugin::getIntPythonParameter(String name) { - -#ifdef PYTHON_DEBUG -#if defined(__linux__) - pid_t tid; - tid = syscall(SYS_gettid); -#else - uint64_t tid; - pthread_threadid_np(NULL, &tid); -#endif - std::cout << "in getintparam pthread_threadid_np()=" << tid << std::endl; -#endif + LOG_ENTER("getIntPythonParameter"); int value; - PyEval_RestoreThread(GUIThreadState); + const PythonLock pyLock; value = (*getIntParamFunction)(name.getCharPointer().getAddress()); - GUIThreadState = PyEval_SaveThread(); return value; } float PythonPlugin::getFloatPythonParameter(String name) { + LOG_ENTER("getFloatPythonParameter"); - + float value; + const PythonLock pyLock; + value = (*getFloatParamFunction)(name.getCharPointer().getAddress()); + return value; +} + +void PythonPlugin::resetPlugin() +{ + pluginFunction = nullptr; + pluginIsReady = nullptr; + pluginStartupFunction = nullptr; + getParamNumFunction = nullptr; + getParamConfigFunction = nullptr; + updateSettingsFunction = nullptr; + channelChangedFunction = nullptr; + setIntParamFunction = nullptr; + setFloatParamFunction = nullptr; + getIntParamFunction = nullptr; + getFloatParamFunction = nullptr; + eventFunction = nullptr; + spikeFunction = nullptr; + + plugin.close(); +} + + +// PythonLock + +PythonPlugin::PythonLock::PythonLock() + : pgss(PyGILState_Ensure()) +{ + // if current state is not the mainState or saved threadState, need to save it + PyThreadState* currState = PyThreadState_Get(); + if (currState != mainState && currState != threadState) + { + // abusing the API a little - call ...Ensure again to increment the counter + // and prevent it from being deleted automatically when the lock is released + PyGILState_Ensure(); + + // delete the old thread state, if any + if (threadState) + { + PyThreadState_Clear(threadState); + PyThreadState_Delete(threadState); + } + + threadState = currState; + } +} + +PythonPlugin::PythonLock::~PythonLock() +{ + PyGILState_Release(pgss); +} + +static PyThreadState* startInterpreter() +{ + // if on windows, PYTHON_HOME_NAME is set by PythonEnv.props (corresponds to CONDA_HOME environment variable) +#ifndef _WIN32 +#define QUOTE(name) #name +#define STR(macro) QUOTE(macro) +#define PYTHON_HOME_NAME STR(PYTHON_HOME) +#endif + + char * old_python_home = getenv("PYTHONHOME"); + if (old_python_home == NULL || strcmp(old_python_home, PYTHON_HOME_NAME) != 0) + { #ifdef PYTHON_DEBUG -#if defined(__linux__) - pid_t tid; - tid = syscall(SYS_gettid); + std::cout << "setting PYTHONHOME" << std::endl; +#endif + +#ifdef _WIN32 + _putenv_s("PYTHONHOME", PYTHON_HOME_NAME); +#else + setenv("PYTHONHOME", PYTHON_HOME_NAME, 1); +#endif + } + +#ifdef PYTHON_DEBUG + std::cout << "PYTHONHOME: " << getenv("PYTHONHOME") << std::endl; +#endif + +#ifdef _WIN32 + // set PYTHONPATH to avoid error described here: https://stackoverflow.com/questions/5694706/py-initialize-fails-unable-to-load-the-file-system-codec + _putenv_s("PYTHONPATH", PYTHON_HOME_NAME "\\DLLs;" PYTHON_HOME_NAME "\\Lib;" PYTHON_HOME_NAME "\\Lib\\site-packages"); +#endif + +#if PY_MAJOR_VERSION==3 + Py_SetProgramName((wchar_t *)"PythonPlugin"); #else - uint64_t tid; - pthread_threadid_np(NULL, &tid); + Py_SetProgramName((char *)"PythonPlugin"); #endif - std::cout << "in getfloatparam pthread_threadid_np()=" << tid << std::endl; + Py_Initialize(); + PyEval_InitThreads(); + + PyRun_SimpleString("import sys"); + PyRun_SimpleString("sys.setcheckinterval(10000)"); +#ifdef PYTHON_DEBUG + std::cout << Py_GetPrefix() << std::endl; + std::cout << Py_GetVersion() << std::endl; #endif - - PyEval_RestoreThread(GUIThreadState); - float value; - value = (*getFloatParamFunction)(name.getCharPointer().getAddress()); - GUIThreadState = PyEval_SaveThread(); - return value; + return PyEval_SaveThread(); } +const PyThreadState* PythonPlugin::PythonLock::mainState(startInterpreter()); +PyThreadState* PythonPlugin::PythonLock::threadState(nullptr); diff --git a/PythonPlugin/PythonPlugin.h b/PythonPlugin/PythonPlugin.h index b72dba3..1ad847f 100644 --- a/PythonPlugin/PythonPlugin.h +++ b/PythonPlugin/PythonPlugin.h @@ -34,43 +34,45 @@ ============================================================================== */ + + #ifndef __PYTHONPLUGIN_H #define __PYTHONPLUGIN_H +//Hack to get around python37_d.lib not exisiting on Windows (at least on my system) +#if defined(_WIN32) && defined(_DEBUG) +#define _DEBUG_TEMP _DEBUG +#undef _DEBUG +#include +#define _DEBUG _DEBUG_TEMP +#undef _DEBUG_TEMP +#else #include - -#if PY_MAJOR_VERSION>=3 -#define DL_IMPORT PyAPI_FUNC -#endif - -#ifndef __PYX_EXTERN_C - #ifdef __cplusplus - #define __PYX_EXTERN_C extern "C" - #else - #define __PYX_EXTERN_C extern - #endif #endif - #include "PythonParamConfig.h" #include "PythonEvent.h" -//extern "C" typedef void (*initfunc_t)(void); +#include "PythonEditor.h" -#if PY_MAJOR_VERSION>=3 +//#if PY_MAJOR_VERSION>=3 typedef PyObject * (*initfunc_t)(void); -#else -typedef PyMODINIT_FUNC (*initfunc_t)(void); -#endif -typedef DL_IMPORT(void) (*startupfunc_t)(float); // passes the sampling rate -typedef DL_IMPORT(void) (*pluginfunc_t)(float *, int, int, int, PythonEvent *); -typedef DL_IMPORT(int) (*isreadyfunc_t)(void); -typedef DL_IMPORT(int) (*getparamnumfunc_t)(void); -typedef DL_IMPORT(void) (*getparamconfigfunc_t)(struct ParamConfig*); -typedef DL_IMPORT(void) (*setintparamfunc_t)(char*, int); -typedef DL_IMPORT(void) (*setfloatparamfunc_t)(char*, float); -typedef DL_IMPORT(int) (*getintparamfunc_t)(char*); -typedef DL_IMPORT(float) (*getfloatparamfunc_t)(char*); +//#else +//typedef PyMODINIT_FUNC (*initfunc_t)(void); +//#endif +typedef void (*startupfunc_t)(int, float, int*); // passes the sampling rate and channel states +typedef void (*eventfunc_t)(int, int, int, double, int);// CJB added +typedef void (*spikefunc_t)(int, int, float[18]);// CJB added +typedef void (*pluginfunc_t)(float *, int, int, int, PythonEvent *); +typedef int (*isreadyfunc_t)(void); +typedef int (*getparamnumfunc_t)(void); +typedef void (*getparamconfigfunc_t)(struct ParamConfig*); +typedef void (*updatefunc_t)(int, float); +typedef void (*chanchangefunc_t)(int, int); +typedef void (*setintparamfunc_t)(char*, int); +typedef void (*setfloatparamfunc_t)(char*, float); +typedef int (*getintparamfunc_t)(char*); +typedef float (*getfloatparamfunc_t)(char*); #ifdef _WIN32 @@ -84,15 +86,12 @@ typedef DL_IMPORT(float) (*getfloatparamfunc_t)(char*); //============================================================================= /* */ -class PythonPlugin : public GenericProcessor +class PythonPlugin : public GenericProcessor { public: /** The class constructor, used to initialize any members. */ PythonPlugin(const String &processorName = "Python Plugin"); - /** The class destructor, used to deallocate memory */ - ~PythonPlugin(); - /** Determines whether the processor is treated as a source. */ virtual bool isSource() { @@ -119,10 +118,8 @@ class PythonPlugin : public GenericProcessor */ virtual void process(AudioSampleBuffer& buffer /* , MidiBuffer& events */); - /** Any variables used by the "process" function _must_ be modified only through - this method while data acquisition is active. If they are modified in any - other way, the application will crash. */ - void setParameter(int parameterIndex, float newValue); + void handleEvent (const EventChannel* eventInfo, const MidiMessage& event, int sampleNum); // CJB added + void handleSpike(const SpikeChannel* channelInfo, const MidiMessage& event, int samplePosition); //CJB added AudioProcessorEditor* createEditor(); @@ -132,6 +129,7 @@ class PythonPlugin : public GenericProcessor } void updateSettings(); + void channelChanged(int chan, bool state); void createEventChannels(); void setFile(String fullpath); String getFile(); @@ -156,38 +154,68 @@ class PythonPlugin : public GenericProcessor int getIntPythonParameter(String name); float getFloatPythonParameter(String name); - - void resetConnections(); + private: + void sendEventPlugin(int eventType, int sourceID, int subProcessorIdx, double timestamp, int sourceIndex); //CJB added + + // close plugin library and reset all functions to null + void resetPlugin(); + + /* Added by EBB + Why do it this way: + * Using a class allows object destruction to control releasing the GIL (RAII) + * Private inner class so that random other objects with other threads can't use it; + it's just for the main GUI and process threads + * Static state pointers b/c all instances of PythonPlugin use the same threads and therefore + can use the same Python states + * State pointers encapuslated in here so that they can only be manipulated by creating + and destroying PythonLocks (abstracting away confusing Python C API) + */ + class PythonLock + { + public: + PythonLock(); + ~PythonLock(); + + private: + const PyGILState_STATE pgss; + + static const PyThreadState* mainState; + static PyThreadState* threadState; + + JUCE_DECLARE_NON_COPYABLE(PythonLock); + }; String filePath; - void *plugin; - // private members and methods go here - // - // e.g.: - // - // float threshold; - // bool state; + DynamicLibrary plugin; int numPythonParams = 0; ParamConfig *params; Component **paramsControl; + + // data to keep track of and send to plugin + int nChans = 0; + float dataSampleRate = 44100; + Array chanEnabled; + // function pointers to the python plugin - pluginfunc_t pluginFunction; - isreadyfunc_t pluginIsReady; - startupfunc_t pluginStartupFunction; - getparamnumfunc_t getParamNumFunction; - getparamconfigfunc_t getParamConfigFunction; - setintparamfunc_t setIntParamFunction; - setfloatparamfunc_t setFloatParamFunction; - getintparamfunc_t getIntParamFunction; - getfloatparamfunc_t getFloatParamFunction; - PyThreadState *GUIThreadState = 0; - PyThreadState *processThreadState = 0; - const EventChannel* ttlChannel{ nullptr }; - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PythonPlugin); + pluginfunc_t pluginFunction = nullptr; + isreadyfunc_t pluginIsReady = nullptr; + startupfunc_t pluginStartupFunction = nullptr; + getparamnumfunc_t getParamNumFunction = nullptr; + getparamconfigfunc_t getParamConfigFunction = nullptr; + updatefunc_t updateSettingsFunction = nullptr; + chanchangefunc_t channelChangedFunction = nullptr; + setintparamfunc_t setIntParamFunction = nullptr; + setfloatparamfunc_t setFloatParamFunction = nullptr; + getintparamfunc_t getIntParamFunction = nullptr; + getfloatparamfunc_t getFloatParamFunction = nullptr; + eventfunc_t eventFunction = nullptr; + spikefunc_t spikeFunction = nullptr; + const EventChannel* ttlChannel = nullptr; bool wasTriggered = 0; uint16 lastChan = 0; + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PythonPlugin); }; diff --git a/PythonPlugin/PythonSource.cpp b/PythonPlugin/PythonSource.cpp index 2e943fa..8483948 100644 --- a/PythonPlugin/PythonSource.cpp +++ b/PythonPlugin/PythonSource.cpp @@ -1,28 +1,28 @@ /* ------------------------------------------------------------------ - + Python Plugin Copyright (C) 2016 FP Battaglia - + based on Open Ephys GUI Copyright (C) 2013, 2015 Open Ephys - + ------------------------------------------------------------------ - + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - + You should have received a copy of the GNU General Public License along with this program. If not, see . - + */ /* ============================================================================== @@ -40,7 +40,17 @@ PythonSource::PythonSource() : PythonPlugin("Python Source") //, threshold(200.0), state(true) { + nChans = 16; +} + +int getDefaultNumDataOutputs(DataChannel::DataChannelTypes type, int subProcessorIdx = 0) +{ + return nChans; +} +int getNumOutputs(int subProcessorIdx) +{ + return nChans; } PythonSource::~PythonSource() diff --git a/PythonPlugin/PythonSource.h b/PythonPlugin/PythonSource.h index cada6ab..5521bc7 100644 --- a/PythonPlugin/PythonSource.h +++ b/PythonPlugin/PythonSource.h @@ -1,28 +1,28 @@ /* ------------------------------------------------------------------ - + Python Plugin Copyright (C) 2016 FP Battaglia - + based on Open Ephys GUI Copyright (C) 2013, 2015 Open Ephys - + ------------------------------------------------------------------ -v +v This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - + You should have received a copy of the GNU General Public License along with this program. If not, see . - + */ /* ============================================================================== @@ -34,6 +34,11 @@ v ============================================================================== */ +//virtual int getNumOutputs() const; +//GenericProcessor.h: virtual int getNumOutputs(int subProcessorIdx) const; +//GenericProcessor.h: int numOutputs; +//GenericProcessor.h: virtual int getDefaultNumDataOutputs(DataChannel::DataChannelTypes type, int subProcessorIdx = 0) const; + #ifndef __PYTHONSOURCE_H #define __PYTHONSOURCE_H @@ -60,7 +65,9 @@ class PythonSource : public PythonPlugin return false; } + virtual int getDefaultNumDataOutputs(DataChannel::DataChannelTypes type, int subProcessorIdx = 0) const; + virtual int getNumOutputs(int subProcessorIdx) const; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PythonSource); diff --git a/README.md b/README.md index 4e742fc..6d6671b 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,19 @@ -# PythonPlugin +# PythonPlugin -A plugin for open-ephys enabling the insertion of Cython (Python code translated to C and compiled) into the open-ephys signal chain. -Most of the Cython peculiarities are dealt with by a wrapper code, so that essentially usual, "pure" Python code may be used. -A tutorial on how to write a python module will follow soon, however, the code in the examples under the `python-modules` directory may serve as good guidance for now. +A plugin for open-ephys enabling the insertion of Cython (Python code translated to C and compiled) into the open-ephys signal chain. +Most of the Cython peculiarities are dealt with by a wrapper code, so that essentially usual, "pure" Python code may be used. +A tutorial on how to write a python module can be found below. Example modules can be found under the `python-modules` directory. ## Installation Instruction -At the moment, the plugin is compatible with the Linux and MacOSX versions of Open Ephys. I don't have Windows development expertise, so I will not port it to Windows myseld. If you are interested in porting it to Windows, this is probably a fairly simple task for an experienced developer (most if not all of the work will be replacing the dlopen/dlsym UNIX-style DLL import with the Windows equivalent). Please do contact me, and I will support the port as far as I can. - ### Compile from source code -The Plugin is organized so that it can be compiled as much as possible outside of the main open-ephys source tree. Under Linux, a symlink to the Source/Plugins directory is however necessary. -A recent Python version is required -The Plugin needs to link to a recent enough version of Python. Development work was done with a recent [Anaconda Python](https://www.continuum.io/why-anaconda) distribution, supporting python 3.5. -By default, we expect Anaconda to be installed in `/usr/local/anaconda` , however this may be changed easily as explained below. +The Plugin is organized so that it can be compiled as much as possible outside of the main open-ephys source tree. Under Linux, a symlink to the Source/Plugins directory is however necessary. +A recent Python version is required. +The Plugin needs to link to a recent enough version of Python. Development work was done with a recent [Anaconda Python](https://www.continuum.io/why-anaconda) distribution, supporting python 3.5 to 3.7. Windows users must use python 3.6 (see below). -To compile, extract in a folder just outside the Open Ephys plugin-GUI source tree -e.g. +To compile the plugin, extract in a folder just outside the Open Ephys plugin-GUI source tree +e.g. ``` $ ls src @@ -27,30 +24,135 @@ etc... The rest of the procedure is system dependent -####Linux -- With Anaconda: edit `build-linux.sh` and change `CONDA_HOME` to the Anaconda installation directory (default `/usr/local/anaconda`), if needed. -- With a different Python distribution: Edit `Builds/Linux/Makefile` and change the include and lib directories as needed. -- `cd PythonPlugin` -- run `./build-linux.sh`. The Plugin should be copied to the neighboring plugin-GUI source tree. +#### Linux + +The script `build-linux.sh` should detect the version and location of the python installation automatically. It will use the one of the executable that is at the top of the PATH, so make sure that the shell you are running it from is properly configured + +Under Ubuntu 16.04 and later: +- with default python install. This comes by default without a proper `distutils` package, and without the Python development environment, to install those run +``` +sudo apt install python3-distutils +sudo apt install python3-dev +``` + +To compile the python modules you will need (at least) Cython and numpy which may be installed by +``` +pip install cython +pip install numpy +``` + + +- With Anaconda: everything should be detected automatically, so no further action is needed at compilation time. +To compile the python modules you will need Cython which may be installed by +``` +conda install cython numpy +``` +or even better make your virtual environment with all the packages that are needed by your module. + +- run `./build-linux.sh`. The Plugin should be compiled and copied to the neighboring plugin-GUI source tree. + +#### MacOSX +- With Anaconda: a detection script runs at compilation. Because the compilation environment gets evaluated by XCode before any of the build phases are run, you may need to build the project *twice*, the second time should succeed. If any XCode guru has a solution for that, that would be welcome. + +#### Windows +- Install Anaconda, python3.6 ONLY (see compilation section), then add the required modules: +``` +conda install cython numpy +``` + +- Copy the folder `PythonPlugin` to your plugin-GUI source tree under `Source\Plugins`. + +- Copy the contents of `WindowsPlugin` to a new folder in your plugin-GUI source tree called `Builds\VisualStudio2013\Plugins\PythonPlugin`. + +- Open the Plugins solution in Visual Studio and add the Python plugin by right-clicking the top-level solution in the Solution Explorer, selecting `Add > Existing Project`, and opening the `Python.vcxproj` project file that you just copied into the `PythonPlugin` build folder. + +- If you created a virtual environment for Open Ephys (as suggested below) or installed Anaconda in somewhere other than the default (your home directory), you need to tell the plugin where your Python root is by setting the `CONDA_HOME` environment variable. Follow these steps: + +1. From the Start menu, start typing "environment" and then select "Edit environment variables for your account" +2. Click "New..." to create a new variable. Enter `CONDA_HOME` as the name and the path to your Python 3.6 root folder as the value. For example, if you are using a conda environment called `oeEnv`, this would be something like `C:\Users\your_username\Anaconda3\envs\oeEnv`. Do _not_ use a trailing slash. Click OK twice to save. +3. Restart Visual Studio completely if you have it open. Open the Plugins solution and select Project > Rescan Solution to make sure the `PYTHON_HOME_NAME` macro gets updated. -####MacOSX -- With Anaconda: edit `Builds/MacOS/Config/Plugin.xcconfig` and set `PYTHON_DIR` to the Anaconda installation directory -- With a different Python distribution: in the same file, edit `HEADER_SEARCH_PATHS` and `LIBRARY_SEARCH_PATHS` to the proper places. -- Open `Builds/MacOS/PythonPlugin.xcodeproj` in XCode and compile +## Usage +### Create New Module Directory and Framework Code +- Navigate to python_modules directy from the command line. +``` +cd PythonPlugin/python_modules +``` +- Run module creation code, where "YourPluginName" is the name you choose for the plugin. +``` +python generatePlugin.py YourPluginName +``` +### Modifying Template Code +- Place data to be held in RAM within the __init__(self) function. For example, if you want to hold an "electrodes" variable accessible by other functions in the class, initialize it as follows: +``` +def __init__(self): + """initialize object data""" + self.Enabled = 1 + self.electrodes = 16 +``` + +- The startup(self, sr) function is called after selecting the .so file. This allows for the sampling rate (sr) to be passed off to the python plugin. This is also a useful place to connect to an arduino (after importing the serial library), shown as follows: +``` +def startup(self, sr): + self.samplingRate = sr + print (self.samplingRate) + self.arduino = serial.Serial('/dev/tty.usbmodem45561', 57600) + print ("Arduino: ", self.arduino) +``` + +- The params_config(self) functions allows for the python plugin to have various buttons and sliders. This allows for the module to be more dynamic. The following code shows how to create a toggle button: +``` +def param_config(self): + """return button, sliders, etc to be present in the editor OE side""" + return [("toggle", "Enabled", True)] +``` -### Binary installation -A binary installation (Linux only for the time being) is provided [here](https://github.com/fpbattaglia/PythonPlugin-linux-binaries). This assumes that you have a suitable Anaconda Python distribution (see above) and that that resides in /usr/local/anaconda. If that is not the case, a symlink should be enough -```bash -sudo ln -s /my/install/anaconda /usr/local/anaconda +- The bufferfunction(self,n_arr) function is where the voltage data comes in and events go out to the rest of the OE signal chain. The variable "n_arr" is a matrix of the voltage data, accessed as `n_arr[electode][sample]`. The function must return events, even if the events are empty. The following code shows how to send out an event if the minimum value in the buffer on electrode 12 is below a predefined threshold. +``` +def bufferfunction(self, n_arr): + events = [] + min = np.min(n_arr[12][:]) + if min < self.thresh: + events.append({'type': 3, 'sampleNum': 10, 'eventId': 1}) + return events ``` -- Use the Binary-distributed version of Open-Ephys or compile it from source with the Release configuration. -- Copy PythonPlugin.so to the `plugins` directory, and you are done. +- The handleEvents(eventType,sourceID,subProcessorIdx,timestamp,sourceIndex) function passes on events (but not spike events) generated elsewhere in the OE signal chain to the python plugin. The following code shows how to save the timestamps of these events. +``` +def handleEvents(eventType,sourceID,subProcessorIdx,timestamp,sourceIndex): + """handle events passed from OE""" + self.eventBuffer.append(timestamp) +```` +- The handleSpike(self,electrode,sortedID,n_arr) function passes on spike events generated elsewhere in the OE signal chain to the python plugin. the n_arr is an 18 element long spike waveform. +### Compilation +Currently, only Cython version 0.28.2 is supported. Recently downloaded or upgraded versions of Anaconda will come with version 0.29.2, which will cause the application to crash upon loading a python module. To avoid this, create a virtual enviroment with the correct versions of python and cython by running: - +``` +conda create -n oeEnv python=3.6 cython=0.28.2 +``` +To activate the enviroment on Linux or Mac: +``` +source activate oeEnv +``` +To activate the enviroment on Windows: +``` +activate oeEnv +``` +For more information on virtual enviroments, please click [here](https://conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html). +- To compile the python module with cython, run setup.py in the module's directory (i.e. "YourPluginName/"). +``` +python setup.py build_ext --inplace +``` +- If you get a warning about the NumPy version, you can safely ignore it. +### Load Python Module in Open Ephys +- Drag Python Filter into signal chain +- Click on the select file button +- Navigate to the module's directory in the file selector +- Double click on the .so file (or select the file and click "open") +![alt text](https://github.com/MemDynLab/PythonPlugin/blob/event_reciever/images/demonstration.gif) diff --git a/WindowsPlugin/Python.vcxproj b/WindowsPlugin/Python.vcxproj new file mode 100644 index 0000000..ec0729c --- /dev/null +++ b/WindowsPlugin/Python.vcxproj @@ -0,0 +1,172 @@ + + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + + {03F9F201-3F09-48C1-B4EC-73C5904632F6} + Win32Proj + Python + PythonPlugin + + + + DynamicLibrary + true + v120 + Unicode + + + DynamicLibrary + true + v120 + Unicode + + + DynamicLibrary + false + v120 + true + Unicode + + + DynamicLibrary + false + v120 + true + Unicode + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + true + + + false + + + false + + + + + + Level3 + Disabled + WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + + + Console + true + + + + + + + Level3 + Disabled + WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + + + Console + true + + + + + Level3 + + + MaxSpeed + true + true + WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + + + Console + true + true + true + + + + + Level3 + + + MaxSpeed + true + true + WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + %(AdditionalIncludeDirectories) + + + Console + true + true + true + %(AdditionalDependencies) + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/WindowsPlugin/Python.vcxproj.filters b/WindowsPlugin/Python.vcxproj.filters new file mode 100644 index 0000000..bfcf981 --- /dev/null +++ b/WindowsPlugin/Python.vcxproj.filters @@ -0,0 +1,60 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + \ No newline at end of file diff --git a/WindowsPlugin/PythonEnv.props b/WindowsPlugin/PythonEnv.props new file mode 100644 index 0000000..2961d01 --- /dev/null +++ b/WindowsPlugin/PythonEnv.props @@ -0,0 +1,23 @@ + + + + + $(USERPROFILE)\Anaconda3 + $(CONDA_HOME) + + + + + $(PYTHON_HOME)\include;%(AdditionalIncludeDirectories) + PYTHON_HOME_NAME=R"?($(PYTHON_HOME))?";%(PreprocessorDefinitions) + + + $(PYTHON_HOME)\libs;%(AdditionalLibraryDirectories) + python36.lib;%(AdditionalDependencies) + + + copy /Y "$(PYTHON_HOME)\python36.dll" "$(GUIDir)" +%(Command) + + + \ No newline at end of file diff --git a/build-linux.sh b/build-linux.sh index 0ef7e55..f5da5ff 100755 --- a/build-linux.sh +++ b/build-linux.sh @@ -1,6 +1,23 @@ #!/bin/bash -export CONDA_HOME=/usr/local/anaconda -export CONFIG=Release + +if hash python 2>/dev/null; then + PYTHON_COMMAND=python +else + PYTHON_COMMAND=python3 +fi + +export CONDA_HOME=$(${PYTHON_COMMAND} -c "from distutils import sysconfig; \ +import re; \ +z = sysconfig.get_config_var('prefix'); \ +print(z)") + +export PYTHON_VERSION=$(${PYTHON_COMMAND} -c "from distutils import sysconfig; \ +import re; \ +z = sysconfig.get_config_var('BLDLIBRARY'); \ +m = re.search(r'-l(.*)', z); \ +print(m.group(1))") + +export CONFIG=Debug ln -s ../../../PythonPlugin/PythonPlugin/ ../plugin-GUI/Source/Plugins/PythonPlugin cd ../plugin-GUI/Builds/Linux/ make -f Makefile.plugins diff --git a/examplePic1.png b/examplePic1.png new file mode 100644 index 0000000..a778119 Binary files /dev/null and b/examplePic1.png differ diff --git a/images/demonstration.gif b/images/demonstration.gif new file mode 100644 index 0000000..2c380c0 Binary files /dev/null and b/images/demonstration.gif differ diff --git a/images/examplePic1.png b/images/examplePic1.png new file mode 100644 index 0000000..a778119 Binary files /dev/null and b/images/examplePic1.png differ diff --git a/images/examplePic2.png b/images/examplePic2.png new file mode 100644 index 0000000..941907a Binary files /dev/null and b/images/examplePic2.png differ diff --git a/images/holder.txt b/images/holder.txt new file mode 100644 index 0000000..8bd6648 --- /dev/null +++ b/images/holder.txt @@ -0,0 +1 @@ +asdf diff --git a/python_modules/.config.py b/python_modules/.config.py new file mode 100644 index 0000000..2c7c68a --- /dev/null +++ b/python_modules/.config.py @@ -0,0 +1,2 @@ +# Location of Python Plugin source (absolute or relative to the location of setup.py) +PYTHON_PLUGIN_SRC_DIR = "../../PythonPlugin" diff --git a/python_modules/generatePlugin.py b/python_modules/generatePlugin.py new file mode 100644 index 0000000..aa081b0 --- /dev/null +++ b/python_modules/generatePlugin.py @@ -0,0 +1,16 @@ +import sys +import os +name = str(sys.argv[1]) +os.mkdir(name) + +with open(os.path.join(name, 'setup.py'),'w+') as w: + with open('template/setup.py','r') as f: + for line in f: + toWrite = line.replace("EXAMPLE", name) + w.write(toWrite) + +with open(os.path.join(name, name +'.pyx'),'w+') as w: + with open('template/template.pyx','r') as f: + for line in f: + toWrite = line.replace("EXAMPLE", name) + w.write(toWrite) diff --git a/python_modules/pulse_test_delay/.gitignore b/python_modules/old_style_plugins/pulse_test_delay/.gitignore similarity index 100% rename from python_modules/pulse_test_delay/.gitignore rename to python_modules/old_style_plugins/pulse_test_delay/.gitignore diff --git a/python_modules/pulse_test_delay/__init__.py b/python_modules/old_style_plugins/pulse_test_delay/__init__.py similarity index 100% rename from python_modules/pulse_test_delay/__init__.py rename to python_modules/old_style_plugins/pulse_test_delay/__init__.py diff --git a/python_modules/pulse_test_delay/delaytest.xml b/python_modules/old_style_plugins/pulse_test_delay/delaytest.xml similarity index 100% rename from python_modules/pulse_test_delay/delaytest.xml rename to python_modules/old_style_plugins/pulse_test_delay/delaytest.xml diff --git a/python_modules/old_style_plugins/pulse_test_delay/pulse_test_delay.pyx b/python_modules/old_style_plugins/pulse_test_delay/pulse_test_delay.pyx new file mode 100644 index 0000000..49cc08f --- /dev/null +++ b/python_modules/old_style_plugins/pulse_test_delay/pulse_test_delay.pyx @@ -0,0 +1,77 @@ +import sys +import numpy as np +cimport numpy as np +from cython cimport view +import serial + + +isDebug = False + + +class Pulse_test_delay(object): + def __init__(self): + self.enabled = 1 + self.chan_in = 0 + self.thresh_min = -2 + self.thresh_max = 2 + self.thresh_start = 0 + self.threshold = self.thresh_start + self.arduino = None + + + self.triggered = 0 + + + def startup(self, sr): + self.samplingRate = sr + print (self.samplingRate) + self.arduino = serial.Serial('/dev/tty.usbmodem45561', 57600) + print ("Arduino: ", self.arduino) + self.enabled = 1 + + def plugin_name(self): + return "pulse_test_delay" + + def is_ready(self): + return 1 + + def param_config(self): + chan_labels = list(range(1,44)) + + return (("toggle", "Enabled", True), + ("int_set", "chan_in", chan_labels), + ("float_range", "threshold", self.thresh_min, self.thresh_max, self.thresh_start)) + + + def bufferfunction(self, n_arr): + #print ("plugin start") + events = [] + cdef int chan_in + cdef int chan_out + chan_in = self.chan_in + cdef int n_samples = n_arr.shape[1] + + if np.any(n_arr[chan_in-1,:] > self.threshold): + if not self.triggered: + #print ('triggered') + events.append({'type': 3, 'sampleNum': 10, 'eventId': 1}) + self.triggered = 1 + self.arduino.write(b'1') + #n_arr[chan_in-2,:] = 1 * np.ones((1,n_samples)) + elif self.triggered: + self.triggered = 0 + events.append({'type': 3, 'sampleNum': 10, 'eventId': 5}) + #n_arr[chan_in-2,:] = np.zeros((1,n_samples)) + else: + pass + # n_arr[chan_in-2,:] = np.zeros((1,n_samples)) + + + + #print( "plugin end") + return events + +pluginOp = Pulse_test_delay() + +include "../plugin.pyx" + diff --git a/python_modules/spwdouble/.gitignore b/python_modules/old_style_plugins/spwdouble/.gitignore similarity index 100% rename from python_modules/spwdouble/.gitignore rename to python_modules/old_style_plugins/spwdouble/.gitignore diff --git a/python_modules/spwdouble/__init__.py b/python_modules/old_style_plugins/spwdouble/__init__.py similarity index 100% rename from python_modules/spwdouble/__init__.py rename to python_modules/old_style_plugins/spwdouble/__init__.py diff --git a/python_modules/spwdouble/get_test_data.py b/python_modules/old_style_plugins/spwdouble/get_test_data.py similarity index 100% rename from python_modules/spwdouble/get_test_data.py rename to python_modules/old_style_plugins/spwdouble/get_test_data.py diff --git a/python_modules/old_style_plugins/spwdouble/spwdouble.pyx b/python_modules/old_style_plugins/spwdouble/spwdouble.pyx new file mode 100644 index 0000000..fe7fd8a --- /dev/null +++ b/python_modules/old_style_plugins/spwdouble/spwdouble.pyx @@ -0,0 +1,258 @@ +import numpy as np +cimport numpy as np +from cython cimport view +import serial +import scipy.signal +import logging + +isDebug = False + +class SPWFinder(object): + def __init__(self): + self.enabled = True + + self.jitter_count_down_thresh = 0 + self.jitter_count_down = 0 + self.jitter_time = 200. # in ms + self.refractory_count_down_thresh = 0 + self.refractory_count_down = 0 + self.refractory_time = 100. # time that the plugin will not react to trigger after one pulse + self.double_count_down_thresh = 0 + self.double_count_down = 0 + self.double_time = 200. + self.double_rate = 1. / 3. + + self.averaging_time_min = 10. + self.averaging_time_max = 50. + self.averaging_time_start = 20. + self.averaging_time = self.averaging_time_start # in ms + self.chan_in = 1 + self.chan_out = 0 + self.n_samples = 0 + self.chan_ripples = 1 + self.band_lo_min = 50. + self.band_lo_max = 200. + self.band_lo_start = 100. + self.band_lo = self.band_lo_start + + self.band_hi_min = 100. + self.band_hi_max = 500. + self.band_hi_start = 300. + self.band_hi = self.band_hi_start + + self.thresh_min = 5. + self.thresh_max = 200. + self.thresh_start = 30. + self.threshold = self.thresh_start + + + self.swing_thresh_min = 10. + self.swing_thresh_max = 20000. + self.swing_thresh_start = 1000. + self.swing_thresh = self.swing_thresh_start + + self.SWINGING = 1 + self.NOT_SWINGING = 0 + self.swing_state = self.NOT_SWINGING + self.swing_count_down_thresh = 0 + self.swing_count_down = 0 + self.swing_down_time = 2000. # time that it will be prevetned from firing after a swing event + + self.pulseNo = 0 + self.triggered = 0 + self.samplingRate = 0. + self.polarity = 0 + self.filter_a = [] + self.filter_b = [] + self.arduino = None + self.lfp_buffer_max_count = 1000 + self.lfp_buffer = np.zeros((self.lfp_buffer_max_count,)) + self.READY=1 + self.ARMED=2 + self.REFRACTORY=3 + self.FIRING = 4 + self.TRIGGERED2 = 5 + self.FIRING2 = 6 + self.state = self.READY + logging.basicConfig(filename='spwdouble.log', format='%(asctime)s %(message)s', level=logging.DEBUG) + print ("finished SPWfinder constructor") + + def startup(self, sampling_rate): + self.samplingRate = sampling_rate + print (self.samplingRate) + + self.filter_b, self.filter_a = scipy.signal.butter(3, + (self.band_lo/(self.samplingRate/2), self.band_hi/(self.samplingRate/2)), + 'pass') + print(self.filter_a) + print(self.filter_b) + print(self.band_lo) + print(self.band_hi) + print(self.band_lo/(self.samplingRate/2)) + print(self.band_hi/(self.samplingRate/2)) + self.enabled = 1 + try: + self.arduino = serial.Serial('/dev/ttyACM0', 57600) + except (OSError, serial.serialutil.SerialException): + print("Can't open Arduino") + + def plugin_name(self): + return "SPWFinder" + + def is_ready(self): + return 1 + + def param_config(self): + chan_labels = range(1,33) + return (("toggle", "enabled", True), + ("int_set", "chan_in", chan_labels), + ("float_range", "threshold", self.thresh_min, self.thresh_max, self.thresh_start), + ("float_range", "swing_thresh", self.swing_thresh_min, self.swing_thresh_max, self.swing_thresh_start), + ("float_range", "averaging_time", self.averaging_time_min, self.averaging_time_max, self.averaging_time_start)) + + + def spw_condition(self, n_arr): + return (np.mean(n_arr[self.chan_out+1,:]) > self.threshold) and self.swing_state == self.NOT_SWINGING + + def stimulate(self): + try: + self.arduino.write(b'1') + except AttributeError: + print("Can't send pulse") + self.pulseNo += 1 + print("generating pulse ", self.pulseNo) + logging.debug('sending pulse') + + def new_event(self, events, code, channel=0, timestamp=None): + if not timestamp: + timestamp = self.n_samples + events.append({'type': 3, 'sampleNum': timestamp, 'eventId': code, 'eventChannel': channel}) + + def bufferfunction(self, n_arr): + #print("plugin start") + if isDebug: + print("shape: ", n_arr.shape) + events = [] + cdef int chan_in + cdef int chan_out + chan_in = self.chan_in - 1 + self.chan_out = self.chan_ripples + + self.n_samples = int(n_arr.shape[1]) + + if self.n_samples == 0: + return events + + # setting up count down thresholds in units of samples + self.refractory_count_down_thresh = self.refractory_time * self.samplingRate / 1000. + self.double_count_down_thresh = self.double_time * self.samplingRate / 1000. + self.swing_count_down_thresh = self.swing_down_time * self.samplingRate / 1000. + self.jitter_count_down_thresh = self.jitter_time * self.samplingRate / 1000. + self.samples_for_average = self.averaging_time * self.samplingRate / 1000. + + signal_to_filter = np.hstack((self.lfp_buffer, n_arr[chan_in,:])) + signal_to_filter = signal_to_filter - signal_to_filter[-1] + filtered_signal = scipy.signal.lfilter(self.filter_b, self.filter_a, signal_to_filter) + n_arr[self.chan_out,:] = filtered_signal[self.lfp_buffer.size:] + self.lfp_buffer = np.append(self.lfp_buffer, n_arr[chan_in,:]) + if self.lfp_buffer.size > self.lfp_buffer_max_count: + self.lfp_buffer = self.lfp_buffer[-self.lfp_buffer_max_count:] + n_arr[self.chan_out+1,:] = np.fabs(n_arr[self.chan_out,:]) + n_arr[self.chan_out+2,:] = 5. *np.mean(filtered_signal[-self.samples_for_average:]) * np.ones((1,self.n_samples)) + + + # the swing detector state machine + max_swing = np.max(np.fabs(n_arr[chan_in,:])) + if self.swing_state == self.NOT_SWINGING: + if max_swing > self.swing_thresh: + self.swing_state = self.SWINGING + self.swing_count_down = self.swing_count_down_thresh + self.new_event(events, 6) + logging.debug("SWINGING") + else: + self.swing_count_down -= self.n_samples + if self.swing_count_down <= 0: + self.swing_state = self.NOT_SWINGING + logging.debug("NOT_SWINGING") + + + if isDebug: + print("Mean: ", np.mean(n_arr[self.chan_out+1,:])) + print("done processing") + + #events + # 1: pulse sent + # 2: jittered, pulse_sent + # 3: triggered, not enabled + # 4: trigger armed, jittered + # 5: terminating pulse + # 6: swing detected + # machines: + # ENABLED vs. DISABLED vs. JITTERED + # states: + # READY, REFRACTORY, ARMED, FIRING + # now w/ logging + + + # finite state machine + if self.state == self.READY: + if self.spw_condition(n_arr): + if self.enabled: + logging.debug('got spw') + self.jitter_count_down = self.jitter_count_down_thresh + self.state = self.ARMED + logging.debug('ARMED') + self.new_event(events, 1, 1) + else: + self.new_event(events, 3) + elif self.state == self.ARMED: + logging.debug('in ARMED with countdown %d', self.jitter_count_down) + if self.jitter_count_down == self.jitter_count_down_thresh: + self.new_event(events, 5, 1) + self.jitter_count_down -= self.n_samples + if self.jitter_count_down <= 0: + self.stimulate() + self.new_event(events, 2) + self.state = self.FIRING + logging.debug('FIRING') + self.new_event(events, 1) + elif self.state == self.FIRING: + if np.random.random() < self.double_rate: + self.double_count_down = self.double_count_down_thresh-1 + print('double') + self.state = self.TRIGGERED2 + logging.debug('TRIGGERED2') + else: + self.refractory_count_down = self.refractory_count_down_thresh-1 + self.state = self.REFRACTORY + logging.debug('REFRACTORY') + self.new_event(events, 5) + elif self.state == self.TRIGGERED2: + self.double_count_down -= self.n_samples + if self.double_count_down <= 0: + self.stimulate() + self.new_event(events, 1) + self.state = self.FIRING2 + logging.debug('FIRING2') + elif self.state == self.FIRING2: + self.refractory_count_down = self.refractory_count_down_thresh-1 + self.state = self.REFRACTORY + logging.debug('REFRACTORY') + self.new_event(events, 5) + elif self.state == self.REFRACTORY: + self.refractory_count_down -= self.n_samples + if self.refractory_count_down <= 0: + self.state = self.READY + logging.debug('READY') + else: + # checking for a leftover ARMED state + self.state = self.READY + logging.debug('READY') + + + return events + + +pluginOp = SPWFinder() + +include "../plugin.pyx" \ No newline at end of file diff --git a/python_modules/spwdouble/test_params.py b/python_modules/old_style_plugins/spwdouble/test_params.py similarity index 100% rename from python_modules/spwdouble/test_params.py rename to python_modules/old_style_plugins/spwdouble/test_params.py diff --git a/python_modules/spwfinder/.gitignore b/python_modules/old_style_plugins/spwfinder/.gitignore similarity index 100% rename from python_modules/spwfinder/.gitignore rename to python_modules/old_style_plugins/spwfinder/.gitignore diff --git a/python_modules/spwfinder/__init__.py b/python_modules/old_style_plugins/spwfinder/__init__.py similarity index 100% rename from python_modules/spwfinder/__init__.py rename to python_modules/old_style_plugins/spwfinder/__init__.py diff --git a/python_modules/spwfinder/get_test_data.py b/python_modules/old_style_plugins/spwfinder/get_test_data.py similarity index 100% rename from python_modules/spwfinder/get_test_data.py rename to python_modules/old_style_plugins/spwfinder/get_test_data.py diff --git a/python_modules/old_style_plugins/spwfinder/spwfinder.pyx b/python_modules/old_style_plugins/spwfinder/spwfinder.pyx new file mode 100644 index 0000000..74e1a46 --- /dev/null +++ b/python_modules/old_style_plugins/spwfinder/spwfinder.pyx @@ -0,0 +1,242 @@ +import numpy as np +cimport numpy as np +from cython cimport view +import serial +import scipy.signal + + +isDebug = False + +class SPWFinder(object): + def __init__(self): + self.enabled = True + self.jitter = False + self.jitter_count_down_thresh = 0 + self.jitter_count_down = 0 + self.jitter_time = 200. # in ms + self.refractory_count_down_thresh = 0 + self.refractory_count_down = 0 + self.refractory_time = 100. # time that the plugin will not react to trigger after one pulse + self.chan_in = 1 + self.chan_out = 0 + self.n_samples = 0 + self.chan_ripples = 1 + self.band_lo_min = 50. + self.band_lo_max = 200. + self.band_lo_start = 100. + self.band_lo = self.band_lo_start + + self.band_hi_min = 100. + self.band_hi_max = 500. + self.band_hi_start = 300. + self.band_hi = self.band_hi_start + + self.thresh_min = 5. + self.thresh_max = 200. + self.thresh_start = 30. + self.threshold = self.thresh_start + + + self.averaging_time_min = 10. + self.averaging_time_max = 50. + self.averaging_time_start = 20. + self.averaging_time = self.averaging_time_start # in ms + self.samples_for_average = 0 + + self.swing_thresh_min = 10. + self.swing_thresh_max = 20000. + self.swing_thresh_start = 1000. + self.swing_thresh = self.swing_thresh_start + + self.SWINGING = 1 + self.NOT_SWINGING = 0 + self.swing_state = self.NOT_SWINGING + self.swing_count_down_thresh = 0 + self.swing_count_down = 0 + self.swing_down_time = 2000. # time that it will be prevetned from firing after a swing event + + self.pulseNo = 0 + self.triggered = 0 + self.samplingRate = 0. + self.polarity = 0 + self.filter_a = [] + self.filter_b = [] + self.arduino = None + self.lfp_buffer_max_count = 1000 + self.lfp_buffer = np.zeros((self.lfp_buffer_max_count,)) + self.spw_power = 0. + self.READY=1 + self.ARMED=2 + self.REFRACTORY=3 + self.FIRING = 4 + self.state = self.READY + + print ("finished SPWfinder constructor") + + def startup(self, sampling_rate): + self.samplingRate = sampling_rate + print (self.samplingRate) + + self.filter_b, self.filter_a = scipy.signal.butter(3, + (self.band_lo/(self.samplingRate/2), self.band_hi/(self.samplingRate/2)), + 'pass') + print(self.filter_a) + print(self.filter_b) + print(self.band_lo) + print(self.band_hi) + print(self.band_lo/(self.samplingRate/2)) + print(self.band_hi/(self.samplingRate/2)) + self.enabled = 1 + self.jitter = 0 + try: + self.arduino = serial.Serial('/dev/ttyACM0', 57600) + except (OSError, serial.serialutil.SerialException): + print("Can't open Arduino") + + def plugin_name(self): + return "SPWFinder" + + def is_ready(self): + return 1 + + def param_config(self): + chan_labels = range(1, 33) + return (("toggle", "enabled", True), + ("int_set", "chan_in", chan_labels), + ("float_range", "threshold", self.thresh_min, self.thresh_max, self.thresh_start), + ("float_range", "swing_thresh", self.swing_thresh_min, self.swing_thresh_max, self.swing_thresh_start), + ("float_range", "averaging_time", self.averaging_time_min, self.averaging_time_max, self.averaging_time_start)) + + def spw_condition(self, n_arr): + return (self.spw_power > self.threshold) and self.swing_state == self.NOT_SWINGING + + def stimulate(self): + try: + self.arduino.write(b'1') + except AttributeError: + print("Can't send pulse") + self.pulseNo += 1 + print("generating pulse ", self.pulseNo) + + def new_event(self, events, code, channel=0, timestamp=None): + if not timestamp: + timestamp = self.n_samples + events.append({'type': 3, 'sampleNum': timestamp, 'eventId': code, 'eventChannel': channel}) + + def bufferfunction(self, n_arr): + #print("plugin start") + if isDebug: + print("shape: ", n_arr.shape) + events = [] + cdef int chan_in + cdef int chan_out + chan_in = self.chan_in - 1 + self.chan_out = self.chan_ripples + + self.n_samples = int(n_arr.shape[1]) + + if self.n_samples == 0: + return events + + # setting up count down thresholds in units of samples + self.refractory_count_down_thresh = int(self.refractory_time * self.samplingRate / 1000.) + self.swing_count_down_thresh = int(self.swing_down_time * self.samplingRate / 1000.) + self.jitter_count_down_thresh = int(self.jitter_time * self.samplingRate / 1000.) + self.samples_for_average = int(self.averaging_time * self.samplingRate / 1000.) + + signal_to_filter = np.hstack((self.lfp_buffer, n_arr[chan_in,:])) + signal_to_filter = signal_to_filter - signal_to_filter[-1] + filtered_signal = scipy.signal.lfilter(self.filter_b, self.filter_a, signal_to_filter) + n_arr[self.chan_out,:] = filtered_signal[self.lfp_buffer.size:] + self.lfp_buffer = np.append(self.lfp_buffer, n_arr[chan_in,:]) + if self.lfp_buffer.size > self.lfp_buffer_max_count: + self.lfp_buffer = self.lfp_buffer[-self.lfp_buffer_max_count:] + n_arr[self.chan_out+1,:] = np.fabs(n_arr[self.chan_out,:]) + self.spw_power = np.mean(np.fabs(filtered_signal[-self.samples_for_average:])) + n_arr[self.chan_out+2,:] = 5. *self.spw_power * np.ones((1,self.n_samples)) + + + # the swing detector state machine + max_swing = np.max(np.fabs(n_arr[chan_in,:])) + if self.swing_state == self.NOT_SWINGING: + if max_swing > self.swing_thresh: + self.swing_state = self.SWINGING + self.swing_count_down = self.swing_count_down_thresh + self.new_event(events, 6) + print("SWINGING") + else: + self.swing_count_down -= self.n_samples + if self.swing_count_down <= 0: + self.swing_state = self.NOT_SWINGING + print("NOT_SWINGING") + + + if isDebug: + print("Mean: ", np.mean(n_arr[self.chan_out+1,:])) + print("done processing") + + #events + # 1: pulse sent + # 2: jittered, pulse_sent + # 3: triggered, not enabled + # 4: trigger armed, jittered + # 5: terminating pulse + # 6: swing detected + # machines: + # ENABLED vs. DISABLED vs. JITTERED + # states: + # READY, REFRACTORY, ARMED, FIRING + + if not self.enabled: + # DISABLED machine, has only READY state + if self.spw_condition(n_arr): + self.new_event(events, 3) + elif not self.jitter: + # ENABLED machine, has READY, REFRACTORY, FIRING states + if self.state == self.READY: + if self.spw_condition(n_arr): + self.stimulate() + self.new_event(events, 1) + self.state = self.FIRING + elif self.state == self.FIRING: + self.refractory_count_down = self.refractory_count_down_thresh-1 + self.state = self.REFRACTORY + self.new_event(events, 5) + elif self.state == self.REFRACTORY: + self.refractory_count_down -= self.n_samples + if self.refractory_count_down <= 0: + self.state = self.READY + else: + # checking for a leftover ARMED state + self.state = self.READY + else: + # JITTERED machine, has READY, ARMED, FIRING and REFRACTORY states + if self.state == self.READY: + if self.spw_condition(n_arr): + self.jitter_count_down = self.jitter_count_down_thresh + self.state = self.ARMED + self.new_event(events, 1, 1) + elif self.state == self.ARMED: + if self.jitter_count_down <= self.jitter_count_down_thresh: + self.new_event(events, 5, 1) + self.jitter_count_down -= self.n_samples + if self.jitter_count_down == 0: + self.stimulate() + self.new_event(events, 2) + self.state = self.FIRING + self.new_event(events, 1) + elif self.state == self.FIRING: + self.refractory_count_down = self.refractory_count_down_thresh-1 + self.state = self.REFRACTORY + self.new_event(events, 5) + else: + self.refractory_count_down -= self.n_samples + if self.refractory_count_down <= 0: + self.state = self.READY + + return events + + +pluginOp = SPWFinder() + +include "../plugin.pyx" \ No newline at end of file diff --git a/python_modules/spwfinder/test1.ipynb b/python_modules/old_style_plugins/spwfinder/test1.ipynb similarity index 100% rename from python_modules/spwfinder/test1.ipynb rename to python_modules/old_style_plugins/spwfinder/test1.ipynb diff --git a/python_modules/spwfinder/test_params.py b/python_modules/old_style_plugins/spwfinder/test_params.py similarity index 100% rename from python_modules/spwfinder/test_params.py rename to python_modules/old_style_plugins/spwfinder/test_params.py diff --git a/python_modules/spwrandom/.gitignore b/python_modules/old_style_plugins/spwrandom/.gitignore similarity index 100% rename from python_modules/spwrandom/.gitignore rename to python_modules/old_style_plugins/spwrandom/.gitignore diff --git a/python_modules/spwrandom/__init__.py b/python_modules/old_style_plugins/spwrandom/__init__.py similarity index 100% rename from python_modules/spwrandom/__init__.py rename to python_modules/old_style_plugins/spwrandom/__init__.py diff --git a/python_modules/spwrandom/get_test_data.py b/python_modules/old_style_plugins/spwrandom/get_test_data.py similarity index 100% rename from python_modules/spwrandom/get_test_data.py rename to python_modules/old_style_plugins/spwrandom/get_test_data.py diff --git a/python_modules/old_style_plugins/spwrandom/spwrandom.pyx b/python_modules/old_style_plugins/spwrandom/spwrandom.pyx new file mode 100644 index 0000000..0544d80 --- /dev/null +++ b/python_modules/old_style_plugins/spwrandom/spwrandom.pyx @@ -0,0 +1,175 @@ +import numpy as np +cimport numpy as np +from cython cimport view +import serial + + +isDebug = False + +class SPWFinder(object): + def __init__(self): + self.enabled = True + + self.refractory_count_down_thresh = 0 + self.refractory_count_down = 0 + self.refractory_time = 100. # time that the plugin will not react to trigger after one pulse + self.chan_in = 1 + self.chan_out = 0 + self.n_samples = 0 + self.chan_ripples = 1 + + + self.SWINGING = 1 + self.NOT_SWINGING = 0 + self.swing_state = self.NOT_SWINGING + self.swing_count_down_thresh = 0 + self.swing_count_down = 0 + self.swing_down_time = 2000. # time that it will be prevetned from firing after a swing event + + self.pulseNo = 0 + self.triggered = 0 + self.samplingRate = 0. + self.polarity = 0 + self.filter_a = [] + self.filter_b = [] + self.arduino = None + self.lfp_buffer = np.zeros((500,)) + self.lfp_buffer_max_count = 500 + self.READY=1 + self.ARMED=2 + self.REFRACTORY=3 + self.FIRING = 4 + self.state = self.READY + self.random_stim_rate = 1 # in Hertz + self.random_stim_rate_min = 0.1 + self.random_stim_rate_max = 5 + self.prob_threshold = 0. + + self.swing_thresh_min = 10. + self.swing_thresh_max = 20000. + self.swing_thresh_start = 1000. + self.swing_thresh = self.swing_thresh_start + + + print ("finished SPWrandom constructor") + + def startup(self, sampling_rate): + self.samplingRate = sampling_rate + print (self.samplingRate) + + print('starting random stimulation at rate ', self.random_stim_rate) + + self.enabled = 1 + try: + self.arduino = serial.Serial('/dev/ttyACM0', 57600) + except (OSError, serial.serialutil.SerialException): + print("Can't open Arduino") + + def plugin_name(self): + return "SPWRandom" + + def is_ready(self): + return 1 + + def param_config(self): + chan_labels = range(1,33) + return (("toggle", "enabled", True), + ("float_range", "random_stim_rate", self.random_stim_rate_min, self.random_stim_rate_max, self.random_stim_rate), + ("float_range", "swing_thresh", self.swing_thresh_min, self.swing_thresh_max, self.swing_thresh_start)) + + + def spw_condition(self, n_arr): + return np.random.random() < self.prob_threshold and self.swing_state == self.NOT_SWINGING + + def stimulate(self): + try: + self.arduino.write(b'1') + except AttributeError: + print("Can't send pulse") + self.pulseNo += 1 + print("generating pulse ", self.pulseNo) + + def new_event(self, events, code, channel=0, timestamp=None): + if not timestamp: + timestamp = self.n_samples + events.append({'type': 3, 'sampleNum': timestamp, 'eventId': code, 'eventChannel': channel}) + + def bufferfunction(self, n_arr): + #print("plugin start") + if isDebug: + print("shape: ", n_arr.shape) + events = [] + cdef int chan_in + cdef int chan_out + chan_in = self.chan_in - 1 + self.chan_out = self.chan_ripples + + self.n_samples = int(n_arr.shape[1]) + + if self.n_samples == 0: + return events + + # setting up frame dependent parameters + frame_time = 1000. * self.n_samples / self.samplingRate # in milliseconds + self.prob_threshold = frame_time * self.random_stim_rate / 1000. + self.refractory_count_down_thresh = int(self.refractory_time / frame_time) + self.swing_count_down_thresh = int(self.swing_down_time / frame_time) + + + # the swing detector state machine + max_swing = np.max(np.fabs(n_arr[chan_in,:])) + if self.swing_state == self.NOT_SWINGING: + if max_swing > self.swing_thresh: + self.swing_state = self.SWINGING + self.swing_count_down = self.swing_count_down_thresh + self.new_event(events, 6) + print("SWINGING") + else: + self.swing_count_down -= 1 + if self.swing_count_down == 0: + self.swing_state = self.NOT_SWINGING + print("NOT_SWINGING") + + + if isDebug: + print("Mean: ", np.mean(n_arr[self.chan_out+1,:])) + print("done processing") + + #events + # 1: pulse sent + # 2: jittered, pulse_sent + # 3: triggered, not enabled + # 4: trigger armed, jittered + # 5: terminating pulse + # 6: swing detected + # machines: + # ENABLED vs. DISABLED vs. JITTERED + # states: + # READY, REFRACTORY, ARMED, FIRING + + + if self.enabled: + # ENABLED machine, has READY, REFRACTORY, FIRING states + if self.state == self.READY: + if self.spw_condition(n_arr): + self.stimulate() + self.new_event(events, 1) + self.state = self.FIRING + elif self.state == self.FIRING: + self.refractory_count_down = self.refractory_count_down_thresh-1 + self.state = self.REFRACTORY + self.new_event(events, 5) + elif self.state == self.REFRACTORY: + self.refractory_count_down -= 1 + if self.refractory_count_down == 0: + self.state = self.READY + else: + # checking for a leftover ARMED state + self.state = self.READY + + return events + + +pluginOp = SPWFinder() + +include "../plugin.pyx" \ No newline at end of file diff --git a/python_modules/spwrandom/test_params.py b/python_modules/old_style_plugins/spwrandom/test_params.py similarity index 100% rename from python_modules/spwrandom/test_params.py rename to python_modules/old_style_plugins/spwrandom/test_params.py diff --git a/python_modules/swofinder/.gitignore b/python_modules/old_style_plugins/swofinder/.gitignore similarity index 100% rename from python_modules/swofinder/.gitignore rename to python_modules/old_style_plugins/swofinder/.gitignore diff --git a/python_modules/swofinder/__init__.py b/python_modules/old_style_plugins/swofinder/__init__.py similarity index 100% rename from python_modules/swofinder/__init__.py rename to python_modules/old_style_plugins/swofinder/__init__.py diff --git a/python_modules/swofinder/get_test_data.py b/python_modules/old_style_plugins/swofinder/get_test_data.py similarity index 100% rename from python_modules/swofinder/get_test_data.py rename to python_modules/old_style_plugins/swofinder/get_test_data.py diff --git a/python_modules/swofinder/swofinder.pyx b/python_modules/old_style_plugins/swofinder/swofinder.pyx similarity index 100% rename from python_modules/swofinder/swofinder.pyx rename to python_modules/old_style_plugins/swofinder/swofinder.pyx diff --git a/python_modules/plugin.pyx b/python_modules/plugin.pyx index 17507f9..016e918 100644 --- a/python_modules/plugin.pyx +++ b/python_modules/plugin.pyx @@ -1,3 +1,4 @@ +# cython: language_level=3 import sys import numpy as np cimport numpy as np @@ -7,13 +8,13 @@ from libc.string cimport memcpy sr = 1. -cdef extern from "../../PythonPlugin/PythonParamConfig.h": +cdef extern from "PythonParamConfig.h": enum paramType: TOGGLE, INT_SET, FLOAT_RANGE -cdef extern from "../../PythonPlugin/PythonParamConfig.h": +cdef extern from "PythonParamConfig.h": struct ParamConfig: paramType type char *name @@ -25,7 +26,7 @@ cdef extern from "../../PythonPlugin/PythonParamConfig.h": float startValue -cdef extern from "../../PythonPlugin/PythonEvent.h": +cdef extern from "PythonEvent.h": struct PythonEvent: unsigned char type int sampleNum @@ -37,26 +38,25 @@ cdef extern from "../../PythonPlugin/PythonEvent.h": # noinspection PyPep8Naming -cdef public void pluginStartup(float sampling_rate) with gil: - global sr +cdef public void pluginStartup(int nChans, float samplingRate, int *chanStates): + print("pre anything") global isDebug + print("after is debug") global pluginOp - #import scipy.signal - #import PIL - #print "executable is", sys.executable -# print "signal is", scipy.signal - if isDebug: - print("The python path is") - print(sys.path) - sr = sampling_rate - pluginOp.startup(sr) + cdef bint[:] states + if nChans == 0: + # pointer might be null + pluginOp.startup(nChans, samplingRate, []) + else: + states = ( chanStates) + pluginOp.startup(nChans, samplingRate, states) # noinspection PyPep8Naming -cdef public int getParamNum() with gil: +cdef public int getParamNum(): return len(pluginOp.param_config()) # noinspection PyPep8Naming -cdef public void getParamConfig(ParamConfig *params) with gil: +cdef public void getParamConfig(ParamConfig *params): cdef int *ent cdef char * par_name cdef size_t par_len @@ -66,19 +66,16 @@ cdef public void getParamConfig(ParamConfig *params) with gil: print("par[0], ", par[0]) print("par[1], ", par[1]) print("par[2], ", par[2]) - par_len = len(par[1])+1 print("par len: ",par_len) par_name = malloc(par_len) par_bytes = par[1].encode('utf-8') print("par_bytes: ", par_bytes) print("par_name 1: ", par_name) - memcpy(par_name, par_bytes, int(par_len-1)) #par_name = par_bytes par_name[par_len-1] = 0 print("par_name 2: ", par_name) - if par[0] == "toggle": params[i].type = TOGGLE params[i].name = par_name @@ -101,19 +98,17 @@ cdef public void getParamConfig(ParamConfig *params) with gil: # noinspection PyPep8Naming -cdef public void pluginFunction(float *data_buffer, int nChans, int nSamples, int nRealSamples, PythonEvent *events) with gil: +cdef public void pluginFunction(float *data_buffer, int nChans, int nSamples, int nRealSamples, PythonEvent *events): global sr n_arr = np.asarray( data_buffer) #pluginOp.set_events(events) #pm2 = PluginModule(pm) - if isDebug: print("sr: ", sr) samples_to_read = nRealSamples events_to_add = [] if samples_to_read > 0: events_to_add = pluginOp.bufferfunction(n_arr[:,0:samples_to_read]) - # struct PythonEvent: # unsigned char type # int sampleNum @@ -138,8 +133,17 @@ cdef public void pluginFunction(float *data_buffer, int nChans, int nSamples, in add_event(e_c, e_py) last_e_c = e_c last_e_c.nextEvent = NULL - -cdef void add_event(PythonEvent *e_c, object e_py) with gil: + +# noinspection PyPep8Naming +cdef public void eventFunction(int eventType, int sourceID, int subProcessorIdx, double timestamp, int sourceIndex): + pluginOp.handleEvents(eventType,sourceID,subProcessorIdx,timestamp,sourceIndex) + +# noinspection PyPep8Naming +cdef public void spikeFunction(int electrode, int sortedID, float[18] spikeSample): + n_arr = np.asarray( spikeSample) + pluginOp.handleSpike(electrode,sortedID,n_arr) + +cdef void add_event(PythonEvent *e_c, object e_py): e_c.type = e_py['type'] e_c.sampleNum = e_py['sampleNum'] if 'eventId' in e_py: @@ -152,29 +156,39 @@ cdef void add_event(PythonEvent *e_c, object e_py) with gil: e_c.eventData = e_py['eventData'] # TODO to be tested if this works with a numpy input -cdef public int pluginisready() with gil: + +cdef public int pluginisready(): return pluginOp.is_ready() + +# called from C++ updateSettings (not during acquisition) +cdef public void updateSettings(int nChans, float samplingRate): + pluginOp.update_settings(nChans, samplingRate) + +# called any time param button is changed (maybe during acquisition) +cdef public void channelChanged(int chan, int newState): + pluginOp.channel_changed(chan, newState) + # noinspection PyPep8Naming -cdef public void setIntParam(char *name, int value) with gil: +cdef public void setIntParam(char *name, int value): if isDebug: print("In Python: ", name, ": ", value) setattr(pluginOp, name.decode('utf-8'), value) # noinspection PyPep8Naming -cdef public void setFloatParam(char *name, float value) with gil: - # print "In Python: ", name, ": ", value +cdef public void setFloatParam(char *name, float value): + # print ("In Python: ", name, ": ", value) setattr(pluginOp, name.decode('utf-8'), value) # noinspection PyPep8Naming -cdef public int getIntParam(char *name) with gil: +cdef public int getIntParam(char *name): if isDebug: print("In Python getIntParam: ", name) value = getattr(pluginOp, name.decode('utf-8')) return value # noinspection PyPep8Naming -cdef public float getFloatParam(char *name) with gil: - # print "In Python: ", name, ": ", value +cdef public float getFloatParam(char *name): + # print( "In Python: ", name, ": ", value) value = getattr(pluginOp, name.decode('utf-8')) return value diff --git a/python_modules/pulse_test_delay/pulse_test_delay.pyx b/python_modules/pulse_test_delay/pulse_test_delay.pyx index 49cc08f..f81595c 100644 --- a/python_modules/pulse_test_delay/pulse_test_delay.pyx +++ b/python_modules/pulse_test_delay/pulse_test_delay.pyx @@ -1,49 +1,75 @@ -import sys +# noinspection PyUnresolvedReferences import numpy as np +# noinspection PyUnresolvedReferences cimport numpy as np +# noinspection PyUnresolvedReferences from cython cimport view import serial - isDebug = False -class Pulse_test_delay(object): +# noinspection PyPep8Naming +class pulse_test_delay(object): def __init__(self): - self.enabled = 1 + """initialize object data""" + self.Enabled = 1 + self.chan_enabled = [] + self.chan_in = 0 self.thresh_min = -2 self.thresh_max = 2 self.thresh_start = 0 self.threshold = self.thresh_start self.arduino = None - - self.triggered = 0 + self.samplingRate = 0 + def startup(self, nchans, srate, states): + """to be run upon startup""" + self.update_settings(nchans, srate) + for chan in range(nchans): + if not states[chan]: + self.channel_changed(chan, False) - def startup(self, sr): - self.samplingRate = sr - print (self.samplingRate) self.arduino = serial.Serial('/dev/tty.usbmodem45561', 57600) print ("Arduino: ", self.arduino) - self.enabled = 1 + self.Enabled = 1 def plugin_name(self): + """tells OE the name of the program""" return "pulse_test_delay" def is_ready(self): - return 1 + """tells OE everything ran smoothly""" + return self.Enabled def param_config(self): + """return button, sliders, etc to be present in the editor OE side""" chan_labels = list(range(1,44)) return (("toggle", "Enabled", True), ("int_set", "chan_in", chan_labels), ("float_range", "threshold", self.thresh_min, self.thresh_max, self.thresh_start)) + def update_settings(self, nchans, srate): + """handle changing number of channels and sample rates""" + if srate != self.samplingRate: + self.samplingRate = srate + print(self.samplingRate) + + old_nchans = len(self.chan_enabled) + if old_nchans > nchans: + del self.chan_enabled[nchans:] + elif len(self.chan_enabled) < nchans: + self.chan_enabled.extend([True] * (nchans - old_nchans)) + + def channel_changed(self, chan, state): + """do something when channels are turned on or off in PARAMS tab""" + self.chan_enabled[chan] = state def bufferfunction(self, n_arr): + """Access to voltage data buffer. Returns events""" #print ("plugin start") events = [] cdef int chan_in @@ -56,8 +82,11 @@ class Pulse_test_delay(object): #print ('triggered') events.append({'type': 3, 'sampleNum': 10, 'eventId': 1}) self.triggered = 1 - self.arduino.write(b'1') - #n_arr[chan_in-2,:] = 1 * np.ones((1,n_samples)) + try: + self.arduino.write(b'1') + except AttributeError: + print("Can't send pulse") + elif self.triggered: self.triggered = 0 events.append({'type': 3, 'sampleNum': 10, 'eventId': 5}) @@ -66,12 +95,15 @@ class Pulse_test_delay(object): pass # n_arr[chan_in-2,:] = np.zeros((1,n_samples)) + return events + def handleEvents(self, eventType, sourceID, subProcessorIdx, timestamp, sourceIndex): + """handle events passed from OE""" - #print( "plugin end") - return events + def handleSpike(self, electrode, sortedID, n_arr): + """handle spikes passed from OE""" -pluginOp = Pulse_test_delay() -include "../plugin.pyx" +pluginOp = pulse_test_delay() +include '../plugin.pyx' diff --git a/python_modules/pulse_test_delay/setup.py b/python_modules/pulse_test_delay/setup.py new file mode 100644 index 0000000..6c9f122 --- /dev/null +++ b/python_modules/pulse_test_delay/setup.py @@ -0,0 +1,33 @@ +from distutils.core import setup, Extension +from Cython.Build import cythonize +import numpy +import runpy + +cfg = runpy.run_path('../.config.py') + +setup( + name="pulse_test_delay", + include_dirs=[numpy.get_include(), cfg['PYTHON_PLUGIN_SRC_DIR']], + ext_modules=cythonize( + Extension( + 'pulse_test_delay', + sources=["pulse_test_delay.pyx"], + export_symbols=[ + 'pluginStartup', + 'pluginisready', + 'getParamNum', + 'getParamConfig', + 'pluginFunction', + 'eventFunction', + 'spikeFunction', + 'setIntParam', + 'setFloatParam', + 'getIntParam', + 'getFloatParam', + 'updateSettings', + 'channelChanged' + ] + ), + language_level=3 + ) +) \ No newline at end of file diff --git a/python_modules/spwdouble/setup.py b/python_modules/spwdouble/setup.py new file mode 100644 index 0000000..a5c620f --- /dev/null +++ b/python_modules/spwdouble/setup.py @@ -0,0 +1,34 @@ +from distutils.core import setup, Extension +from Cython.Build import cythonize +import numpy +import runpy + +cfg = runpy.run_path('../.config.py') + + +setup( + name="spwdouble", + include_dirs=[numpy.get_include(), cfg['PYTHON_PLUGIN_SRC_DIR']], + ext_modules=cythonize( + Extension( + 'spwdouble', + sources=["spwdouble.pyx"], + export_symbols=[ + 'pluginStartup', + 'pluginisready', + 'getParamNum', + 'getParamConfig', + 'pluginFunction', + 'eventFunction', + 'spikeFunction', + 'setIntParam', + 'setFloatParam', + 'getIntParam', + 'getFloatParam', + 'updateSettings', + 'channelChanged' + ] + ), + language_level=3 + ) +) \ No newline at end of file diff --git a/python_modules/spwdouble/spwdouble.pyx b/python_modules/spwdouble/spwdouble.pyx index fe7fd8a..b6d2998 100644 --- a/python_modules/spwdouble/spwdouble.pyx +++ b/python_modules/spwdouble/spwdouble.pyx @@ -1,5 +1,8 @@ +# noinspection PyUnresolvedReferences import numpy as np +# noinspection PyUnresolvedReferences cimport numpy as np +# noinspection PyUnresolvedReferences from cython cimport view import serial import scipy.signal @@ -7,14 +10,19 @@ import logging isDebug = False -class SPWFinder(object): + +# noinspection PyPep8Naming +class spwdouble(object): def __init__(self): - self.enabled = True + """initialize object data""" + self.Enabled = 1 + self.chan_enabled = [] self.jitter_count_down_thresh = 0 self.jitter_count_down = 0 self.jitter_time = 200. # in ms self.refractory_count_down_thresh = 0 + self.samples_for_average = 0 self.refractory_count_down = 0 self.refractory_time = 100. # time that the plugin will not react to trigger after one pulse self.double_count_down_thresh = 0 @@ -75,42 +83,37 @@ class SPWFinder(object): self.FIRING2 = 6 self.state = self.READY logging.basicConfig(filename='spwdouble.log', format='%(asctime)s %(message)s', level=logging.DEBUG) - print ("finished SPWfinder constructor") - - def startup(self, sampling_rate): - self.samplingRate = sampling_rate - print (self.samplingRate) - - self.filter_b, self.filter_a = scipy.signal.butter(3, - (self.band_lo/(self.samplingRate/2), self.band_hi/(self.samplingRate/2)), - 'pass') - print(self.filter_a) - print(self.filter_b) - print(self.band_lo) - print(self.band_hi) - print(self.band_lo/(self.samplingRate/2)) - print(self.band_hi/(self.samplingRate/2)) - self.enabled = 1 + + def startup(self, nchans, srate, states): + """to be run upon startup""" + self.update_settings(nchans, srate) + for chan in range(nchans): + if not states[chan]: + self.channel_changed(chan, False) + + self.Enabled = 1 try: self.arduino = serial.Serial('/dev/ttyACM0', 57600) except (OSError, serial.serialutil.SerialException): print("Can't open Arduino") def plugin_name(self): - return "SPWFinder" + """tells OE the name of the program""" + return "spwdouble" def is_ready(self): - return 1 + """tells OE everything ran smoothly""" + return self.Enabled def param_config(self): + """return button, sliders, etc to be present in the editor OE side""" chan_labels = range(1,33) - return (("toggle", "enabled", True), + return (("toggle", "Enabled", True), ("int_set", "chan_in", chan_labels), ("float_range", "threshold", self.thresh_min, self.thresh_max, self.thresh_start), ("float_range", "swing_thresh", self.swing_thresh_min, self.swing_thresh_max, self.swing_thresh_start), ("float_range", "averaging_time", self.averaging_time_min, self.averaging_time_max, self.averaging_time_start)) - def spw_condition(self, n_arr): return (np.mean(n_arr[self.chan_out+1,:]) > self.threshold) and self.swing_state == self.NOT_SWINGING @@ -128,8 +131,34 @@ class SPWFinder(object): timestamp = self.n_samples events.append({'type': 3, 'sampleNum': timestamp, 'eventId': code, 'eventChannel': channel}) + def update_settings(self, nchans, srate): + """handle changing number of channels and sample rates""" + if srate != self.samplingRate: + self.samplingRate = srate + + # noinspection PyTupleAssignmentBalance + self.filter_b, self.filter_a = scipy.signal.butter(3, + (self.band_lo/(self.samplingRate/2), self.band_hi/(self.samplingRate/2)), + 'pass') + print(self.filter_a) + print(self.filter_b) + print(self.band_lo) + print(self.band_hi) + print(self.band_lo/(self.samplingRate/2)) + print(self.band_hi/(self.samplingRate/2)) + + old_nchans = len(self.chan_enabled) + if old_nchans > nchans: + del self.chan_enabled[nchans:] + elif len(self.chan_enabled) < nchans: + self.chan_enabled.extend([True] * (nchans - old_nchans)) + + def channel_changed(self, chan, state): + """do something when channels are turned on or off in PARAMS tab""" + self.chan_enabled[chan] = state + def bufferfunction(self, n_arr): - #print("plugin start") + """Access to voltage data buffer. Returns events""" if isDebug: print("shape: ", n_arr.shape) events = [] @@ -148,7 +177,7 @@ class SPWFinder(object): self.double_count_down_thresh = self.double_time * self.samplingRate / 1000. self.swing_count_down_thresh = self.swing_down_time * self.samplingRate / 1000. self.jitter_count_down_thresh = self.jitter_time * self.samplingRate / 1000. - self.samples_for_average = self.averaging_time * self.samplingRate / 1000. + self.samples_for_average = int(self.averaging_time * self.samplingRate / 1000.) signal_to_filter = np.hstack((self.lfp_buffer, n_arr[chan_in,:])) signal_to_filter = signal_to_filter - signal_to_filter[-1] @@ -197,7 +226,7 @@ class SPWFinder(object): # finite state machine if self.state == self.READY: if self.spw_condition(n_arr): - if self.enabled: + if self.Enabled: logging.debug('got spw') self.jitter_count_down = self.jitter_count_down_thresh self.state = self.ARMED @@ -249,10 +278,15 @@ class SPWFinder(object): self.state = self.READY logging.debug('READY') - return events + def handleEvents(self, eventType, sourceID, subProcessorIdx, timestamp, sourceIndex): + """handle events passed from OE""" + + def handleSpike(self, electrode, sortedID, n_arr): + """handle spikes passed from OE""" + -pluginOp = SPWFinder() +pluginOp = spwdouble() -include "../plugin.pyx" \ No newline at end of file +include '../plugin.pyx' diff --git a/python_modules/spwfinder/setup.py b/python_modules/spwfinder/setup.py new file mode 100644 index 0000000..d6aecb6 --- /dev/null +++ b/python_modules/spwfinder/setup.py @@ -0,0 +1,33 @@ +from distutils.core import setup, Extension +from Cython.Build import cythonize +import numpy +import runpy + +cfg = runpy.run_path('../.config.py') + +setup( + name= "spwfinder", + include_dirs = [numpy.get_include(), cfg['PYTHON_PLUGIN_SRC_DIR']], + ext_modules = cythonize( + Extension( + 'spwfinder', + sources=["spwfinder.pyx"], + export_symbols=[ + 'pluginStartup', + 'pluginisready', + 'getParamNum', + 'getParamConfig', + 'pluginFunction', + 'eventFunction', + 'spikeFunction', + 'setIntParam', + 'setFloatParam', + 'getIntParam', + 'getFloatParam', + 'updateSettings', + 'channelChanged' + ] + ), + language_level=3 + ) +) \ No newline at end of file diff --git a/python_modules/spwfinder/spwfinder.pyx b/python_modules/spwfinder/spwfinder.pyx index 74e1a46..7ae3d67 100644 --- a/python_modules/spwfinder/spwfinder.pyx +++ b/python_modules/spwfinder/spwfinder.pyx @@ -1,22 +1,30 @@ +# import sys +# noinspection PyUnresolvedReferences import numpy as np +import scipy.signal +import serial +# noinspection PyUnresolvedReferences cimport numpy as np +# noinspection PyUnresolvedReferences from cython cimport view -import serial -import scipy.signal - isDebug = False -class SPWFinder(object): + +# noinspection PyPep8Naming +class spwfinder(object): def __init__(self): - self.enabled = True + """initialize object data""" + self.Enabled = 1 + self.chan_enabled = [] + self.jitter = False self.jitter_count_down_thresh = 0 self.jitter_count_down = 0 - self.jitter_time = 200. # in ms + self.jitter_time = 200. # in ms self.refractory_count_down_thresh = 0 self.refractory_count_down = 0 - self.refractory_time = 100. # time that the plugin will not react to trigger after one pulse + self.refractory_time = 100. # time that the plugin will not react to trigger after one pulse self.chan_in = 1 self.chan_out = 0 self.n_samples = 0 @@ -36,11 +44,10 @@ class SPWFinder(object): self.thresh_start = 30. self.threshold = self.thresh_start - self.averaging_time_min = 10. self.averaging_time_max = 50. self.averaging_time_start = 20. - self.averaging_time = self.averaging_time_start # in ms + self.averaging_time = self.averaging_time_start # in ms self.samples_for_average = 0 self.swing_thresh_min = 10. @@ -53,7 +60,7 @@ class SPWFinder(object): self.swing_state = self.NOT_SWINGING self.swing_count_down_thresh = 0 self.swing_count_down = 0 - self.swing_down_time = 2000. # time that it will be prevetned from firing after a swing event + self.swing_down_time = 2000. # time that it will be prevetned from firing after a swing event self.pulseNo = 0 self.triggered = 0 @@ -65,28 +72,20 @@ class SPWFinder(object): self.lfp_buffer_max_count = 1000 self.lfp_buffer = np.zeros((self.lfp_buffer_max_count,)) self.spw_power = 0. - self.READY=1 - self.ARMED=2 - self.REFRACTORY=3 + self.READY = 1 + self.ARMED = 2 + self.REFRACTORY = 3 self.FIRING = 4 self.state = self.READY - print ("finished SPWfinder constructor") - - def startup(self, sampling_rate): - self.samplingRate = sampling_rate - print (self.samplingRate) - - self.filter_b, self.filter_a = scipy.signal.butter(3, - (self.band_lo/(self.samplingRate/2), self.band_hi/(self.samplingRate/2)), - 'pass') - print(self.filter_a) - print(self.filter_b) - print(self.band_lo) - print(self.band_hi) - print(self.band_lo/(self.samplingRate/2)) - print(self.band_hi/(self.samplingRate/2)) - self.enabled = 1 + def startup(self, nchans, srate, states): + """to be run upon startup""" + self.update_settings(nchans, srate) + for chan in range(nchans): + if not states[chan]: + self.channel_changed(chan, False) + + self.Enabled = 1 self.jitter = 0 try: self.arduino = serial.Serial('/dev/ttyACM0', 57600) @@ -94,14 +93,18 @@ class SPWFinder(object): print("Can't open Arduino") def plugin_name(self): - return "SPWFinder" + """tells OE the name of the program""" + return "spwfinder" def is_ready(self): - return 1 + """tells OE everything ran smoothly""" + return self.Enabled def param_config(self): + """return button, sliders, etc to be present in the editor OE side""" + """return button, sliders, etc to be present in the editor OE side""" chan_labels = range(1, 33) - return (("toggle", "enabled", True), + return (("toggle", "Enabled", True), ("int_set", "chan_in", chan_labels), ("float_range", "threshold", self.thresh_min, self.thresh_max, self.thresh_start), ("float_range", "swing_thresh", self.swing_thresh_min, self.swing_thresh_max, self.swing_thresh_start), @@ -123,7 +126,37 @@ class SPWFinder(object): timestamp = self.n_samples events.append({'type': 3, 'sampleNum': timestamp, 'eventId': code, 'eventChannel': channel}) + def update_settings(self, nchans, srate): + """handle changing number of channels and sample rates""" + if srate != self.samplingRate: + self.samplingRate = srate + + # noinspection PyTupleAssignmentBalance + self.filter_b, self.filter_a = scipy.signal.butter(3, + (self.band_lo / (self.samplingRate / 2), + self.band_hi / (self.samplingRate / 2)), + btype='bandpass', + output='ba') + print(self.filter_a) + print(self.filter_b) + print(self.band_lo) + print(self.band_hi) + print(self.band_lo / (self.samplingRate / 2)) + print(self.band_hi / (self.samplingRate / 2)) + + old_nchans = len(self.chan_enabled) + if old_nchans > nchans: + del self.chan_enabled[nchans:] + elif len(self.chan_enabled) < nchans: + self.chan_enabled.extend([True] * (nchans - old_nchans)) + + def channel_changed(self, chan, state): + """do something when channels are turned on or off in PARAMS tab""" + self.chan_enabled[chan] = state + def bufferfunction(self, n_arr): + """Access to voltage data buffer. Returns events""" + """Access to voltage data buffer. Returns events""" #print("plugin start") if isDebug: print("shape: ", n_arr.shape) @@ -187,7 +220,7 @@ class SPWFinder(object): # states: # READY, REFRACTORY, ARMED, FIRING - if not self.enabled: + if not self.Enabled: # DISABLED machine, has only READY state if self.spw_condition(n_arr): self.new_event(events, 3) @@ -236,7 +269,17 @@ class SPWFinder(object): return events + def handleEvents(self, eventType,sourceID,subProcessorIdx,timestamp,sourceIndex): + """handle events passed from OE""" + pass + + def handleSpike(self, electrode, sortedID, n_arr): + """handle spikes passed from OE""" + pass + +pluginOp = spwfinder() + +include '../plugin.pyx' + -pluginOp = SPWFinder() -include "../plugin.pyx" \ No newline at end of file diff --git a/python_modules/spwrandom/setup.py b/python_modules/spwrandom/setup.py new file mode 100644 index 0000000..0e747bd --- /dev/null +++ b/python_modules/spwrandom/setup.py @@ -0,0 +1,33 @@ +from distutils.core import setup, Extension +from Cython.Build import cythonize +import numpy +import runpy + +cfg = runpy.run_path('../.config.py') + +setup( + name= "spwrandom", + include_dirs = [numpy.get_include(), cfg['PYTHON_PLUGIN_SRC_DIR']], + ext_modules = cythonize( + Extension( + 'spwrandom', + sources=["spwrandom.pyx"], + export_symbols=[ + 'pluginStartup', + 'pluginisready', + 'getParamNum', + 'getParamConfig', + 'pluginFunction', + 'eventFunction', + 'spikeFunction', + 'setIntParam', + 'setFloatParam', + 'getIntParam', + 'getFloatParam', + 'updateSettings', + 'channelChanged' + ] + ), + language_level=3 + ) +) diff --git a/python_modules/spwrandom/spwrandom.pyx b/python_modules/spwrandom/spwrandom.pyx index 0544d80..9c3afd6 100644 --- a/python_modules/spwrandom/spwrandom.pyx +++ b/python_modules/spwrandom/spwrandom.pyx @@ -1,14 +1,20 @@ +# noinspection PyUnresolvedReferences import numpy as np +# noinspection PyUnresolvedReferences cimport numpy as np +# noinspection PyUnresolvedReferences from cython cimport view import serial - isDebug = False -class SPWFinder(object): + +# noinspection PyPep8Naming +class spwrandom(object): def __init__(self): - self.enabled = True + """initialize object data""" + self.Enabled = 1 + self.chan_enabled = [] self.refractory_count_down_thresh = 0 self.refractory_count_down = 0 @@ -50,34 +56,36 @@ class SPWFinder(object): self.swing_thresh_start = 1000. self.swing_thresh = self.swing_thresh_start - - print ("finished SPWrandom constructor") - - def startup(self, sampling_rate): - self.samplingRate = sampling_rate - print (self.samplingRate) + def startup(self, nchans, srate, states): + """to be run upon startup""" + self.update_settings(nchans, srate) + for chan in range(nchans): + if not states[chan]: + self.channel_changed(chan, False) print('starting random stimulation at rate ', self.random_stim_rate) - self.enabled = 1 + self.Enabled = 1 try: self.arduino = serial.Serial('/dev/ttyACM0', 57600) except (OSError, serial.serialutil.SerialException): print("Can't open Arduino") def plugin_name(self): - return "SPWRandom" + """tells OE the name of the program""" + return "spwrandom" def is_ready(self): - return 1 + """tells OE everything ran smoothly""" + return self.Enabled def param_config(self): + """return button, sliders, etc to be present in the editor OE side""" chan_labels = range(1,33) - return (("toggle", "enabled", True), + return (("toggle", "Enabled", True), ("float_range", "random_stim_rate", self.random_stim_rate_min, self.random_stim_rate_max, self.random_stim_rate), ("float_range", "swing_thresh", self.swing_thresh_min, self.swing_thresh_max, self.swing_thresh_start)) - def spw_condition(self, n_arr): return np.random.random() < self.prob_threshold and self.swing_state == self.NOT_SWINGING @@ -94,8 +102,25 @@ class SPWFinder(object): timestamp = self.n_samples events.append({'type': 3, 'sampleNum': timestamp, 'eventId': code, 'eventChannel': channel}) + def update_settings(self, nchans, srate): + """handle changing number of channels and sample rates""" + if srate != self.samplingRate: + self.samplingRate = srate + print (self.samplintRate) + + old_nchans = len(self.chan_enabled) + if old_nchans > nchans: + del self.chan_enabled[nchans:] + elif len(self.chan_enabled) < nchans: + self.chan_enabled.extend([True] * (nchans - old_nchans)) + + def channel_changed(self, chan, state): + """do something when channels are turned on or off in PARAMS tab""" + self.chan_enabled[chan] = state + def bufferfunction(self, n_arr): - #print("plugin start") + """Access to voltage data buffer. Returns events""" + #print("plugin start") if isDebug: print("shape: ", n_arr.shape) events = [] @@ -130,7 +155,6 @@ class SPWFinder(object): self.swing_state = self.NOT_SWINGING print("NOT_SWINGING") - if isDebug: print("Mean: ", np.mean(n_arr[self.chan_out+1,:])) print("done processing") @@ -148,7 +172,7 @@ class SPWFinder(object): # READY, REFRACTORY, ARMED, FIRING - if self.enabled: + if self.Enabled: # ENABLED machine, has READY, REFRACTORY, FIRING states if self.state == self.READY: if self.spw_condition(n_arr): @@ -169,7 +193,13 @@ class SPWFinder(object): return events + def handleEvents(self, eventType, sourceID, subProcessorIdx, timestamp, sourceIndex): + """handle events passed from OE""" + + def handleSpike(self, electrode, sortedID, n_arr): + """handle spikes passed from OE""" + -pluginOp = SPWFinder() +pluginOp = spwrandom() -include "../plugin.pyx" \ No newline at end of file +include '../plugin.pyx' diff --git a/python_modules/template/setup.py b/python_modules/template/setup.py new file mode 100644 index 0000000..5a61026 --- /dev/null +++ b/python_modules/template/setup.py @@ -0,0 +1,33 @@ +from distutils.core import setup, Extension +from Cython.Build import cythonize +import numpy +import runpy + +cfg = runpy.run_path('../.config.py') + +setup( + name="EXAMPLE", + include_dirs=[numpy.get_include(), cfg['PYTHON_PLUGIN_SRC_DIR']], + ext_modules=cythonize( + Extension( + 'EXAMPLE', + sources=["EXAMPLE.pyx"], + export_symbols=[ + 'pluginStartup', + 'pluginisready', + 'getParamNum', + 'getParamConfig', + 'pluginFunction', + 'eventFunction', + 'spikeFunction', + 'setIntParam', + 'setFloatParam', + 'getIntParam', + 'getFloatParam', + 'updateSettings', + 'channelChanged' + ] + ), + language_level=3 + ) +) diff --git a/python_modules/template/template.pyx b/python_modules/template/template.pyx new file mode 100644 index 0000000..27b2f2f --- /dev/null +++ b/python_modules/template/template.pyx @@ -0,0 +1,61 @@ +import sys +import numpy as np +cimport numpy as np +from cython cimport view + +isDebug = False + +class EXAMPLE(object): + def __init__(self): + """initialize object data""" + self.Enabled = 1 + self.samplingRate = 0. + self.chan_enabled = [] + + def startup(self, nchans, srate, states): + """to be run upon startup""" + self.update_settings(nchans, srate) + for chan in range(nchans): + if not states[chan]: + self.channel_changed(chan, False) + + def plugin_name(self): + """tells OE the name of the program""" + return "EXAMPLE" + + def is_ready(self): + """tells OE everything ran smoothly""" + return self.Enabled + + def param_config(self): + """return button, sliders, etc to be present in the editor OE side""" + return [] + + def update_settings(self, nchans, srate): + """handle changing number of channels and sample rates""" + self.samplingRate = srate + + old_nchans = len(self.chan_enabled) + if old_nchans > nchans: + del self.chan_enabled[nchans:] + elif len(self.chan_enabled) < nchans: + self.chan_enabled.extend([True] * (nchans - old_nchans)) + + def channel_changed(self, chan, state): + """do something when channels are turned on or off in PARAMS tab""" + self.chan_enabled[chan] = state + + def bufferfunction(self, n_arr): + """Access to voltage data buffer. Returns events""" + events = [] + return events + + def handleEvents(self, eventType,sourceID,subProcessorIdx,timestamp,sourceIndex): + """handle events passed from OE""" + + def handleSpike(self, electrode, sortedID, n_arr): + """handle spikes passed from OE""" + +pluginOp = EXAMPLE() + +include '../plugin.pyx' diff --git a/python_modules/test/setup.py b/python_modules/test/setup.py new file mode 100644 index 0000000..7bfc949 --- /dev/null +++ b/python_modules/test/setup.py @@ -0,0 +1,33 @@ +from distutils.core import setup, Extension +from Cython.Build import cythonize +import numpy +import runpy + +cfg = runpy.run_path('../.config.py') + +setup( + name="test", + include_dirs=[numpy.get_include(), cfg['PYTHON_PLUGIN_SRC_DIR']], + ext_modules=cythonize( + Extension( + 'test', + sources=["test.pyx"], + export_symbols=[ + 'pluginStartup', + 'pluginisready', + 'getParamNum', + 'getParamConfig', + 'pluginFunction', + 'eventFunction', + 'spikeFunction', + 'setIntParam', + 'setFloatParam', + 'getIntParam', + 'getFloatParam', + 'updateSettings', + 'channelChanged' + ] + ), + language_level=3 + ) +) diff --git a/python_modules/test/test.pyx b/python_modules/test/test.pyx new file mode 100644 index 0000000..d15c0d2 --- /dev/null +++ b/python_modules/test/test.pyx @@ -0,0 +1,72 @@ +import sys +import numpy as np +cimport numpy as np +from cython cimport view + +isDebug = False + +class test(object): + def __init__(self): + """initialize object data""" + self.Enabled = 1 + self.samplingRate = 0. + self.chan_enabled = [] + + def startup(self, nchans, srate, states): + """to be run upon startup""" + self.update_settings(nchans, srate) + for chan in range(nchans): + if not states[chan]: + self.channel_changed(chan, False) + + def plugin_name(self): + """tells OE the name of the program""" + return "test" + + def is_ready(self): + """tells OE everything ran smoothly""" + return self.Enabled + + def param_config(self): + """return button, sliders, etc to be present in the editor OE side""" + return [] + + def update_settings(self, nchans, srate): + """handle changing number of channels and sample rates""" + print('Setting sample rate to', srate) + self.samplingRate = srate + + old_nchans = len(self.chan_enabled) + if old_nchans > nchans: + print('Removing all but first', nchans, 'channels') + del self.chan_enabled[nchans:] + + elif len(self.chan_enabled) < nchans: + print('Adding', nchans - old_nchans, 'new channels') + self.chan_enabled.extend([True] * (nchans - old_nchans)) + + def channel_changed(self, chan, state): + """do something when channels are turned on or off in PARAMS tab""" + if state: + print('Enabling channel', chan) + else: + print('Disabling channel', chan) + + self.chan_enabled[chan] = state + + def bufferfunction(self, n_arr): + """Access to voltage data buffer. Returns events""" + events = [] + return events + + def handleEvents(self, eventType,sourceID,subProcessorIdx,timestamp,sourceIndex): + """handle events passed from OE""" + + def handleSpike(self, electrode, sortedID, n_arr): + """handle spikes passed from OE""" + + +pluginOp = test() + +include '../plugin.pyx' + diff --git a/python_modules/test2/setup.py b/python_modules/test2/setup.py new file mode 100644 index 0000000..a5222a9 --- /dev/null +++ b/python_modules/test2/setup.py @@ -0,0 +1,35 @@ +from distutils.core import setup, Extension +from Cython.Build import cythonize +import numpy +import runpy + +cfg = runpy.run_path('../.config.py') + +setup( + name= "test2", + include_dirs=[numpy.get_include(), cfg['PYTHON_PLUGIN_SRC_DIR']], + ext_modules = cythonize( + Extension( + 'test2', + sources = ["test2.pyx"], + export_symbols = [ + 'pluginStartup', + 'pluginisready', + 'getParamNum', + 'getParamConfig', + 'pluginFunction', + 'eventFunction', + 'spikeFunction', + 'setIntParam', + 'setFloatParam', + 'getIntParam', + 'getFloatParam', + 'updateSettings', + 'channelChanged' + ] + ), + language_level=3 + ) +) + + diff --git a/python_modules/test2/test2.pyx b/python_modules/test2/test2.pyx new file mode 100644 index 0000000..106c928 --- /dev/null +++ b/python_modules/test2/test2.pyx @@ -0,0 +1,67 @@ +# cython: language_level=3 +import sys +import numpy as np +cimport numpy as np +from cython cimport view + +isDebug = False + +class test2(object): + def __init__(self): + """initialize object data""" + self.Enabled = 1 + self.samplingRate = 0. + self.chan_enabled = [] + + def startup(self, nchans, srate, states): + """to be run upon startup""" + self.update_settings(nchans, srate) + for chan in range(nchans): + if not states[chan]: + self.channel_changed(chan, False) + + def plugin_name(self): + """tells OE the name of the program""" + return "test2" + + def is_ready(self): + """tells OE everything ran smoothly""" + return self.Enabled + + def param_config(self): + """return button, sliders, etc to be present in the editor OE side""" + return [] + + def update_settings(self, nchans, srate): + """handle changing number of channels and sample rates""" + self.samplingRate = srate + + old_nchans = len(self.chan_enabled) + if old_nchans > nchans: + del self.chan_enabled[nchans:] + + elif len(self.chan_enabled) < nchans: + self.chan_enabled.extend([True] * (nchans - old_nchans)) + + def channel_changed(self, chan, state): + """do something when channels are turned on or off in PARAMS tab""" + self.chan_enabled[chan] = state + + def bufferfunction(self, n_arr): + """Access to voltage data buffer. Returns events""" + events = [] + return events + + def handleEvents(self, eventType,sourceID,subProcessorIdx,timestamp,sourceIndex): + """handle events passed from OE""" + + def handleSpike(self, electrode, sortedID, n_arr): + """handle spikes passed from OE""" + + +pluginOp = test2() + +include '../plugin.pyx' + + + diff --git a/python_modules/testML/setup.py b/python_modules/testML/setup.py new file mode 100644 index 0000000..a2d4b9c --- /dev/null +++ b/python_modules/testML/setup.py @@ -0,0 +1,9 @@ +from distutils.core import setup, Extension +from Cython.Build import cythonize +import numpy + +setup( + name= "testML", + ext_modules = cythonize("testML.pyx"), + include_dirs = [numpy.get_include()] + ) diff --git a/python_modules/testML/subClient.py b/python_modules/testML/subClient.py new file mode 100644 index 0000000..faa08aa --- /dev/null +++ b/python_modules/testML/subClient.py @@ -0,0 +1,33 @@ +import zmq +import _pickle as pickle +import numpy as np +import matplotlib.pyplot as plt +plt.ion() +fig, ax = plt.subplots() +x, y = [],[] +sc = ax.scatter(x,y) +plt.xlim(-200,200) +plt.ylim(-200,200) +plt.draw() + +# Socket to talk to server +context = zmq.Context() +socket = context.socket(zmq.SUB) +port = "5556" +socket.connect ("tcp://localhost:%s" % port) + +topicfilter = "10001" +socket.setsockopt_string(zmq.SUBSCRIBE, '') + + +total_value = 0 +while True: + data = pickle.loads(socket.recv_pyobj()) + print(np.shape(data)) + obs,d=np.shape(data) + for i in range(0,obs): + x.append(data[i,0]) + y.append(data[i,1]) + sc.set_offsets(np.c_[x,y]) + fig.canvas.draw_idle() + plt.pause(0.1)